diff --git a/.gitignore b/.gitignore index 2fbb32200..4f3152245 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ bin/convertSettings.json src/static/js/jquery.js npm-debug.log *.DS_Store +.ep_initialized diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 866edeb02..4a570e219 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -24,6 +24,7 @@ var async = require("async"); var padManager = require("../db/PadManager"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); +var AttributeManager = require("ep_etherpad-lite/static/js/AttributeManager"); var authorManager = require("../db/AuthorManager"); var readOnlyManager = require("../db/ReadOnlyManager"); var settings = require('../utils/Settings'); @@ -31,6 +32,7 @@ var securityManager = require("../db/SecurityManager"); var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins.js"); var log4js = require('log4js'); var messageLogger = log4js.getLogger("message"); +var _ = require('underscore'); /** * A associative array that translates a session to a pad @@ -591,8 +593,12 @@ function _correctMarkersInPad(atext, apool) { var offset = 0; while (iter.hasNext()) { var op = iter.next(); - var listValue = Changeset.opAttributeValue(op, 'list', apool); - if (listValue) { + + var hasMarker = _.find(AttributeManager.lineAttributes, function(attribute){ + return Changeset.opAttributeValue(op, attribute, apool); + }) !== undefined; + + if (hasMarker) { for(var i=0;i 0 && text.charAt(offset-1) != '\n') { badMarkers.push(offset); diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js index 8a9751eb5..48d507224 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.js @@ -4,13 +4,14 @@ var formidable = require('formidable'); var apiHandler = require('../../handler/APIHandler'); //This is for making an api call, collecting all post information and passing it to the apiHandler -exports.apiCaller = function(req, res, fields) { +var apiCaller = function(req, res, fields) { res.header("Content-Type", "application/json; charset=utf-8"); apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields)); //wrap the send function so we can log the response - res._send = res.send; + //note: res._send seems to be already in use, so better use a "unique" name + res._____send = res.send; res.send = function (response) { response = JSON.stringify(response); apiLogger.info("RESPONSE, " + req.params.func + ", " + response); @@ -19,13 +20,14 @@ exports.apiCaller = function(req, res, fields) { if(req.query.jsonp) response = req.query.jsonp + "(" + response + ")"; - res._send(response); + res._____send(response); } //call the api handler apiHandler.handle(req.params.func, fields, req, res); } +exports.apiCaller = apiCaller; exports.expressCreateServer = function (hook_name, args, cb) { //This is a api GET call, collect all post informations and pass it to the apiHandler @@ -55,4 +57,4 @@ exports.expressCreateServer = function (hook_name, args, cb) { res.end("OK"); }); }); -} \ No newline at end of file +} diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index 14c93f5c7..5c1abac5d 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -56,6 +56,7 @@ , "rjquery.js" , "AttributePool.js" , "Changeset.js" + , "ChangesetUtils.js" , "security.js" , "skiplist.js" , "virtual_lines.js" @@ -66,6 +67,7 @@ , "changesettracker.js" , "linestylefilter.js" , "domline.js" + , "AttributeManager.js" , "ace2_inner.js" ] } diff --git a/src/static/css/pad.css b/src/static/css/pad.css index 19d148a31..21f365e24 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -42,9 +42,8 @@ a img border-bottom: 1px solid #ccc; overflow: hidden; padding-top: 3px; - position: absolute; - left: 0; - right: 0; + width: 100%; + white-space: nowrap; height: 32px; } @@ -177,7 +176,6 @@ a#backtoprosite { padding-left: 20px; left: 6px; background: url(static/img/protop.gif) no-repeat -5px -6px; } #accountnav { right: 30px; color: #fff; } -.propad a#topbaretherpad { background: url(static/img/protop.gif) no-repeat -397px -3px; } #specialkeyarea { top: 5px; left: 250px; color: yellow; font-weight: bold; font-size: 1.5em; position: absolute; } @@ -606,8 +604,6 @@ table#otheruserstable { display: none; } text-align: left; } -.nonprouser #sharebox-stripe { display: none; } - .sharebox-url { width: 440px; height: 18px; text-align: left; diff --git a/src/static/css/timeslider.css b/src/static/css/timeslider.css index 38e4cfb10..03e970481 100644 --- a/src/static/css/timeslider.css +++ b/src/static/css/timeslider.css @@ -1,4 +1,7 @@ -#editorcontainerbox {overflow:auto; top:40px;} +#editorcontainerbox { + overflow:auto; top:40px; + position: static; +} #padcontent {font-size:12px; padding:10px;} @@ -67,8 +70,9 @@ width:122px; } + .topbarcenter, #docbar {display:none;} -#padmain {top:30px;} +#padmain {top:0px !important;} #editbarright {float:right;} #returnbutton {color:#222; font-size:16px; line-height:29px; margin-top:0; padding-right:6px;} #importexport .popup {width:185px;} @@ -77,6 +81,53 @@ width:185px; } + +.timeslider-bar +{ + background: #f7f7f7; + background: linear-gradient(#f7f7f7, #f1f1f1 80%); + border-bottom: 1px solid #ccc; + overflow: hidden; + padding-top: 3px; + width: 100%; +} + +.timeslider-bar #editbar +{ + border-bottom: none; + float: right; + width: 170px; + width: initial; +} + +.timeslider-bar h1 +{ + margin: 5px; +} +.timeslider-bar p +{ + margin: 5px; +} +#timeslider-top { + width: 100%; + position: fixed; + z-index: 1; +} + +#authorsList .author { + padding-left: 0.4em; + padding-right: 0.4em; +} + +#authorsList .author-anonymous { + padding-left: 0.6em; + padding-right: 0.6em; +} + +#padeditor { + position: static; +} + /* lists */ .list-bullet2, .list-indent2, .list-number2 {margin-left:3em;} .list-bullet3, .list-indent3, .list-number3 {margin-left:4.5em;} diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js new file mode 100644 index 000000000..2d523f6aa --- /dev/null +++ b/src/static/js/AttributeManager.js @@ -0,0 +1,164 @@ +var Changeset = require('./Changeset'); +var ChangesetUtils = require('./ChangesetUtils'); +var _ = require('./underscore'); + +var lineMarkerAttribute = 'lmkr'; + +// If one of these attributes are set to the first character of a +// line it is considered as a line attribute marker i.e. attributes +// set on this marker are applied to the whole line. +// The list attribute is only maintained for compatibility reasons +var lineAttributes = [lineMarkerAttribute,'list']; + +/* + The Attribute manager builds changesets based on a document + representation for setting and removing range or line-based attributes. + + @param rep the document representation to be used + @param applyChangesetCallback this callback will be called + once a changeset has been built. + + + A document representation contains + - an array `alines` containing 1 attributes string for each line + - an Attribute pool `apool` + - a SkipList `lines` containing the text lines of the document. +*/ + +var AttributeManager = function(rep, applyChangesetCallback) +{ + this.rep = rep; + this.applyChangesetCallback = applyChangesetCallback; + this.author = ''; + + // If the first char in a line has one of the following attributes + // it will be considered as a line marker +}; + +AttributeManager.lineAttributes = lineAttributes; + +AttributeManager.prototype = _(AttributeManager.prototype).extend({ + + applyChangeset: function(changeset){ + if(!this.applyChangesetCallback) return changeset; + + var cs = changeset.toString(); + if (!Changeset.isIdentity(cs)) + { + this.applyChangesetCallback(cs); + } + + return changeset; + }, + + /* + Sets attributes on a range + @param start [row, col] tuple pointing to the start of the range + @param end [row, col] tuple pointing to the end of the range + @param attribute: an array of attributes + */ + setAttributesOnRange: function(start, end, attribs) + { + var builder = Changeset.builder(this.rep.lines.totalWidth()); + ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, start); + ChangesetUtils.buildKeepRange(this.rep, builder, start, end, attribs, this.rep.apool); + return this.applyChangeset(builder); + }, + + /* + Returns if the line already has a line marker + @param lineNum: the number of the line + */ + lineHasMarker: function(lineNum){ + var that = this; + + return _.find(lineAttributes, function(attribute){ + return that.getAttributeOnLine(lineNum, attribute) != ''; + }) !== undefined; + }, + + /* + Gets a specified attribute on a line + @param lineNum: the number of the line to set the attribute for + @param attributeKey: the name of the attribute to get, e.g. list + */ + getAttributeOnLine: function(lineNum, attributeName){ + // get `attributeName` attribute of first char of line + var aline = this.rep.alines[lineNum]; + if (aline) + { + var opIter = Changeset.opIterator(aline); + if (opIter.hasNext()) + { + return Changeset.opAttributeValue(opIter.next(), attributeName, this.rep.apool) || ''; + } + } + return ''; + }, + + /* + Sets a specified attribute on a line + @param lineNum: the number of the line to set the attribute for + @param attributeKey: the name of the attribute to set, e.g. list + @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level) + + */ + setAttributeOnLine: function(lineNum, attributeName, attributeValue){ + var loc = [0,0]; + var builder = Changeset.builder(this.rep.lines.totalWidth()); + var hasMarker = this.lineHasMarker(lineNum); + + ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0])); + + if(hasMarker){ + ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [ + [attributeName, attributeValue] + ], this.rep.apool); + }else{ + // add a line marker + builder.insert('*', [ + ['author', this.author], + ['insertorder', 'first'], + [lineMarkerAttribute, '1'], + [attributeName, attributeValue] + ], this.rep.apool); + } + + return this.applyChangeset(builder); + }, + + /* + Removes a specified attribute on a line + @param lineNum: the number of the affected line + @param attributeKey: the name of the attribute to remove, e.g. list + + */ + removeAttributeOnLine: function(lineNum, attributeName, attributeValue){ + + var loc = [0,0]; + var builder = Changeset.builder(this.rep.lines.totalWidth()); + var hasMarker = this.lineHasMarker(lineNum); + + if(hasMarker){ + ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0])); + ChangesetUtils.buildRemoveRange(this.rep, builder, loc, (loc = [lineNum, 1])); + } + + return this.applyChangeset(builder); + }, + + /* + Sets a specified attribute on a line + @param lineNum: the number of the line to set the attribute for + @param attributeKey: the name of the attribute to set, e.g. list + @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level) + */ + toggleAttributeOnLine: function(lineNum, attributeName, attributeValue) { + return this.getAttributeOnLine(attributeName) ? + this.removeAttributeOnLine(lineNum, attributeName) : + this.setAttributeOnLine(lineNum, attributeName, attributeValue); + + } +}); + +module.exports = AttributeManager; \ No newline at end of file diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.js index a9245daf8..f5990c07d 100644 --- a/src/static/js/AttributePool.js +++ b/src/static/js/AttributePool.js @@ -22,6 +22,12 @@ * limitations under the License. */ +/* + An AttributePool maintains a mapping from [key,value] Pairs called + Attributes to Numbers (unsigened integers) and vice versa. These numbers are + used to reference Attributes in Changesets. +*/ + var AttributePool = function () { this.numToAttrib = {}; // e.g. {0: ['foo','bar']} this.attribToNum = {}; // e.g. {'foo,bar': 0} diff --git a/src/static/js/ChangesetUtils.js b/src/static/js/ChangesetUtils.js new file mode 100644 index 000000000..e0b67881f --- /dev/null +++ b/src/static/js/ChangesetUtils.js @@ -0,0 +1,60 @@ +/** + * This module contains several helper Functions to build Changesets + * based on a SkipList + */ + +/** + * 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. + */ +exports.buildRemoveRange = function(rep, builder, start, end) +{ + var startLineOffset = rep.lines.offsetOfIndex(start[0]); + var endLineOffset = rep.lines.offsetOfIndex(end[0]); + + if (end[0] > start[0]) + { + builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]); + builder.remove(end[1]); + } + else + { + builder.remove(end[1] - start[1]); + } +} + +exports.buildKeepRange = function(rep, builder, start, end, attribs, pool) +{ + var startLineOffset = rep.lines.offsetOfIndex(start[0]); + var endLineOffset = rep.lines.offsetOfIndex(end[0]); + + if (end[0] > start[0]) + { + builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool); + builder.keep(end[1], 0, attribs, pool); + } + else + { + builder.keep(end[1] - start[1], 0, attribs, pool); + } +} + +exports.buildKeepToStartOfRange = function(rep, builder, start) +{ + var startLineOffset = rep.lines.offsetOfIndex(start[0]); + + builder.keep(startLineOffset, start[0]); + builder.keep(start[1]); +} + diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 4dfcc64ef..6ea2938b9 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -256,6 +256,10 @@ require.setGlobalKeyPath("require");\n\ $$INCLUDE_CSS("../static/css/iframe_editor.css"); $$INCLUDE_CSS("../static/css/pad.css"); $$INCLUDE_CSS("../static/custom/pad.css"); + + var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ return '../static/plugins/' + path }); + includedCSS = includedCSS.concat(additionalCSS); + pushStyleTagsFor(iframeHTML, includedCSS); var includedJS = []; @@ -294,6 +298,11 @@ require.setGlobalKeyPath("require");\n\ $$INCLUDE_CSS("../static/css/iframe_editor.css"); $$INCLUDE_CSS("../static/css/pad.css"); $$INCLUDE_CSS("../static/custom/pad.css"); + + + var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ return '../static/plugins/' + path }); + includedCSS = includedCSS.concat(additionalCSS); + pushStyleTagsFor(outerHTML, includedCSS); // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index f8393d0b8..d2d145035 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -35,6 +35,8 @@ var isNodeText = Ace2Common.isNodeText, binarySearchInfinite = Ace2Common.binarySearchInfinite, htmlPrettyEscape = Ace2Common.htmlPrettyEscape, noop = Ace2Common.noop; + var hooks = require('./pluginfw/hooks'); + function Ace2Inner(){ @@ -45,10 +47,12 @@ function Ace2Inner(){ var domline = require('./domline').domline; var AttribPool = require('./AttributePool'); var Changeset = require('./Changeset'); + var ChangesetUtils = require('./ChangesetUtils'); var linestylefilter = require('./linestylefilter').linestylefilter; var SkipList = require('./skiplist'); var undoModule = require('./undomodule').undoModule; var makeVirtualLineView = require('./virtual_lines').makeVirtualLineView; + var AttributeManager = require('./AttributeManager'); var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" // changed to false @@ -78,14 +82,11 @@ function Ace2Inner(){ var overlaysdiv = lineMetricsDiv.nextSibling; initLineNumbers(); - var outsideKeyDown = function(evt) - {}; - var outsideKeyPress = function(evt) - { - return true; - }; - var outsideNotifyDirty = function() - {}; + var outsideKeyDown = noop; + + var outsideKeyPress = function(){return true;}; + + var outsideNotifyDirty = noop; // selFocusAtStart -- determines whether the selection extends "backwards", so that the focus // point (controlled with the arrow keys) is at the beginning; not supported in IE, though @@ -100,6 +101,7 @@ function Ace2Inner(){ alines: [], apool: new AttribPool() }; + // lines, alltext, alines, and DOM are set up in setup() if (undoModule.enabled) { @@ -119,6 +121,7 @@ function Ace2Inner(){ iframePadRight = 0; var console = (DEBUG && window.console); + var documentAttributeManager; if (!window.console) { @@ -155,6 +158,7 @@ function Ace2Inner(){ var textFace = 'monospace'; var textSize = 12; + function textLineHeight() { @@ -929,7 +933,10 @@ function Ace2Inner(){ }, grayedout: setClassPresenceNamed(outerWin.document.body, "grayedout"), dmesg: function(){ dmesg = window.dmesg = value; }, - userauthor: function(value){ thisAuthor = String(value); }, + userauthor: function(value){ + thisAuthor = String(value); + documentAttributeManager.author = thisAuthor; + }, styled: setStyled, textface: setTextFace, textsize: setTextSize, @@ -1864,55 +1871,6 @@ function Ace2Inner(){ } } - - function setupMozillaCaretHack(lineNum) - { - // This is really ugly, but by god, it works! - // Fixes annoying Firefox caret artifact (observed in 2.0.0.12 - // and unfixed in Firefox 2 as of now) where mutating the DOM - // and then moving the caret to the beginning of a line causes - // an image of the caret to be XORed at the top of the iframe. - // The previous solution involved remembering to set the selection - // later, in response to the next event in the queue, which was hugely - // annoying. - // This solution: add a space character (0x20) to the beginning of the line. - // After setting the selection, remove the space. - var lineNode = rep.lines.atIndex(lineNum).lineNode; - - var fc = lineNode.firstChild; - while (isBlockElement(fc) && fc.firstChild) - { - fc = fc.firstChild; - } - var textNode; - if (isNodeText(fc)) - { - fc.nodeValue = " " + fc.nodeValue; - textNode = fc; - } - else - { - textNode = doc.createTextNode(" "); - fc.parentNode.insertBefore(textNode, fc); - } - markNodeClean(lineNode); - return { - unhack: function() - { - if (textNode.nodeValue == " ") - { - textNode.parentNode.removeChild(textNode); - } - else - { - textNode.nodeValue = textNode.nodeValue.substring(1); - } - markNodeClean(lineNode); - } - }; - } - - function getPointForLineAndChar(lineAndChar) { var line = lineAndChar[0]; @@ -2247,6 +2205,9 @@ function Ace2Inner(){ } + /* + Converts the position of a char (index in String) into a [row, col] tuple + */ function lineAndColumnFromChar(x) { var lineEntry = rep.lines.atOffset(x); @@ -2301,8 +2262,8 @@ function Ace2Inner(){ // CCCC\n // end[0]: -------\n var builder = Changeset.builder(rep.lines.totalWidth()); - buildKeepToStartOfRange(builder, start); - buildRemoveRange(builder, start, end); + ChangesetUtils.buildKeepToStartOfRange(rep, builder, start); + ChangesetUtils.buildRemoveRange(rep, builder, start, end); builder.insert(newText, [ ['author', thisAuthor] ], rep.apool); @@ -2313,69 +2274,17 @@ function Ace2Inner(){ function performDocumentApplyAttributesToCharRange(start, end, attribs) { - if (end >= rep.alltext.length) - { - end = rep.alltext.length - 1; - } - performDocumentApplyAttributesToRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs); + end = Math.min(end, rep.alltext.length - 1); + documentAttributeManager.setAttributesOnRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs); } editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange; - - function performDocumentApplyAttributesToRange(start, end, attribs) - { - var builder = Changeset.builder(rep.lines.totalWidth()); - buildKeepToStartOfRange(builder, start); - buildKeepRange(builder, start, end, attribs, rep.apool); - var cs = builder.toString(); - performDocumentApplyChangeset(cs); - } - editorInfo.ace_performDocumentApplyAttributesToRange = performDocumentApplyAttributesToRange; - - function buildKeepToStartOfRange(builder, start) - { - var startLineOffset = rep.lines.offsetOfIndex(start[0]); - - builder.keep(startLineOffset, start[0]); - builder.keep(start[1]); - } - - function buildRemoveRange(builder, start, end) - { - var startLineOffset = rep.lines.offsetOfIndex(start[0]); - var endLineOffset = rep.lines.offsetOfIndex(end[0]); - - if (end[0] > start[0]) - { - builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]); - builder.remove(end[1]); - } - else - { - builder.remove(end[1] - start[1]); - } - } - - function buildKeepRange(builder, start, end, attribs, pool) - { - var startLineOffset = rep.lines.offsetOfIndex(start[0]); - var endLineOffset = rep.lines.offsetOfIndex(end[0]); - - if (end[0] > start[0]) - { - builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool); - builder.keep(end[1], 0, attribs, pool); - } - else - { - builder.keep(end[1] - start[1], 0, attribs, pool); - } - } - + + function setAttributeOnSelection(attributeName, attributeValue) { if (!(rep.selStart && rep.selEnd)) return; - performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ + documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ [attributeName, attributeValue] ]); } @@ -2436,13 +2345,13 @@ function Ace2Inner(){ if (selectionAllHasIt) { - performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ + documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ [attributeName, ''] ]); } else { - performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ + documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ [attributeName, 'true'] ]); } @@ -2964,6 +2873,10 @@ function Ace2Inner(){ "ul": 1 }; + _.each(hooks.callAll('aceRegisterBlockElements'), function(element){ + _blockElems[element] = 1; + }); + function isBlockElement(n) { return !!_blockElems[(n.tagName || "").toLowerCase()]; @@ -3328,6 +3241,7 @@ function Ace2Inner(){ { return; } + var lineNum = rep.selStart[0]; var listType = getLineListType(lineNum); @@ -3399,11 +3313,9 @@ function Ace2Inner(){ } } - if (mods.length > 0) - { - setLineListTypes(mods); - } - + _.each(mods, function(mod){ + setLineListType(mod[0], mod[1]); + }); return true; } editorInfo.ace_doIndentOutdent = doIndentOutdent; @@ -3453,6 +3365,9 @@ function Ace2Inner(){ var thisLineListType = getLineListType(theLine); var prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1)); var prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker); + + var thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine); + if (thisLineListType) { // this line is a list @@ -3466,6 +3381,9 @@ function Ace2Inner(){ // delistify performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], ''); } + }else if (thisLineHasMarker && prevLineEntry){ + // If the line has any attributes assigned, remove them by removing the marker '*' + performDocumentReplaceRange([theLine -1 , prevLineEntry.text.length], [theLine, lineEntry.lineMarker], ''); } else if (theLine > 0) { @@ -3818,26 +3736,17 @@ function Ace2Inner(){ return; } - var mozillaCaretHack = (false && browser.mozilla && selStart && selEnd && selStart[0] == selEnd[0] && selStart[1] == rep.lines.atIndex(selStart[0]).lineMarker && selEnd[1] == rep.lines.atIndex(selEnd[0]).lineMarker && setupMozillaCaretHack(selStart[0])); - var selection = {}; var ss = [selStart[0], selStart[1]]; - if (mozillaCaretHack) ss[1] += 1; selection.startPoint = getPointForLineAndChar(ss); var se = [selEnd[0], selEnd[1]]; - if (mozillaCaretHack) se[1] += 1; selection.endPoint = getPointForLineAndChar(se); selection.focusAtStart = !! rep.selFocusAtStart; setSelection(selection); - - if (mozillaCaretHack) - { - mozillaCaretHack.unhack(); - } } function getRepHTML() @@ -4926,27 +4835,30 @@ function Ace2Inner(){ } } } - + + var listAttributeName = 'list'; + function getLineListType(lineNum) { - // get "list" attribute of first char of line - var aline = rep.alines[lineNum]; - if (aline) - { - var opIter = Changeset.opIterator(aline); - if (opIter.hasNext()) - { - return Changeset.opAttributeValue(opIter.next(), 'list', rep.apool) || ''; - } - } - return ''; + return documentAttributeManager.getAttributeOnLine(lineNum, listAttributeName) } function setLineListType(lineNum, listType) { - setLineListTypes([ - [lineNum, listType] - ]); + if(listType == ''){ + documentAttributeManager.removeAttributeOnLine(lineNum, listAttributeName); + }else{ + documentAttributeManager.setAttributeOnLine(lineNum, listAttributeName, listType); + } + + //if the list has been removed, it is necessary to renumber + //starting from the *next* line because the list may have been + //separated. If it returns null, it means that the list was not cut, try + //from the current one. + if(renumberList(lineNum+1)==null) + { + renumberList(lineNum); + } } function renumberList(lineNum){ @@ -4993,8 +4905,8 @@ function Ace2Inner(){ } else if(curLevel == level) { - buildKeepRange(builder, loc, (loc = [line, 0])); - buildKeepRange(builder, loc, (loc = [line, 1]), [ + ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0])); + ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [ ['start', position] ], rep.apool); @@ -5025,62 +4937,6 @@ function Ace2Inner(){ } - function setLineListTypes(lineNumTypePairsInOrder) - { - var loc = [0, 0]; - var builder = Changeset.builder(rep.lines.totalWidth()); - for (var i = 0; i < lineNumTypePairsInOrder.length; i++) - { - var pair = lineNumTypePairsInOrder[i]; - var lineNum = pair[0]; - var listType = pair[1]; - buildKeepRange(builder, loc, (loc = [lineNum, 0])); - if (getLineListType(lineNum)) - { - // already a line marker - if (listType) - { - // make different list type - buildKeepRange(builder, loc, (loc = [lineNum, 1]), [ - ['list', listType] - ], rep.apool); - } - else - { - // remove list marker - buildRemoveRange(builder, loc, (loc = [lineNum, 1])); - } - } - else - { - // currently no line marker - if (listType) - { - // add a line marker - builder.insert('*', [ - ['author', thisAuthor], - ['insertorder', 'first'], - ['list', listType] - ], rep.apool); - } - } - } - - var cs = builder.toString(); - if (!Changeset.isIdentity(cs)) - { - performDocumentApplyChangeset(cs); - } - - //if the list has been removed, it is necessary to renumber - //starting from the *next* line because the list may have been - //separated. If it returns null, it means that the list was not cut, try - //from the current one. - if(renumberList(lineNum+1)==null) - { - renumberList(lineNum); - } - } function doInsertList(type) { @@ -5118,7 +4974,10 @@ function Ace2Inner(){ var t = getLineListType(n); mods.push([n, allLinesAreList ? 'indent' + level : (t ? type + level : type + '1')]); } - setLineListTypes(mods); + + _.each(mods, function(mod){ + setLineListType(mod[0], mod[1]); + }); } function doInsertUnorderedList(){ @@ -5538,6 +5397,11 @@ function Ace2Inner(){ } } } + + + // Init documentAttributeManager + documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset); + editorInfo.ace_performDocumentApplyAttributesToRange = documentAttributeManager.setAttributesOnRange; $(document).ready(function(){ doc = document; // defined as a var in scope outside @@ -5577,7 +5441,13 @@ function Ace2Inner(){ bindTheEventHandlers(); }); - + + hooks.callAll('aceInitialized', { + editorInfo: editorInfo, + rep: rep, + documentAttributeManager: documentAttributeManager + }); + scheduler.setTimeout(function() { parent.readyFunc(); // defined in code that sets up the inner iframe diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js index f88eb5ddc..a2a157733 100644 --- a/src/static/js/broadcast_slider.js +++ b/src/static/js/broadcast_slider.js @@ -170,41 +170,67 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) $('#error').show(); } + var fixPadHeight = _.throttle(function(){ + var height = $('#timeslider-top').height(); + $('#editorcontainerbox').css({marginTop: height}); + }, 600); + function setAuthors(authors) { - $("#authorstable").empty(); + var authorsList = $("#authorsList"); + authorsList.empty(); var numAnonymous = 0; var numNamed = 0; + var colorsAnonymous = []; _.each(authors, function(author) { + var authorColor = clientVars.colorPalette[author.colorId] || author.colorId; if (author.name) { + if (numNamed !== 0) authorsList.append(', '); + + $('') + .text(author.name || "unnamed") + .css('background-color', authorColor) + .addClass('author') + .appendTo(authorsList); + numNamed++; - var tr = $(''); - var swatchtd = $(''); - var swatch = $('
'); - swatch.css('background-color', clientVars.colorPalette[author.colorId]); - swatchtd.append(swatch); - tr.append(swatchtd); - var nametd = $(''); - nametd.text(author.name || "unnamed"); - tr.append(nametd); - $("#authorstable").append(tr); } else { numAnonymous++; + if(authorColor) colorsAnonymous.push(authorColor); } }); if (numAnonymous > 0) { - var html = "" + (numNamed > 0 ? "...and " : "") + numAnonymous + " unnamed author" + (numAnonymous > 1 ? "s" : "") + ""; - $("#authorstable").append($(html)); + var anonymousAuthorString = numAnonymous + " unnamed author" + (numAnonymous > 1 ? "s" : "") + if (numNamed !== 0){ + authorsList.append(' + ' + anonymousAuthorString); + } else { + authorsList.append(anonymousAuthorString); + } + + if(colorsAnonymous.length > 0){ + authorsList.append(' ('); + _.each(colorsAnonymous, function(color, i){ + if( i > 0 ) authorsList.append(' '); + $(' ') + .css('background-color', color) + .addClass('author author-anonymous') + .appendTo(authorsList); + }); + authorsList.append(')'); + } + } if (authors.length == 0) { - $("#authorstable").append($("No Authors")) + authorsList.append("No Authors"); } + + fixPadHeight(); } BroadcastSlider = { @@ -465,7 +491,6 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) { if (clientVars.supportsSlider) { - $("#padmain, #rightbars").css('top', "130px"); $("#timeslider").show(); setSliderLength(clientVars.totalRevs); setSliderPosition(clientVars.revNum); diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index e04fbb432..219976d33 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -27,6 +27,7 @@ var _MAX_LIST_LEVEL = 8; var Changeset = require('./Changeset'); var hooks = require('./pluginfw/hooks'); +var _ = require('./underscore'); function sanitizeUnicode(s) { @@ -161,12 +162,6 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class var selection, startPoint, endPoint; var selStart = [-1, -1], selEnd = [-1, -1]; - var blockElems = { - "div": 1, - "p": 1, - "pre": 1 - }; - function _isEmpty(node, state) { // consider clean blank lines pasted in IE to be empty @@ -188,7 +183,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class { var ln = lines.length() - 1; var chr = lines.textOfLine(ln).length; - if (chr == 0 && state.listType && state.listType != 'none') + if (chr == 0 && !_.isEmpty(state.lineAttributes)) { chr += 1; // listMarker } @@ -240,25 +235,30 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class function _enterList(state, listType) { - var oldListType = state.listType; - state.listLevel = (state.listLevel || 0) + 1; + var oldListType = state.lineAttributes['list']; if (listType != 'none') { state.listNesting = (state.listNesting || 0) + 1; } - state.listType = listType; + + if(listType === 'none' || !listType ){ + delete state.lineAttributes['list']; + } + else{ + state.lineAttributes['list'] = listType; + } + _recalcAttribString(state); return oldListType; } function _exitList(state, oldListType) { - state.listLevel--; - if (state.listType != 'none') + if (state.lineAttributes['list']) { state.listNesting--; } - state.listType = oldListType; + if(oldListType) state.lineAttributes['list'] = oldListType; _recalcAttribString(state); } @@ -301,21 +301,29 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class state.attribString = Changeset.makeAttribsString('+', lst, apool); } - function _produceListMarker(state) + function _produceLineAttributesMarker(state) { - lines.appendText('*', Changeset.makeAttribsString('+', [ - ['list', state.listType], + // TODO: This has to go to AttributeManager. + var attributes = [ + ['lmkr', '1'], ['insertorder', 'first'] - ], apool)); + ].concat( + _.map(state.lineAttributes,function(value,key){ + console.log([key, value]) + return [key, value]; + }) + ); + + lines.appendText('*', Changeset.makeAttribsString('+', attributes , apool)); } cc.startNewLine = function(state) { if (state) { var atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0; - if (atBeginningOfLine && state.listType && state.listType != 'none') + if (atBeginningOfLine && !_.isEmpty(state.lineAttributes)) { - _produceListMarker(state); + _produceLineAttributesMarker(state); } } lines.startNew(); @@ -345,7 +353,14 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class localAttribs: null, attribs: { /*name -> nesting counter*/ }, - attribString: '' + attribString: '', + // lineAttributes maintain a map from attributes to attribute values set on a line + lineAttributes: { + /* + example: + 'list': 'bullet1', + */ + } }; } var localAttribs = state.localAttribs; @@ -407,9 +422,9 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class // newlines in the source mustn't become spaces at beginning of line box txt2 = txt2.replace(/^\n*/, ''); } - if (atBeginningOfLine && state.listType && state.listType != 'none') + if (atBeginningOfLine && !_.isEmpty(state.lineAttributes)) { - _produceListMarker(state); + _produceLineAttributesMarker(state); } lines.appendText(textify(txt2), state.attribString); x += consumed; diff --git a/src/static/js/domline.js b/src/static/js/domline.js index d4f52a3ce..5d8bb7195 100644 --- a/src/static/js/domline.js +++ b/src/static/js/domline.js @@ -29,7 +29,7 @@ var Security = require('./security'); var hooks = require('./pluginfw/hooks'); var _ = require('./underscore'); - +var lineAttributeMarker = require('./linestylefilter').lineAttributeMarker; var Ace2Common = require('./ace2_common'); var noop = Ace2Common.noop; @@ -82,7 +82,8 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) } var html = []; - var preHtml, postHtml; + var preHtml = '', + postHtml = ''; var curHTML = null; function processSpaces(s) @@ -95,7 +96,9 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) var lineClass = 'ace-line'; result.appendSpan = function(txt, cls) { - if (cls.indexOf('list') >= 0) + var processedMarker = false; + // Handle lineAttributeMarker, if present + if (cls.indexOf(lineAttributeMarker) >= 0) { var listType = /(?:^| )list:(\S+)/.exec(cls); var start = /(?:^| )start:(\S+)/.exec(cls); @@ -115,10 +118,26 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) preHtml = '
  1. '; postHtml = '
'; } - } + } + processedMarker = true; + } + + _.map(hooks.callAll("aceDomLineProcessLineAttributes", { + domline: domline, + cls: cls + }), function(modifier) + { + preHtml += modifier.preHtml; + postHtml += modifier.postHtml; + processedMarker |= modifier.processedMarker; + }); + + if( processedMarker ){ result.lineMarker += txt.length; return; // don't append any text - } + } + + } var href = null; var simpleTags = null; @@ -203,7 +222,7 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) { newHTML = (preHtml || '') + newHTML + (postHtml || ''); } - html = preHtml = postHtml = null; // free memory + html = preHtml = postHtml = ''; // free memory if (newHTML !== curHTML) { curHTML = newHTML; diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index f328bd56a..1cbfac29c 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -32,6 +32,8 @@ var Changeset = require('./Changeset'); var hooks = require('./pluginfw/hooks'); var linestylefilter = {}; var _ = require('./underscore'); +var AttributeManager = require('./AttributeManager'); + linestylefilter.ATTRIB_CLASSES = { 'bold': 'tag:b', @@ -40,6 +42,9 @@ linestylefilter.ATTRIB_CLASSES = { 'strikethrough': 'tag:s' }; +var lineAttributeMarker = 'lineAttribMarker'; +exports.lineAttributeMarker = lineAttributeMarker; + linestylefilter.getAuthorClassName = function(author) { return "author-" + author.replace(/[^a-y0-9]/g, function(c) @@ -68,14 +73,19 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun function attribsToClasses(attribs) { var classes = ''; + var isLineAttribMarker = false; + Changeset.eachAttribNumber(attribs, function(n) { - var key = apool.getAttribKey(n); + var key = apool.getAttribKey(n); if (key) { var value = apool.getAttribValue(n); if (value) { + if (!isLineAttribMarker && _.indexOf(AttributeManager.lineAttributes, key) >= 0){ + isLineAttribMarker = true; + } if (key == 'author') { classes += ' ' + linestylefilter.getAuthorClassName(value); @@ -99,10 +109,12 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun key: key, value: value }, " ", " ", ""); - } + } } } }); + + if(isLineAttribMarker) classes += ' ' + lineAttributeMarker; return classes.substring(1); } diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 426eb089e..3a1c1633e 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -534,7 +534,7 @@ var pad = { if(padcookie.getPref("showAuthorshipColors") == false){ pad.changeViewOption('showAuthorColors', false); } - hooks.aCallAll("postAceInit"); + hooks.aCallAll("postAceInit", {ace: padeditor.ace}); } }, dispose: function() diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 7e8cf96e8..95d195058 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -200,10 +200,11 @@ var padeditbar = (function() else { var nth_child = indexOf(modules, moduleName) + 1; - if (nth_child > 0 && nth_child <= 3) { + if (nth_child > 0 && nth_child <= (modules.length-1)) { $("#editbar ul#menu_right li:not(:nth-child(" + nth_child + "))").removeClass("selected"); $("#editbar ul#menu_right li:nth-child(" + nth_child + ")").toggleClass("selected"); } + if(modules[modules.length-1] === moduleName) $("#editbar ul#menu_right li").removeClass("selected"); //hide all modules that are not selected and show the selected one for(var i=0;i= n while (newNode.levels == 0 || Math.random() < 0.01) { var lvl = newNode.levels; diff --git a/src/templates/index.html b/src/templates/index.html index 58f688017..ecfa0d9e0 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -130,6 +130,7 @@ <% } %>