diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index 0342408c..53b233e0 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -400,7 +400,19 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ this.removeAttributeOnLine(lineNum, attributeName) : this.setAttributeOnLine(lineNum, attributeName, attributeValue); - } + }, + + hasAttributeOnSelectionOrCaretPosition: function(attributeName) { + var hasSelection = ((this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])); + var hasAttrib; + if (hasSelection) { + hasAttrib = this.getAttributeOnSelection(attributeName); + }else { + var attributesOnCaretPosition = this.getAttributesOnCaret(); + hasAttrib = _.contains(_.flatten(attributesOnCaretPosition), attributeName); + } + return hasAttrib; + }, }); module.exports = AttributeManager; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 83f947ba..df9c9642 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -75,6 +75,9 @@ function Ace2Inner(){ var EDIT_BODY_PADDING_TOP = 8; var EDIT_BODY_PADDING_LEFT = 8; + var FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough']; + var SELECT_BUTTON_CLASS = 'selected'; + var caughtErrors = []; var thisAuthor = ''; @@ -2472,17 +2475,11 @@ function Ace2Inner(){ } } - if (selectionAllHasIt) - { - documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ - [attributeName, ''] - ]); - } - else - { - documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ - [attributeName, 'true'] - ]); + + var attributeValue = selectionAllHasIt ? '' : 'true'; + documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [[attributeName, attributeValue]]); + if (attribIsFormattingStyle(attributeName)) { + updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ... } } editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection; @@ -2911,6 +2908,9 @@ function Ace2Inner(){ rep.selFocusAtStart = newSelFocusAtStart; currentCallStack.repChanged = true; + // select the formatting buttons when there is the style applied on selection + selectFormattingButtonIfLineHasStyleApplied(rep); + hooks.callAll('aceSelectionChanged', { rep: rep, callstack: currentCallStack, @@ -2939,6 +2939,22 @@ function Ace2Inner(){ return (eventType === 'setup') || (eventType === 'setBaseText') || (eventType === 'importText'); } + function updateStyleButtonState(attribName, hasStyleOnRepSelection) { + var $formattingButton = parent.parent.$('[data-key="' + attribName + '"]').find('a'); + $formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection); + } + + function attribIsFormattingStyle(attributeName) { + return _.contains(FORMATTING_STYLES, attributeName); + } + + function selectFormattingButtonIfLineHasStyleApplied (rep) { + _.each(FORMATTING_STYLES, function (style) { + var hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style); + updateStyleButtonState(style, hasStyleOnRepSelection); + }) + } + function doCreateDomLine(nonEmpty) { if (browser.msie && (!nonEmpty)) diff --git a/tests/frontend/specs/select_formatting_buttons.js b/tests/frontend/specs/select_formatting_buttons.js new file mode 100644 index 00000000..5fb97600 --- /dev/null +++ b/tests/frontend/specs/select_formatting_buttons.js @@ -0,0 +1,166 @@ +describe("select formatting buttons when selection has style applied", function(){ + var STYLES = ['italic', 'bold', 'underline', 'strikethrough']; + var SHORTCUT_KEYS = ['I', 'B', 'U', '5']; // italic, bold, underline, strikethrough + var FIRST_LINE = 0; + + before(function(cb){ + helper.newPad(cb); + this.timeout(60000); + }); + + var applyStyleOnLine = function(style, line) { + var chrome$ = helper.padChrome$; + selectLine(line); + var $formattingButton = chrome$('.buttonicon-' + style); + $formattingButton.click(); + } + + var isButtonSelected = function(style) { + var chrome$ = helper.padChrome$; + var $formattingButton = chrome$('.buttonicon-' + style); + return $formattingButton.parent().hasClass('selected'); + } + + var selectLine = function(lineNumber, offsetStart, offsetEnd) { + var inner$ = helper.padInner$; + var $line = inner$("div").eq(lineNumber); + helper.selectLines($line, $line, offsetStart, offsetEnd); + } + + var placeCaretOnLine = function(lineNumber) { + var inner$ = helper.padInner$; + var $line = inner$("div").eq(lineNumber); + $line.sendkeys('{leftarrow}'); + } + + var undo = function() { + var $undoButton = helper.padChrome$(".buttonicon-undo"); + $undoButton.click(); + } + + var testIfFormattingButtonIsDeselected = function(style) { + it('deselects the ' + style + ' button', function(done) { + helper.waitFor(function(){ + return isButtonSelected(style) === false; + }).done(done) + }); + } + + var testIfFormattingButtonIsSelected = function(style) { + it('selects the ' + style + ' button', function(done) { + helper.waitFor(function(){ + return isButtonSelected(style); + }).done(done) + }); + } + + var applyStyleOnLineAndSelectIt = function(line, style, cb) { + applyStyleOnLineOnFullLineAndRemoveSelection(line, style, selectLine, cb); + } + + var applyStyleOnLineAndPlaceCaretOnit = function(line, style, cb) { + applyStyleOnLineOnFullLineAndRemoveSelection(line, style, placeCaretOnLine, cb); + } + + var applyStyleOnLineOnFullLineAndRemoveSelection = function(line, style, selectTarget, cb) { + applyStyleOnLine(style, line); + + // we have to give some time to Etherpad detects the selection changed + setTimeout(function() { + // remove selection from previous line + selectLine(line + 1); + setTimeout(function() { + // select the text or place the caret on a position that + // has the formatting text applied previously + selectTarget(line); + cb(); + }, 1000); + }, 1000); + } + + var pressFormattingShortcutOnSelection = function(key) { + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + //get the first text element out of the inner iframe + var $firstTextElement = inner$("div").first(); + + //select this text element + $firstTextElement.sendkeys('{selectall}'); + + if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE + var evtType = "keypress"; + }else{ + var evtType = "keydown"; + } + + var e = inner$.Event(evtType); + e.ctrlKey = true; // Control key + e.which = key.charCodeAt(0); // I, U, B, 5 + inner$("#innerdocbody").trigger(e); + } + + STYLES.forEach(function(style){ + context('when selection is in a text with ' + style + ' applied', function(){ + before(function (done) { + this.timeout(4000); + applyStyleOnLineAndSelectIt(FIRST_LINE, style, done); + }); + + after(function () { + undo(); + }); + + testIfFormattingButtonIsSelected(style); + }); + + context('when caret is in a position with ' + style + ' applied', function(){ + before(function (done) { + this.timeout(4000); + applyStyleOnLineAndPlaceCaretOnit(FIRST_LINE, style, done); + }); + + after(function () { + undo(); + }); + + testIfFormattingButtonIsSelected(style) + }); + }); + + context('when user applies a style and the selection does not change', function() { + var style = STYLES[0]; // italic + before(function () { + applyStyleOnLine(style, FIRST_LINE); + }); + + // clean the style applied + after(function () { + applyStyleOnLine(style, FIRST_LINE); + }); + + it('selects the style button', function (done) { + expect(isButtonSelected(style)).to.be(true); + done(); + }); + }); + + SHORTCUT_KEYS.forEach(function(key, index){ + var styleOfTheShortcut = STYLES[index]; // italic, bold, ... + context('when user presses CMD + ' + key, function() { + before(function () { + pressFormattingShortcutOnSelection(key); + }); + + testIfFormattingButtonIsSelected(styleOfTheShortcut); + + context('and user presses CMD + ' + key + ' again', function() { + before(function () { + pressFormattingShortcutOnSelection(key); + }); + + testIfFormattingButtonIsDeselected(styleOfTheShortcut); + }); + }); + }); +});