From c1db8b53531c7bfe75a831b7deb52c3e1b40c1df Mon Sep 17 00:00:00 2001 From: Nicolas Perriault Date: Fri, 28 Nov 2014 09:51:15 +0100 Subject: [PATCH 01/10] Bug 1090173 - Allow to rename a room from Loop panel. --- browser/components/loop/content/js/panel.js | 81 +++++++++++++++++-- browser/components/loop/content/js/panel.jsx | 81 +++++++++++++++++-- .../loop/content/shared/css/panel.css | 27 ++++++- .../loop/test/desktop-local/panel_test.js | 36 +++++++++ browser/components/loop/ui/fake-mozLoop.js | 9 ++- 5 files changed, 211 insertions(+), 23 deletions(-) diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index 48392b4f825e..61e135eaf2df 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -521,6 +521,65 @@ loop.panel = (function(_, mozL10n) { } }); + var EditInPlace = React.createClass({displayName: 'EditInPlace', + mixins: [React.addons.LinkedStateMixin], + + propTypes: { + onChange: React.PropTypes.func.isRequired, + text: React.PropTypes.string, + }, + + getDefaultProps: function() { + return {text: ""}; + }, + + getInitialState: function() { + return {edit: false, text: this.props.text}; + }, + + handleTextClick: function(event) { + event.stopPropagation(); + event.preventDefault(); + this.setState({edit: true}, function() { + this.getDOMNode().querySelector("input").select(); + }.bind(this)); + }, + + handleInputClick: function(event) { + event.stopPropagation(); + }, + + handleFormSubmit: function(event) { + event.preventDefault(); + this.props.onChange(this.state.text); + this.setState({edit: false}); + }, + + cancelEdit: function(event) { + event.stopPropagation(); + event.preventDefault(); + this.setState({edit: false, text: this.props.text}); + }, + + render: function() { + if (!this.state.edit) { + return ( + React.DOM.span({className: "edit-in-place", onClick: this.handleTextClick, + title: mozL10n.get("rooms_name_this_room_tooltip2")}, + this.state.text + ) + ); + } + return ( + React.DOM.form({onSubmit: this.handleFormSubmit}, + React.DOM.input({type: "text", valueLink: this.linkState("text"), + onClick: this.handleInputClick, + onBlur: this.cancelEdit}) + ) + ); + } + }); + /** * Room list entry. */ @@ -539,7 +598,7 @@ loop.panel = (function(_, mozL10n) { (nextState.urlCopied !== this.state.urlCopied); }, - handleClickRoomUrl: function(event) { + handleClickEntry: function(event) { event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.OpenRoom({ roomToken: this.props.room.roomToken @@ -547,6 +606,7 @@ loop.panel = (function(_, mozL10n) { }, handleCopyButtonClick: function(event) { + event.stopPropagation(); event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({ roomUrl: this.props.room.roomUrl @@ -555,6 +615,7 @@ loop.panel = (function(_, mozL10n) { }, handleDeleteButtonClick: function(event) { + event.stopPropagation(); event.preventDefault(); // XXX We should prompt end user for confirmation; see bug 1092953. this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({ @@ -562,6 +623,13 @@ loop.panel = (function(_, mozL10n) { })); }, + renameRoom: function(newRoomName) { + this.props.dispatcher.dispatch(new sharedActions.RenameRoom({ + roomToken: this.props.room.roomToken, + newRoomName: newRoomName + })); + }, + handleMouseLeave: function(event) { this.setState({urlCopied: false}); }, @@ -582,10 +650,11 @@ loop.panel = (function(_, mozL10n) { }); return ( - React.DOM.div({className: roomClasses, onMouseLeave: this.handleMouseLeave}, + React.DOM.div({className: roomClasses, onMouseLeave: this.handleMouseLeave, + onClick: this.handleClickEntry}, React.DOM.h2(null, React.DOM.span({className: "room-notification"}), - room.roomName, + EditInPlace({text: room.roomName, onChange: this.renameRoom}), React.DOM.button({className: copyButtonClasses, title: mozL10n.get("rooms_list_copy_url_tooltip"), onClick: this.handleCopyButtonClick}), @@ -593,11 +662,7 @@ loop.panel = (function(_, mozL10n) { title: mozL10n.get("rooms_list_delete_tooltip"), onClick: this.handleDeleteButtonClick}) ), - React.DOM.p(null, - React.DOM.a({href: "#", onClick: this.handleClickRoomUrl}, - room.roomUrl - ) - ) + React.DOM.p(null, React.DOM.a({href: "#"}, room.roomUrl)) ) ); } diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx index a63892fe92fc..edce0487913b 100644 --- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -521,6 +521,65 @@ loop.panel = (function(_, mozL10n) { } }); + var EditInPlace = React.createClass({ + mixins: [React.addons.LinkedStateMixin], + + propTypes: { + onChange: React.PropTypes.func.isRequired, + text: React.PropTypes.string, + }, + + getDefaultProps: function() { + return {text: ""}; + }, + + getInitialState: function() { + return {edit: false, text: this.props.text}; + }, + + handleTextClick: function(event) { + event.stopPropagation(); + event.preventDefault(); + this.setState({edit: true}, function() { + this.getDOMNode().querySelector("input").select(); + }.bind(this)); + }, + + handleInputClick: function(event) { + event.stopPropagation(); + }, + + handleFormSubmit: function(event) { + event.preventDefault(); + this.props.onChange(this.state.text); + this.setState({edit: false}); + }, + + cancelEdit: function(event) { + event.stopPropagation(); + event.preventDefault(); + this.setState({edit: false, text: this.props.text}); + }, + + render: function() { + if (!this.state.edit) { + return ( + + {this.state.text} + + ); + } + return ( +
+ +
+ ); + } + }); + /** * Room list entry. */ @@ -539,7 +598,7 @@ loop.panel = (function(_, mozL10n) { (nextState.urlCopied !== this.state.urlCopied); }, - handleClickRoomUrl: function(event) { + handleClickEntry: function(event) { event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.OpenRoom({ roomToken: this.props.room.roomToken @@ -547,6 +606,7 @@ loop.panel = (function(_, mozL10n) { }, handleCopyButtonClick: function(event) { + event.stopPropagation(); event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({ roomUrl: this.props.room.roomUrl @@ -555,6 +615,7 @@ loop.panel = (function(_, mozL10n) { }, handleDeleteButtonClick: function(event) { + event.stopPropagation(); event.preventDefault(); // XXX We should prompt end user for confirmation; see bug 1092953. this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({ @@ -562,6 +623,13 @@ loop.panel = (function(_, mozL10n) { })); }, + renameRoom: function(newRoomName) { + this.props.dispatcher.dispatch(new sharedActions.RenameRoom({ + roomToken: this.props.room.roomToken, + newRoomName: newRoomName + })); + }, + handleMouseLeave: function(event) { this.setState({urlCopied: false}); }, @@ -582,10 +650,11 @@ loop.panel = (function(_, mozL10n) { }); return ( -
+

- {room.roomName} +

-

- - {room.roomUrl} - -

+

{room.roomUrl}

); } diff --git a/browser/components/loop/content/shared/css/panel.css b/browser/components/loop/content/shared/css/panel.css index cc526739c866..8cdb47dac7f5 100644 --- a/browser/components/loop/content/shared/css/panel.css +++ b/browser/components/loop/content/shared/css/panel.css @@ -204,9 +204,11 @@ body { .room-list > .room-entry { padding: .5rem 1rem; + cursor: pointer; } .room-list > .room-entry > h2 { + display: inline-block; font-size: .85rem; color: #777; } @@ -298,16 +300,33 @@ body { top: 0px; } -.room-list > .room-entry > h2 { - display: inline-block; -} - /* keep the various room-entry row pieces aligned with each other */ .room-list > .room-entry > h2 > button, .room-list > .room-entry > h2 > span { vertical-align: middle; } +/* Edit in place */ + +.room-list > .room-entry > h2 > .edit-in-place { + cursor: text; +} + +.room-list > .room-entry > h2 > .edit-in-place:hover { + background: #fefbc4; +} + +.room-list > .room-entry > h2 > form { + display: inline-block; +} + +.room-list > .room-entry > h2 > form > input { + border: none; + background: #fefbc4; + font-size: 13.6px; + padding: 0; +} + /* Buttons */ .button-group { diff --git a/browser/components/loop/test/desktop-local/panel_test.js b/browser/components/loop/test/desktop-local/panel_test.js index e678c4a9405e..1dfc15d4daf7 100644 --- a/browser/components/loop/test/desktop-local/panel_test.js +++ b/browser/components/loop/test/desktop-local/panel_test.js @@ -714,6 +714,42 @@ describe("loop.panel", function() { return TestUtils.renderIntoDocument(loop.panel.RoomEntry(props)); } + describe("Edit room name", function() { + var roomEntry, domNode; + + beforeEach(function() { + roomEntry = mountRoomEntry({ + dispatcher: dispatcher, + deleteRoom: sandbox.stub(), + room: new loop.store.Room(roomData) + }); + domNode = roomEntry.getDOMNode(); + + TestUtils.Simulate.click(domNode.querySelector(".edit-in-place")); + }); + + it("should render an edit form on room name click", function() { + expect(domNode.querySelector("form")).not.eql(null); + expect(domNode.querySelector("input").value).eql(roomData.roomName); + }); + + it("should dispatch a RenameRoom action when submitting the form", + function() { + var dispatch = sandbox.stub(dispatcher, "dispatch"); + + TestUtils.Simulate.change(domNode.querySelector("input"), { + target: {value: "New name"} + }); + TestUtils.Simulate.submit(domNode.querySelector("form")); + + sinon.assert.calledOnce(dispatch); + sinon.assert.calledWithExactly(dispatch, new sharedActions.RenameRoom({ + roomToken: roomData.roomToken, + newRoomName: "New name" + })); + }); + }); + describe("Copy button", function() { var roomEntry, copyButton; diff --git a/browser/components/loop/ui/fake-mozLoop.js b/browser/components/loop/ui/fake-mozLoop.js index 0794104c76cf..3fdce1941fcc 100644 --- a/browser/components/loop/ui/fake-mozLoop.js +++ b/browser/components/loop/ui/fake-mozLoop.js @@ -51,9 +51,12 @@ navigator.mozLoop = { ensureRegistered: function() {}, getAudioBlob: function(){}, getLoopPref: function(pref) { - // Ensure UI for rooms is displayed in the showcase. - if (pref === "rooms.enabled") { - return true; + switch(pref) { + // Ensure UI for rooms is displayed in the showcase. + case "rooms.enabled": + // Ensure we skip FTE completely. + case "gettingStarted.seen": + return true; } }, setLoopPref: function(){}, From dee3c94d201c571aad0b1b40cce4c2df1db555af Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Thu, 27 Nov 2014 15:50:33 +0100 Subject: [PATCH 02/10] Bug 1104755 - Re-enable browser_tabMatchesInAwesomebar_perwindowpb.js r=mak --- browser/base/content/test/general/browser.ini | 2 +- .../general/browser_tabMatchesInAwesomebar_perwindowpb.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index 0027f1375fe4..1404034e1c00 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -415,7 +415,7 @@ skip-if = buildapp == 'mulet' || e10s [browser_tabMatchesInAwesomebar.js] skip-if = e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al for e10s (test calls gBrowser.swapBrowsersAndCloseOther) [browser_tabMatchesInAwesomebar_perwindowpb.js] -skip-if = e10s || true # Bug 1093373, Bug 1104755 +skip-if = e10s # Bug 1093373 [browser_tab_drag_drop_perwindow.js] skip-if = buildapp == 'mulet' [browser_tab_dragdrop.js] diff --git a/browser/base/content/test/general/browser_tabMatchesInAwesomebar_perwindowpb.js b/browser/base/content/test/general/browser_tabMatchesInAwesomebar_perwindowpb.js index 5b041b6a5209..e63bfbe37ed9 100644 --- a/browser/base/content/test/general/browser_tabMatchesInAwesomebar_perwindowpb.js +++ b/browser/base/content/test/general/browser_tabMatchesInAwesomebar_perwindowpb.js @@ -80,7 +80,8 @@ function test() { controller.startSearch(testURL); // Wait for the Awesomebar popup to appear. - promisePopupShown(aDestWindow.gURLBar.popup).then(() => { + let popup = aDestWindow.gURLBar.popup; + promisePopupShown(popup).then(() => { function searchIsComplete() { return controller.searchStatus == Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH; @@ -106,8 +107,8 @@ function test() { }, true); } - // Select the second match, if any. - if (controller.matchCount > 1) { + // Make sure the last match is selected. + while (popup.selectedIndex < controller.matchCount - 1) { controller.handleKeyNavigation(KeyEvent.DOM_VK_DOWN); } From c20ee339fb3f9d488f75a2546701fc90332107e9 Mon Sep 17 00:00:00 2001 From: Mike de Boer Date: Fri, 28 Nov 2014 11:45:17 +0100 Subject: [PATCH 03/10] Bug 1092953: show a modal confirm dialog when a user attempts to delete a room. r=paolo --- browser/components/loop/MozLoopAPI.jsm | 24 ++++++++++----- .../components/loop/content/js/contacts.js | 30 +++++++++---------- .../components/loop/content/js/contacts.jsx | 30 +++++++++---------- browser/components/loop/content/js/panel.js | 21 ++++++++++--- browser/components/loop/content/js/panel.jsx | 21 ++++++++++--- 5 files changed, 81 insertions(+), 45 deletions(-) diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index b350e1c8ed72..06698671de8d 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -365,21 +365,31 @@ function injectLoopAPI(targetWindow) { /** * Displays a confirmation dialog using the specified strings. * - * Callback parameters: - * - err null on success, non-null on unexpected failure to show the prompt. - * - {Boolean} True if the user chose the OK button. + * @param {Object} options Confirm dialog options + * @param {Function} callback Function that will be invoked once the operation + * finished. The first argument passed will be an + * `Error` object or `null`. The second argument + * will be the result of the operation, TRUE if + * the user chose the OK button. */ confirm: { enumerable: true, writable: true, - value: function(bodyMessage, okButtonMessage, cancelButtonMessage, callback) { - try { - let buttonFlags = + value: function(options, callback) { + let buttonFlags; + if (options.okButton && options.cancelButton) { + buttonFlags = (Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING) + (Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING); + } else if (!options.okButton && !options.cancelButton) { + buttonFlags = Services.prompt.STD_YES_NO_BUTTONS; + } else { + callback(cloneValueInto(new Error("confirm: missing button options"), targetWindow)); + } + try { let chosenButton = Services.prompt.confirmEx(null, "", - bodyMessage, buttonFlags, okButtonMessage, cancelButtonMessage, + options.message, buttonFlags, options.okButton, options.cancelButton, null, null, {}); callback(null, chosenButton == 0); diff --git a/browser/components/loop/content/js/contacts.js b/browser/components/loop/content/js/contacts.js index 0c8989e2a4db..db4f2dafdd2a 100644 --- a/browser/components/loop/content/js/contacts.js +++ b/browser/components/loop/content/js/contacts.js @@ -403,25 +403,25 @@ loop.contacts = (function(_, mozL10n) { this.props.startForm("contacts_edit", contact); break; case "remove": - navigator.mozLoop.confirm( - mozL10n.get("confirm_delete_contact_alert"), - mozL10n.get("confirm_delete_contact_remove_button"), - mozL10n.get("confirm_delete_contact_cancel_button"), - (err, result) => { + navigator.mozLoop.confirm({ + message: mozL10n.get("confirm_delete_contact_alert"), + okButton: mozL10n.get("confirm_delete_contact_remove_button"), + cancelButton: mozL10n.get("confirm_delete_contact_cancel_button") + }, (err, result) => { + if (err) { + throw err; + } + + if (!result) { + return; + } + + navigator.mozLoop.contacts.remove(contact._guid, err => { if (err) { throw err; } - - if (!result) { - return; - } - - navigator.mozLoop.contacts.remove(contact._guid, err => { - if (err) { - throw err; - } - }); }); + }); break; case "block": case "unblock": diff --git a/browser/components/loop/content/js/contacts.jsx b/browser/components/loop/content/js/contacts.jsx index 7ccecf107d8b..ed9a2b2e5855 100644 --- a/browser/components/loop/content/js/contacts.jsx +++ b/browser/components/loop/content/js/contacts.jsx @@ -403,25 +403,25 @@ loop.contacts = (function(_, mozL10n) { this.props.startForm("contacts_edit", contact); break; case "remove": - navigator.mozLoop.confirm( - mozL10n.get("confirm_delete_contact_alert"), - mozL10n.get("confirm_delete_contact_remove_button"), - mozL10n.get("confirm_delete_contact_cancel_button"), - (err, result) => { + navigator.mozLoop.confirm({ + message: mozL10n.get("confirm_delete_contact_alert"), + okButton: mozL10n.get("confirm_delete_contact_remove_button"), + cancelButton: mozL10n.get("confirm_delete_contact_cancel_button") + }, (err, result) => { + if (err) { + throw err; + } + + if (!result) { + return; + } + + navigator.mozLoop.contacts.remove(contact._guid, err => { if (err) { throw err; } - - if (!result) { - return; - } - - navigator.mozLoop.contacts.remove(contact._guid, err => { - if (err) { - throw err; - } - }); }); + }); break; case "block": case "unblock": diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index 61e135eaf2df..760ec4db8ce6 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -617,10 +617,23 @@ loop.panel = (function(_, mozL10n) { handleDeleteButtonClick: function(event) { event.stopPropagation(); event.preventDefault(); - // XXX We should prompt end user for confirmation; see bug 1092953. - this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({ - roomToken: this.props.room.roomToken - })); + navigator.mozLoop.confirm({ + message: mozL10n.get("rooms_list_deleteConfirmation_label"), + okButton: null, + cancelButton: null + }, function(err, result) { + if (err) { + throw err; + } + + if (!result) { + return; + } + + this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({ + roomToken: this.props.room.roomToken + })); + }.bind(this)); }, renameRoom: function(newRoomName) { diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx index edce0487913b..76c084b71f87 100644 --- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -617,10 +617,23 @@ loop.panel = (function(_, mozL10n) { handleDeleteButtonClick: function(event) { event.stopPropagation(); event.preventDefault(); - // XXX We should prompt end user for confirmation; see bug 1092953. - this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({ - roomToken: this.props.room.roomToken - })); + navigator.mozLoop.confirm({ + message: mozL10n.get("rooms_list_deleteConfirmation_label"), + okButton: null, + cancelButton: null + }, function(err, result) { + if (err) { + throw err; + } + + if (!result) { + return; + } + + this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({ + roomToken: this.props.room.roomToken + })); + }.bind(this)); }, renameRoom: function(newRoomName) { From e37d62973d3fba2c311cb1afaf91740de2e7386f Mon Sep 17 00:00:00 2001 From: Mike de Boer Date: Fri, 28 Nov 2014 11:45:21 +0100 Subject: [PATCH 04/10] Bug 1092953: update the room delete button test to take the confirm dialog into account. r=Standard8 --- .../loop/test/desktop-local/panel_test.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/browser/components/loop/test/desktop-local/panel_test.js b/browser/components/loop/test/desktop-local/panel_test.js index 1dfc15d4daf7..c5c2bab4e15d 100644 --- a/browser/components/loop/test/desktop-local/panel_test.js +++ b/browser/components/loop/test/desktop-local/panel_test.js @@ -54,7 +54,8 @@ describe("loop.panel", function() { callback(null, []); }, on: sandbox.stub() - } + }, + confirm: sandbox.stub() }; document.mozL10n.initialize(navigator.mozLoop); @@ -815,15 +816,27 @@ describe("loop.panel", function() { expect(deleteButton).to.not.equal(null); }); - it("should call the delete function when clicked", function() { + it("should dispatch a delete action when confirmation is granted", function() { sandbox.stub(dispatcher, "dispatch"); + navigator.mozLoop.confirm.callsArgWith(1, null, true); TestUtils.Simulate.click(deleteButton); + sinon.assert.calledOnce(navigator.mozLoop.confirm); sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.DeleteRoom({roomToken: roomData.roomToken})); }); + + it("should not dispatch an action when the confirmation is cancelled", function() { + sandbox.stub(dispatcher, "dispatch"); + + navigator.mozLoop.confirm.callsArgWith(1, null, false); + TestUtils.Simulate.click(deleteButton); + + sinon.assert.calledOnce(navigator.mozLoop.confirm); + sinon.assert.notCalled(dispatcher.dispatch); + }); }); describe("Room URL click", function() { From f84b5876e682cf48977c2e706432cf8b15c0af82 Mon Sep 17 00:00:00 2001 From: Patrick Brosset Date: Fri, 28 Nov 2014 12:12:23 +0100 Subject: [PATCH 05/10] Bug 1098435 - Ignore reflows caused by the highlighter in the LayoutChangesObserver; r=paul --- toolkit/devtools/server/actors/highlighter.js | 17 +++++++++++- toolkit/devtools/server/actors/layout.js | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/toolkit/devtools/server/actors/highlighter.js b/toolkit/devtools/server/actors/highlighter.js index 44ecbcbe8c1d..bc9ea1da67e7 100644 --- a/toolkit/devtools/server/actors/highlighter.js +++ b/toolkit/devtools/server/actors/highlighter.js @@ -12,6 +12,7 @@ const events = require("sdk/event/core"); const Heritage = require("sdk/core/heritage"); const {CssLogic} = require("devtools/styleinspector/css-logic"); const EventEmitter = require("devtools/toolkit/event-emitter"); +const {setIgnoreLayoutChanges} = require("devtools/server/actors/layout"); Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -984,6 +985,8 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype * Should be called whenever node size or attributes change */ _update: function() { + setIgnoreLayoutChanges(true); + if (this._updateBoxModel()) { if (!this.options.hideInfoBar) { this._showInfobar(); @@ -995,15 +998,21 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype // Nothing to highlight (0px rectangle like a