diff --git a/public/javascripts/publisher.js b/public/javascripts/publisher.js index 209a755f5..3f914e62c 100644 --- a/public/javascripts/publisher.js +++ b/public/javascripts/publisher.js @@ -2,7 +2,18 @@ * licensed under the Affero General Public License version 3 or later. See * the COPYRIGHT file. */ - + var KEY = { + UP: 38, + DOWN: 40, + DEL: 46, + TAB: 9, + RETURN: 13, + ESC: 27, + COMMA: 188, + PAGEUP: 33, + PAGEDOWN: 34, + BACKSPACE: 8 + }; //TODO: make this a widget var Publisher = { close: function(){ @@ -62,61 +73,145 @@ var Publisher = { var visibleLoc = Publisher.autocompletion.addMentionToInput(visibleInput, visibleCursorIndex, formatted); $.Autocompleter.Selection(visibleInput[0], visibleLoc[1], visibleLoc[1]); - var hiddenCursorIndex = visibleCursorIndex + Publisher.autocompletion.mentionList.offsetFrom(visibleCursorIndex); - var hiddenLoc = Publisher.autocompletion.addMentionToInput(Publisher.hiddenInput(), hiddenCursorIndex, Publisher.autocompletion.hiddenMentionFromPerson(data)); + var mentionString = Publisher.autocompletion.hiddenMentionFromPerson(data); var mention = { visibleStart: visibleLoc[0], visibleEnd : visibleLoc[1], - hiddenStart : hiddenLoc[0], - hiddenEnd : hiddenLoc[1] + mentionString : mentionString }; + Publisher.autocompletion.mentionList.push(mention); + Publisher.oldInputContent = visibleInput.val(); + Publisher.hiddenInput().val(Publisher.autocompletion.mentionList.generateHiddenInput(visibleInput.val())); }, mentionList : { mentions : [], + sortedMentions : function(){ + return this.mentions.sort(function(m1, m2){ + if(m1.visibleStart > m2.visibleStart){ + return -1; + } else if(m1.visibleStart < m2.visibleStart){ + return 1; + } else { + return 0; + } + }); + }, push : function(mention){ - mention.offset = mention.hiddenEnd - mention.visibleEnd; this.mentions.push(mention); }, - keypressAt : function(visibleCursorIndex){ - var mentionIndex = this.mentionAt(visibleCursorIndex); - var mention = this.mentions[mentionIndex]; - if(!mention){return;} - var visibleMentionString = Publisher.input().val().slice(mention.visibleStart, mention.visibleEnd); - var hiddenContent = Publisher.hiddenInput().val(); - hiddenContent = hiddenContent.slice(0,mention.hiddenStart) + - visibleMentionString + - hiddenContent.slice(mention.hiddenEnd); - Publisher.hiddenInput().val(hiddenContent); + generateHiddenInput : function(visibleString){ + var resultString = visibleString; + for(i in this.sortedMentions()){ + var mention = this.mentions[i]; + var start = resultString.slice(0, mention.visibleStart); + var insertion = mention.mentionString; + var end = resultString.slice(mention.visibleEnd); - this.mentions.splice(mentionIndex, 1); + resultString = start + insertion + end; + } + return resultString; + }, + + insertionAt : function(insertionEndIndex, insertionStartIndex, keyCode){ + this.incrementMentionLocations(insertionStartIndex, insertionEndIndex - insertionStartIndex); + var mentionIndex = this.mentionAt(insertionEndIndex); + + var mention = this.mentions[mentionIndex]; + if(mention){ + this.mentions.splice(mentionIndex, 1); + } + + }, + deletionAt : function(visibleCursorIndex, keyCode){ + + var effectiveCursorIndex; + if(keyCode == KEY.DEL){ + effectiveCursorIndex = visibleCursorIndex; + }else{ + effectiveCursorIndex = visibleCursorIndex - 1; + } + this.decrementMentionLocations(effectiveCursorIndex, keyCode); + + var mentionIndex = this.mentionAt(effectiveCursorIndex); + + var mention = this.mentions[mentionIndex]; + if(mention){ + this.mentions.splice(mentionIndex, 1); + } + + }, + incrementMentionLocations : function(effectiveCursorIndex, offset){ + var changedMentions = this.mentionsAfter(effectiveCursorIndex); + for(i in changedMentions){ + var mention = changedMentions[i]; + mention.visibleStart += offset; + mention.visibleEnd += offset; + } + }, + decrementMentionLocations : function(effectiveCursorIndex){ + var visibleOffset = -1; + var changedMentions = this.mentionsAfter(effectiveCursorIndex); + for(i in changedMentions){ + var mention = changedMentions[i]; + mention.visibleStart += visibleOffset; + mention.visibleEnd += visibleOffset; + } }, mentionAt : function(visibleCursorIndex){ for(i in this.mentions){ var mention = this.mentions[i]; - if(visibleCursorIndex >= mention.visibleStart && visibleCursorIndex < mention.visibleEnd){ + if(visibleCursorIndex > mention.visibleStart && visibleCursorIndex < mention.visibleEnd){ return i; } - return false; } + return false; }, - offsetFrom: function(visibleCursorIndex){ - var mention = {visibleStart : -1, fake: true}; - var currentMention; + mentionsAfter : function(visibleCursorIndex){ + var resultMentions = []; for(i in this.mentions){ - currentMention = this.mentions[i]; - if(visibleCursorIndex >= currentMention.visibleStart && - currentMention.visibleStart > mention.visibleStart){ - mention = currentMention; + var mention = this.mentions[i]; + if(visibleCursorIndex <= mention.visibleStart){ + resultMentions.push(mention); } } - if(mention && !mention.fake){ - return mention.offset; - }else{ - return 0; - } + return resultMentions; + }, + }, + repopulateHiddenInput: function(){ + var newHiddenVal = Publisher.autocompletion.mentionList.generateHiddenInput(Publisher.input().val()); + if(newHiddenVal != Publisher.hiddenInput().val()){ + Publisher.hiddenInput().val(newHiddenVal); } }, + keyUpHandler : function(event){ + var input = Publisher.input(); + var cursorIndexAtKeydown = Publisher.cursorIndexAtKeydown; + Publisher.cursorIndexAtKeydown = -1; + if(input.val() == Publisher.oldInputContent || event.keyCode == KEY.RETURN || event.keyCode == KEY.DEL || event.keyCode == KEY.BACKSPACE){ + Publisher.autocompletion.repopulateHiddenInput(); + return; + }else { + Publisher.oldInputContent = input.val(); + var visibleCursorIndex = input[0].selectionStart; + Publisher.autocompletion.mentionList.insertionAt(visibleCursorIndex, cursorIndexAtKeydown, event.keyCode); + Publisher.autocompletion.repopulateHiddenInput(); + } + }, + + keyDownHandler : function(event){ + var input = Publisher.input(); + var visibleCursorIndex = input[0].selectionStart; + if(Publisher.cursorIndexAtKeydown == -1){ + Publisher.cursorIndexAtKeydown = visibleCursorIndex; + } + + if((event.keyCode == KEY.DEL && visibleCursorIndex < input.val().length) || (event.keyCode == KEY.BACKSPACE && visibleCursorIndex > 0)){ + Publisher.autocompletion.mentionList.deletionAt(visibleCursorIndex, event.keyCode); + } + Publisher.autocompletion.repopulateHiddenInput(); + }, + addMentionToInput: function(input, cursorIndex, formatted){ var inputContent = input.val(); @@ -126,13 +221,15 @@ var Publisher = { var stringEnd = inputContent.slice(stringLoc[1]); input.val(stringStart + formatted + stringEnd); + var offset = formatted.length - stringLoc[1] - stringLoc[0] + Publisher.autocompletion.mentionList.incrementMentionLocations(stringStart.length, offset); return [stringStart.length, stringStart.length + formatted.length] }, findStringToReplace: function(value, cursorIndex){ var atLocation = value.lastIndexOf('@', cursorIndex); if(atLocation == -1){return [0,0];} - var nextAt = cursorIndex//value.indexOf(' @', cursorIndex+1); + var nextAt = cursorIndex if(nextAt == -1){nextAt = value.length;} return [atLocation, nextAt]; @@ -164,6 +261,7 @@ var Publisher = { Publisher.input().autocomplete(Publisher.autocompletion.contactsJSON(), Publisher.autocompletion.options()); Publisher.input().result(Publisher.autocompletion.selectItemCallback); + Publisher.oldInputContent = Publisher.input().val(); } }, initialize: function() { @@ -183,6 +281,8 @@ var Publisher = { Publisher.autocompletion.initialize(); Publisher.hiddenInput().val(Publisher.input().val()); + Publisher.input().keydown(Publisher.autocompletion.keyDownHandler); + Publisher.input().keyup(Publisher.autocompletion.keyUpHandler); Publisher.form().find("textarea").bind("focus", function(evt) { Publisher.open(); $(this).css('min-height', '42px'); diff --git a/spec/javascripts/publisher-spec.js b/spec/javascripts/publisher-spec.js index 4ba86e7e9..8b44ae771 100644 --- a/spec/javascripts/publisher-spec.js +++ b/spec/javascripts/publisher-spec.js @@ -62,19 +62,17 @@ describe("Publisher", function() { }); }); describe("autocompletion", function(){ - describe("onKeypress", function(){ - });, describe("searchTermFromValue", function(){ var func; beforeEach(function(){func = Publisher.autocompletion.searchTermFromValue;}); it("returns nothing if the cursor is before the @", function(){ expect(func('not @dan grip', 2)).toBe(''); }); - it("returns everything after an @ if the cursor is a word after that @", function(){ + it("returns everything up to the cursor if the cursor is a word after that @", function(){ expect(func('not @dan grip', 13)).toBe('dan grip'); }); - it("returns everything after an @ if the cursor is after that @", function(){ - expect(func('not @dan grip', 7)).toBe('dan grip'); + it("returns up to the cursor if the cursor is after that @", function(){ + expect(func('not @dan grip', 7)).toBe('da'); }); it("returns everything after an @ at the start of the line", function(){ @@ -89,11 +87,11 @@ describe("Publisher", function() { it("returns nothing if there are letters preceding the @", function(){ expect(func('ioj@asdo', 8)).toBe(''); }); - it("returns everything between @s if there are 2 @s and the cursor is between them", function(){ - expect(func('@asdpo aoisdj @asodk', 8)).toBe('asdpo aoisdj'); + it("returns everything up to the cursor if there are 2 @s and the cursor is between them", function(){ + expect(func('@asdpo aoisdj @asodk', 8)).toBe('asdpo'); }); - it("returns everything after the 2nd @ if there are 2 @s and the cursor after them", function(){ - expect(func('@asod asdo @asd asok', 15)).toBe('asd asok'); + it("returns everything from the 2nd @ up to the cursor if there are 2 @s and the cursor after them", function(){ + expect(func('@asod asdo @asd asok', 15)).toBe('asd'); }); }); @@ -105,18 +103,15 @@ describe("Publisher", function() { var visibleInput, visibleVal, hiddenInput, hiddenVal, list, - func, mention; beforeEach(function(){ spec.loadFixture('aspects_index'); list = Publisher.autocompletion.mentionList; - func = list.keypressAt; visibleInput = Publisher.input(); hiddenInput = Publisher.hiddenInput(); mention = { visibleStart : 0, visibleEnd : 5, - hiddenStart : 0, - hiddenEnd : 21 + mentionString : "@{Danny; dan@pod.org}" }; list.mentions = []; list.push(mention); @@ -125,6 +120,11 @@ describe("Publisher", function() { hiddenVal = "@{Danny; dan@pod.org} loves testing javascript"; hiddenInput.val(hiddenVal); }); + describe("generateHiddenInput", function(){ + it("replaces mentions in a string", function(){ + expect(list.generateHiddenInput(visibleVal)).toBe(hiddenVal); + }); + }); describe("push", function(){ it("adds mention to mentions array", function(){ expect(list.mentions.length).toBe(1); @@ -142,34 +142,39 @@ describe("Publisher", function() { describe("keypressAt", function(){ it("does nothing if there is no visible mention at that index", function(){ list.keypressAt(8); - expect(visibleInput.val()).toBe(visibleVal) - expect(hiddenInput.val()).toBe(hiddenVal) + expect(visibleInput.val()).toBe(visibleVal); + expect(hiddenInput.val()).toBe(hiddenVal); }); it("deletes the mention from the hidden field if there is a mention", function(){ list.keypressAt(3); - expect(visibleInput.val()).toBe(visibleVal) - expect(hiddenInput.val()).toBe(visibleVal) + expect(visibleInput.val()).toBe(visibleVal); + expect(list.generateHiddenInput(visibleInput.val())).toBe(visibleVal); }); it("deletes the mention from the list", function(){ list.keypressAt(3); expect(list.mentionAt(3)).toBeFalsy(); }); - it("updates the offsets of the remaining mentions in the list"); + it("calls updateMentionLocations", function(){ + mentionTwo = { visibleStart : 8, + visibleEnd : 15, + mentionString : "@{SomeoneElse; other@pod.org}" + }; + list.push(mentionTwo); + spyOn(list, 'updateMentionLocations'); + list.keypressAt(3, 60); + expect(list.updateMentionLocations).toHaveBeenCalled(); + }); }); - describe("offsetFrom", function(){ - var func; - beforeEach(function(){ - func = list.offsetFrom; - }); - it("returns the offset of the mention at that location", function(){ - expect(list.offsetFrom(3)).toBe(mention.offset); - }); - it("returns the offset of the previous mention if there is no mention there", function(){ - expect(list.offsetFrom(10)).toBe(mention.offset); - }); - it("returns 0 if there are no mentions", function(){ - list.mentions = []; - expect(list.offsetFrom(8)).toBe(0); + describe("updateMentionLocations", function(){ + it("updates the offsets of the remaining mentions in the list", function(){ + mentionTwo = { visibleStart : 8, + visibleEnd : 15, + mentionString : "@{SomeoneElse; other@pod.org}" + }; + list.push(mentionTwo); + list.updateMentionLocations(7, 60); + expect(mentionTwo.visibleStart).toBe(9); + expect(mentionTwo.visibleEnd).toBe(16); }); }); }); @@ -184,37 +189,38 @@ describe("Publisher", function() { spec.loadFixture('aspects_index'); func = Publisher.autocompletion.addMentionToInput; input = Publisher.input(); + Publisher.autocompletion.mentionList = []; replaceWith = "Replace with this."; }); - it("replaces everything after an @ if the cursor is a word after that @", function(){ + it("replaces everything up to the cursor if the cursor is a word after that @", function(){ input.val('not @dan grip'); var cursorIndex = 13; func(input, cursorIndex, replaceWith); expect(input.val()).toBe('not ' + replaceWith); }); - it("replaces everything after an @ if the cursor is after that @", function(){ + it("replaces everything between @ and the cursor if the cursor is after that @", function(){ input.val('not @dan grip'); var cursorIndex = 7; func(input, cursorIndex, replaceWith); - expect(input.val()).toBe('not ' + replaceWith); + expect(input.val()).toBe('not ' + replaceWith + 'n grip'); }); - it("replaces everything after an @ at the start of the line", function(){ + it("replaces everything up to the cursor from @ at the start of the line", function(){ input.val('@dan grip'); var cursorIndex = 9; func(input, cursorIndex, replaceWith); expect(input.val()).toBe(replaceWith); }); - it("replaces everything between @s if there are 2 @s and the cursor is between them", function(){ + it("replaces everything between the first @ and the cursor if there are 2 @s and the cursor is between them", function(){ input.val('@asdpo aoisdj @asodk'); var cursorIndex = 8; func(input, cursorIndex, replaceWith); - expect(input.val()).toBe(replaceWith + ' @asodk'); + expect(input.val()).toBe(replaceWith + 'aoisdj @asodk'); }); it("replaces everything after the 2nd @ if there are 2 @s and the cursor after them", function(){ input.val('@asod asdo @asd asok'); var cursorIndex = 15; func(input, cursorIndex, replaceWith); - expect(input.val()).toBe('@asod asdo ' + replaceWith); + expect(input.val()).toBe('@asod asdo ' + replaceWith + ' asok'); }); }); });