diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js index 4b053bb80..659a07374 100644 --- a/src/static/js/broadcast_slider.js +++ b/src/static/js/broadcast_slider.js @@ -16,212 +16,6 @@ var _ = require('./underscore'); var padmodals = require('./pad_modals').padmodals; -var sliderui = require('./sliderui'); -require('./jquery.class'); - -$.Class("RevisionSlider", - {//statics - PLAYBACK_DELAY: 400, - }, - {//instance - init: function (connection, root_element) { - this.connection = connection; - this.revision_number = this.connection.getCurrentRevision().revnum; - this.timestamp = 0; - this.is_playing = false; - // if there was a revision specified in the 'location.hash', jump to it. - if (window.location.hash.length > 1) { - var rev = Number(window.location.hash.substr(1)); - if(!isNaN(rev)) - this.revision_number = rev; - } - - console.log("New RevisionSlider, current_revision = %d", this.revision_number); - // parse the various elements we need: - this.elements = {}; - this.loadElements(root_element); - var _this = this; - this.slider = new SliderUI(this.elements.slider_bar, - options = { - value: this.revision_number, - max: this.connection.getHeadRevision(), - change: function () { _this.onChange.apply(_this, arguments); }, - slide: function () { _this.onSlide.apply(_this, arguments); }, - }); - this.loadSavedRevisionHandles(); - this.slider.render(); - - this._mouseInit(); - - this.goToRevision(this.revision_number); - }, - onChange: function (value) { - console.log("in change handler:", value); - this.goToRevision(value); - }, - onSlide: function (value) { - console.log("in slide handler:", value); - this.goToRevision(value); - }, - loadElements: function (root_element) { - this.elements.root = root_element; - //this.elements['slider-handle'] = root_element.first("#ui-slider-handle"); - this.elements.slider_bar = root_element.find("#ui-slider-bar"); - this.elements.slider = root_element.find("#timeslider-slider"); - this.elements.button_left = root_element.find("#leftstep"); - this.elements.button_right = root_element.find("#rightstep"); - this.elements.button_play = root_element.find("#playpause_button"); - this.elements.timestamp = root_element.find("#timer"); - this.elements.revision_label = root_element.find("#revision_label"); - this.elements.revision_date = root_element.find("#revision_date"); - this.elements.authors = root_element.first("#authorsList"); - }, - loadSavedRevisionHandles: function () { - for (var r in this.connection.savedRevisions) { - var rev = this.connection.savedRevisions[r]; - this.slider.createHandle(rev.revNum, "star"); - } - }, - playpause: function () { - if (this.is_playing) { - this.is_playing = false; - return; - } - - var revnum = this.revision_number; - if (revnum == this.connection.getHeadRevision()) - revnum = 0; - - var _this = this; - var keepPlaying = function (current_revnum) { - if (current_revnum == _this.connection.getHeadRevision()) - _this.is_playing = false; - if (!_this.is_playing) - return; - setTimeout(function () { - _this.goToRevision(current_revnum + 1, keepPlaying); - }, RevisionSlider.PLAYBACK_DELAY); - }; - - this.is_playing = true; - this.goToRevision(revnum, keepPlaying); - }, - /** - * Update the UI elements to the current revision - */ - render: function () { - this.elements.revision_label.html(html10n.get("timeslider.version", { "version": this.revision_number })); - this.slider.setValue(this.revision_number); - window.location.hash = "#" + this.revision_number; - this.setTimestamp(this.timestamp); - if (this.is_playing) - this.elements.button_play.find("div").addClass("pause"); - else - this.elements.button_play.find("div").removeClass("pause"); - if (this.revision_number == this.connection.getHeadRevision()) - this.elements.button_right.addClass("disabled"); - else - this.elements.button_right.removeClass("disabled"); - if (this.revision_number === 0) - this.elements.button_left.addClass("disabled"); - else - this.elements.button_left.removeClass("disabled"); - }, - /** - * Go to a specific revision number. This will perform the actual - * transition to the revision and set the UI elements as required - * once the transition is done. The callback can be used to perform - * actions after the transition is complete and the UI has been - * updated. - * @param {number} revnum - The revision to transition to. - * @param {callback} atRevision_callback - The callback. - */ - goToRevision: function (revnum, atRevision_callback) { - if (revnum > this.connection.getHeadRevision()) - revnum = this.connection.latest_revision; - if (revnum < 0) - revnum = 0; - - var _this = this; - this.connection.goToRevision(revnum, function (revision, timestamp) { - console.log("[revisionslider > goToRevision > callback]", revision, timestamp); - //update UI elements: - _this.revision_number = revision.revnum; - _this.timestamp = timestamp; - _this.render.call(_this); - //TODO: set the enabled/disabled for button-left and button-right - if (atRevision_callback) - atRevision_callback(revnum); - }); - }, - /** - * Set the timestamp and revision date displays - * @param {number} timestamp - The timestamp of the current revision. - */ - setTimestamp: function (timestamp) { - var zeropad = function (str, length) { - str = str + ""; - while (str.length < length) - str = '0' + str; - return str; - }; - var months = [ - html10n.get("timeslider.month.january"), - html10n.get("timeslider.month.february"), - html10n.get("timeslider.month.march"), - html10n.get("timeslider.month.april"), - html10n.get("timeslider.month.may"), - html10n.get("timeslider.month.june"), - html10n.get("timeslider.month.july"), - html10n.get("timeslider.month.august"), - html10n.get("timeslider.month.september"), - html10n.get("timeslider.month.october"), - html10n.get("timeslider.month.november"), - html10n.get("timeslider.month.december") - ]; - var date = new Date(timestamp); - var timestamp_format = html10n.get("timeslider.dateformat", - { - "day": zeropad(date.getDate(), 2), - "month": zeropad(date.getMonth() + 1, 2), - "year": date.getFullYear(), - "hours": zeropad(date.getHours(), 2), - "minutes": zeropad(date.getMinutes(), 2), - "seconds": zeropad(date.getSeconds(), 2), - }); - this.elements.timestamp.html(timestamp_format); - - var revisionDate = html10n.get("timeslider.saved", { - "day": date.getDate(), - "month": months[date.getMonth()], - "year": date.getFullYear() - }); - - this.elements.revision_date.html(revisionDate); - }, - _mouseInit: function () { - var _this = this; - this.elements.button_left.on("click", function (event) { - if ($(this).hasClass("disabled")) - return; - _this.is_playing = false; - _this.goToRevision(_this.revision_number - 1); - }); - - this.elements.button_right.on("click", function (event) { - if ($(this).hasClass("disabled")) - return; - _this.is_playing = false; - _this.goToRevision(_this.revision_number + 1); - }); - - this.elements.button_play.on("click", function (event) { - _this.playpause(); - }); - } - - } -); function init(connection, fireWhenAllScriptsAreLoaded) { @@ -230,7 +24,6 @@ function init(connection, fireWhenAllScriptsAreLoaded) (function() { // wrap this code in its own namespace - tsui = new RevisionSlider(connection, $("#timeslider-top")); var clientVars = connection.clientVars; diff --git a/src/static/js/revisioncache.js b/src/static/js/revisioncache.js index 4858b846d..2164dbe7f 100644 --- a/src/static/js/revisioncache.js +++ b/src/static/js/revisioncache.js @@ -533,9 +533,59 @@ Thread("ChangesetLoader", } ); +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 '.' class, which is applied + // to blobs which were authored by that . + 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; + }, + } +); + var AttribPool = require("./AttributePool"); var domline = require("./domline").domline; -var linestylefilter = require("./linestylefilter").linestylefilter; $.Class("PadClient", {//static USE_COMPOSE: false, @@ -545,19 +595,20 @@ $.Class("PadClient", * Create a PadClient. * @constructor * @param {RevisionCache} revisionCache - A RevisionCache object to use. - * @param {number} revision - The current revision of the pad. - * @param {datetime} timestamp - The timestamp of the current revision. - * @param {string} atext - The attributed text. - * @param {string} attribs - The attributes string. - * @param {object} apool - The attribute pool. + * @param {dict} options - All the necessary options. TODO: document this. */ - init: function (revisionCache, revision, timestamp, atext, attribs, apool) { + init: function (revisionCache, options) { this.revisionCache = revisionCache; - this.revision = this.revisionCache.getRevision(revision); - this.timestamp = timestamp; - this.alines = libchangeset.splitAttributionLines(attribs, atext); - this.apool = (new AttribPool()).fromJsonable(apool); - this.lines = libchangeset.splitTextLines(atext); + 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; + + this.updateAuthors(options.author_info); //TODO: this is a kludge! we should receive the padcontent as an //injected dependency @@ -620,8 +671,30 @@ $.Class("PadClient", atRevision_callback.call(_this, _this.revision, _this.timestamp); } }); - }, + /** + * 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; + 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); + } + }, + /** + * 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. + */ _getDivForLine: function (text, atext) { var dominfo = domline.createDomLine(text != '\n', true); diff --git a/src/static/js/revisionslider.js b/src/static/js/revisionslider.js new file mode 100644 index 000000000..fd2521c08 --- /dev/null +++ b/src/static/js/revisionslider.js @@ -0,0 +1,253 @@ +var sliderui = require('./sliderui'); +require('./jquery.class'); + +$.Class("RevisionSlider", + {//statics + /** + * The number of milliseconds to wait between revisions when playing back. + */ + PLAYBACK_DELAY: 400, + }, + {//instance + /** + * Create a new RevisionSlider, given a connection to the server and a root + * element. + * @constructor + * @param {TimesliderClient} connection - The connection to the server. + * @param {jquery object} root_element - The element to build the slider on. + */ + init: function (connection, root_element) { + this.connection = connection; + this.revision_number = this.connection.getCurrentRevision().revnum; + this.timestamp = 0; + this.is_playing = false; + // if there was a revision specified in the 'location.hash', jump to it. + if (window.location.hash.length > 1) { + var rev = Number(window.location.hash.substr(1)); + if(!isNaN(rev)) + this.revision_number = rev; + } + + console.log("New RevisionSlider, current_revision = %d", this.revision_number); + // parse the various elements we need: + this.elements = {}; + this.loadElements(root_element); + var _this = this; + this.slider = new SliderUI(this.elements.slider_bar, + options = { + value: this.revision_number, + max: this.connection.getHeadRevision(), + change: function () { _this.onChange.apply(_this, arguments); }, + slide: function () { _this.onSlide.apply(_this, arguments); }, + }); + this.loadSavedRevisionHandles(); + this.slider.render(); + + this._mouseInit(); + + this.goToRevision(this.revision_number); + }, + onChange: function (value) { + console.log("in change handler:", value); + this.goToRevision(value); + }, + onSlide: function (value) { + console.log("in slide handler:", value); + this.goToRevision(value); + }, + /** + * Populate the elements dictionary with the various elements we might want + * to use. + * @param {jquery object} root_element - The root element of this slider. + */ + loadElements: function (root_element) { + this.elements.root = root_element; + this.elements.slider_bar = root_element.find("#ui-slider-bar"); + this.elements.slider = root_element.find("#timeslider-slider"); + this.elements.button_left = root_element.find("#leftstep"); + this.elements.button_right = root_element.find("#rightstep"); + this.elements.button_play = root_element.find("#playpause_button"); + this.elements.timestamp = root_element.find("#timer"); + this.elements.revision_label = root_element.find("#revision_label"); + this.elements.revision_date = root_element.find("#revision_date"); + this.elements.authors = root_element.find("#authorsList"); + }, + /** + * Create 'star' handles on the slider for each saved revision. + */ + loadSavedRevisionHandles: function () { + for (var r in this.connection.savedRevisions) { + var rev = this.connection.savedRevisions[r]; + this.slider.createHandle(rev.revNum, "star"); + } + }, + /** + * Toggle (and execute) the playback mode. + */ + playpause: function () { + if (this.is_playing) { + this.is_playing = false; + return; + } + + var revnum = this.revision_number; + if (revnum == this.connection.getHeadRevision()) + revnum = 0; + + var _this = this; + var keepPlaying = function (current_revnum) { + if (current_revnum == _this.connection.getHeadRevision()) + _this.is_playing = false; + if (!_this.is_playing) + return; + setTimeout(function () { + _this.goToRevision(current_revnum + 1, keepPlaying); + }, RevisionSlider.PLAYBACK_DELAY); + }; + + this.is_playing = true; + this.goToRevision(revnum, keepPlaying); + }, + /** + * Update the UI elements to the current revision + */ + render: function () { + this.elements.revision_label.html(html10n.get("timeslider.version", { "version": this.revision_number })); + this.slider.setValue(this.revision_number); + window.location.hash = "#" + this.revision_number; + this.setTimestamp(this.timestamp); + if (this.is_playing) + this.elements.button_play.find("div").addClass("pause"); + else + this.elements.button_play.find("div").removeClass("pause"); + if (this.revision_number == this.connection.getHeadRevision()) + this.elements.button_right.addClass("disabled"); + else + this.elements.button_right.removeClass("disabled"); + if (this.revision_number === 0) + this.elements.button_left.addClass("disabled"); + else + this.elements.button_left.removeClass("disabled"); + + this.renderAuthors(); + }, + + + /** + * Render the authors line. + */ + renderAuthors: function () { + //TODO: consider alphabetizing the authors? + var authors = this.connection.getAuthors(); + this.elements.authors.empty(); + if ($.isEmptyObject(authors)) { + this.elements.authors.append("No authors"); + return; + } + for (var authorid in authors) { + var author = authors[authorid]; + var span = $("") + .text(author.getName()) + .addClass('author') + .addClass(author.getCSSClass()); + this.elements.authors.append(span); + } + }, + /** + * Go to a specific revision number. This will perform the actual + * transition to the revision and set the UI elements as required + * once the transition is done. The callback can be used to perform + * actions after the transition is complete and the UI has been + * updated. + * @param {number} revnum - The revision to transition to. + * @param {callback} atRevision_callback - The callback. + */ + goToRevision: function (revnum, atRevision_callback) { + if (revnum > this.connection.getHeadRevision()) + revnum = this.connection.latest_revision; + if (revnum < 0) + revnum = 0; + + var _this = this; + this.connection.goToRevision(revnum, function (revision, timestamp) { + console.log("[revisionslider > goToRevision > callback]", revision, timestamp); + //update UI elements: + _this.revision_number = revision.revnum; + _this.timestamp = timestamp; + _this.render.call(_this); + //TODO: set the enabled/disabled for button-left and button-right + if (atRevision_callback) + atRevision_callback(revnum); + }); + }, + /** + * Set the timestamp and revision date displays + * @param {number} timestamp - The timestamp of the current revision. + */ + setTimestamp: function (timestamp) { + var zeropad = function (str, length) { + str = str + ""; + while (str.length < length) + str = '0' + str; + return str; + }; + var months = [ + html10n.get("timeslider.month.january"), + html10n.get("timeslider.month.february"), + html10n.get("timeslider.month.march"), + html10n.get("timeslider.month.april"), + html10n.get("timeslider.month.may"), + html10n.get("timeslider.month.june"), + html10n.get("timeslider.month.july"), + html10n.get("timeslider.month.august"), + html10n.get("timeslider.month.september"), + html10n.get("timeslider.month.october"), + html10n.get("timeslider.month.november"), + html10n.get("timeslider.month.december") + ]; + var date = new Date(timestamp); + var timestamp_format = html10n.get("timeslider.dateformat", + { + "day": zeropad(date.getDate(), 2), + "month": zeropad(date.getMonth() + 1, 2), + "year": date.getFullYear(), + "hours": zeropad(date.getHours(), 2), + "minutes": zeropad(date.getMinutes(), 2), + "seconds": zeropad(date.getSeconds(), 2), + }); + this.elements.timestamp.html(timestamp_format); + + var revisionDate = html10n.get("timeslider.saved", { + "day": date.getDate(), + "month": months[date.getMonth()], + "year": date.getFullYear() + }); + + this.elements.revision_date.html(revisionDate); + }, + /** + * Initialize mouse events and handlers + */ + _mouseInit: function () { + var _this = this; + this.elements.button_left.on("click", function (event) { + if ($(this).hasClass("disabled")) + return; + _this.is_playing = false; + _this.goToRevision(_this.revision_number - 1); + }); + + this.elements.button_right.on("click", function (event) { + if ($(this).hasClass("disabled")) + return; + _this.is_playing = false; + _this.goToRevision(_this.revision_number + 1); + }); + + this.elements.button_play.on("click", function (event) { + _this.playpause(); + }); + } + + } +); diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 6139d424a..cb23f8edc 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -173,6 +173,7 @@ SocketClient("AuthenticatedSocketClient", ); require('./revisioncache'); +require('./revisionslider'); AuthenticatedSocketClient("TimesliderClient", { //statics }, @@ -197,11 +198,20 @@ AuthenticatedSocketClient("TimesliderClient", this.revisionCache = new RevisionCache(this, collabClientVars.rev || 0); this.padClient = new PadClient(this.revisionCache, - collabClientVars.rev, - collabClientVars.time, - collabClientVars.initialAttributedText.text, - collabClientVars.initialAttributedText.attribs, - collabClientVars.apool); + { + revnum: collabClientVars.rev, + timestamp: collabClientVars.time, + atext: { + text: collabClientVars.initialAttributedText.text, + attributes: collabClientVars.initialAttributedText.attribs, + apool: collabClientVars.apool, + }, + author_info: collabClientVars.historicalAuthorData, + palette: this.clientVars.colorPalette, + }); + + //TODO: not wild about the timeslider-top selector being hard-coded here. + this.ui = new RevisionSlider(this, $("#timeslider-top")); }, //TODO: handle new revisions, authors etc. @@ -220,7 +230,13 @@ AuthenticatedSocketClient("TimesliderClient", goToRevision: function (revision_number, atRevision_callback) { this.padClient.goToRevision(revision_number, atRevision_callback); }, - + /** + * Get the authors for the current pad. + * @return {dict} - A dictionary of author-id to Author objects. + */ + getAuthors: function () { + return this.padClient.authors; + }, /** * Get the current revision. * @return {Revision} - the current revision. @@ -235,7 +251,6 @@ AuthenticatedSocketClient("TimesliderClient", getHeadRevision: function () { return this.head_revision; }, - } );