author | Michael Ratcliffe <mratcliffe@mozilla.com> |
Wed, 20 Mar 2013 12:11:50 +0000 | |
changeset 125735 | 672608b227c3ca1d778cb581ac5297bc214cdcd0 |
parent 125734 | 8caaaf8e18946ecb65051148872775beac244393 |
child 125736 | f6e7195e87dbf93f65e36b0214f9ff15c65474ed |
push id | 25087 |
push user | eakhgari@mozilla.com |
push date | Thu, 21 Mar 2013 12:27:40 +0000 |
treeherder | mozilla-inbound@be6da6dbf632 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | jwalker |
bugs | 845822 |
milestone | 22.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/devtools/markupview/MarkupView.jsm +++ b/browser/devtools/markupview/MarkupView.jsm @@ -13,16 +13,17 @@ const PAGE_SIZE = 10; const PREVIEW_AREA = 700; const DEFAULT_MAX_CHILDREN = 100; this.EXPORTED_SYMBOLS = ["MarkupView"]; Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); Cu.import("resource:///modules/devtools/CssRuleView.jsm"); +Cu.import("resource:///modules/devtools/InplaceEditor.jsm"); Cu.import("resource:///modules/devtools/Templater.jsm"); Cu.import("resource:///modules/devtools/Undo.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /** * Vocabulary for the purposes of this file: * @@ -957,17 +958,17 @@ function DoctypeEditor(aContainer, aNode * @param string aTemplate The template id to use to build the editor. */ function TextEditor(aContainer, aNode, aTemplate) { this.node = aNode; aContainer.markup.template(aTemplate, this); - _editableField({ + editableField({ element: this.value, stopOnReturn: true, trigger: "dblclick", multiline: true, done: function TE_done(aVal, aCommit) { if (!aCommit) { return; } @@ -1026,26 +1027,26 @@ function ElementEditor(aContainer, aNode } // Create the closing tag this.template("elementClose", this); // Make the tag name editable (unless this is a document element) if (aNode != aNode.ownerDocument.documentElement) { this.tag.setAttribute("tabindex", "0"); - _editableField({ + editableField({ element: this.tag, trigger: "dblclick", stopOnReturn: true, done: this.onTagEdit.bind(this), }); } // Make the new attribute space editable. - _editableField({ + editableField({ element: this.newAttr, trigger: "dblclick", stopOnReturn: true, done: function EE_onNew(aVal, aCommit) { if (!aCommit) { return; } @@ -1115,17 +1116,17 @@ ElementEditor.prototype = { before = this.attrList.firstChild; } else if (aAttr.name == "class") { let idNode = this.attrs["id"]; before = idNode ? idNode.nextSibling : this.attrList.firstChild; } this.attrList.insertBefore(attr, before); // Make the attribute editable. - _editableField({ + editableField({ element: inner, trigger: "dblclick", stopOnReturn: true, selectAll: false, start: function EE_editAttribute_start(aEditor, aEvent) { // If the editing was started inside the name or value areas, // select accordingly. if (aEvent && aEvent.target === name) {
--- a/browser/devtools/markupview/test/browser_inspector_markup_edit.js +++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.js @@ -13,20 +13,19 @@ http://creativecommons.org/publicdomain/ * * This test mostly tries to verify that the editor makes changes to the * underlying DOM, not that the UI updates - UI updates are based on * underlying DOM changes, and the mutation tests should cover those cases. */ function test() { let inspector; - let tempScope = {} - Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); - - let inplaceEditor = tempScope._getInplaceEditorForSpan; + let { + getInplaceEditorForSpan: inplaceEditor + } = Cu.import("resource:///modules/devtools/InplaceEditor.jsm", {}); waitForExplicitFinish(); // Will hold the doc we're viewing let doc; // Holds the MarkupTool object we're testing. let markup;
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/InplaceEditor.jsm @@ -0,0 +1,849 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ + +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Basic use: + * let spanToEdit = document.getElementById("somespan"); + * + * editableField({ + * element: spanToEdit, + * done: function(value, commit) { + * if (commit) { + * spanToEdit.textContent = value; + * } + * }, + * trigger: "dblclick" + * }); + * + * See editableField() for more options. + */ + +"use strict"; + +const Ci = Components.interfaces; +const Cu = Components.utils; + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD; +const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +this.EXPORTED_SYMBOLS = ["editableItem", + "editableField", + "getInplaceEditorForSpan", + "InplaceEditor"]; + +/** + * Mark a span editable. |editableField| will listen for the span to + * be focused and create an InlineEditor to handle text input. + * Changes will be committed when the InlineEditor's input is blurred + * or dropped when the user presses escape. + * + * @param {object} aOptions + * Options for the editable field, including: + * {Element} element: + * (required) The span to be edited on focus. + * {function} canEdit: + * Will be called before creating the inplace editor. Editor + * won't be created if canEdit returns false. + * {function} start: + * Will be called when the inplace editor is initialized. + * {function} change: + * Will be called when the text input changes. Will be called + * with the current value of the text input. + * {function} done: + * Called when input is committed or blurred. Called with + * current value and a boolean telling the caller whether to + * commit the change. This function is called before the editor + * has been torn down. + * {function} destroy: + * Called when the editor is destroyed and has been torn down. + * {string} advanceChars: + * If any characters in advanceChars are typed, focus will advance + * to the next element. + * {boolean} stopOnReturn: + * If true, the return key will not advance the editor to the next + * focusable element. + * {string} trigger: The DOM event that should trigger editing, + * defaults to "click" + */ +function editableField(aOptions) +{ + return editableItem(aOptions, function(aElement, aEvent) { + new InplaceEditor(aOptions, aEvent); + }); +} + +/** + * Handle events for an element that should respond to + * clicks and sit in the editing tab order, and call + * a callback when it is activated. + * + * @param {object} aOptions + * The options for this editor, including: + * {Element} element: The DOM element. + * {string} trigger: The DOM event that should trigger editing, + * defaults to "click" + * @param {function} aCallback + * Called when the editor is activated. + */ +this.editableItem = function editableItem(aOptions, aCallback) +{ + let trigger = aOptions.trigger || "click" + let element = aOptions.element; + element.addEventListener(trigger, function(evt) { + let win = this.ownerDocument.defaultView; + let selection = win.getSelection(); + if (trigger != "click" || selection.isCollapsed) { + aCallback(element, evt); + } + evt.stopPropagation(); + }, false); + + // If focused by means other than a click, start editing by + // pressing enter or space. + element.addEventListener("keypress", function(evt) { + if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN || + evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) { + aCallback(element); + } + }, true); + + // Ugly workaround - the element is focused on mousedown but + // the editor is activated on click/mouseup. This leads + // to an ugly flash of the focus ring before showing the editor. + // So hide the focus ring while the mouse is down. + element.addEventListener("mousedown", function(evt) { + let cleanup = function() { + element.style.removeProperty("outline-style"); + element.removeEventListener("mouseup", cleanup, false); + element.removeEventListener("mouseout", cleanup, false); + }; + element.style.setProperty("outline-style", "none"); + element.addEventListener("mouseup", cleanup, false); + element.addEventListener("mouseout", cleanup, false); + }, false); + + // Mark the element editable field for tab + // navigation while editing. + element._editable = true; +} + +/* + * Various API consumers (especially tests) sometimes want to grab the + * inplaceEditor expando off span elements. However, when each global has its + * own compartment, those expandos live on Xray wrappers that are only visible + * within this JSM. So we provide a little workaround here. + */ +this.getInplaceEditorForSpan = function getInplaceEditorForSpan(aSpan) +{ + return aSpan.inplaceEditor; +}; + +function InplaceEditor(aOptions, aEvent) +{ + this.elt = aOptions.element; + let doc = this.elt.ownerDocument; + this.doc = doc; + this.elt.inplaceEditor = this; + + this.change = aOptions.change; + this.done = aOptions.done; + this.destroy = aOptions.destroy; + this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent; + this.multiline = aOptions.multiline || false; + this.stopOnReturn = !!aOptions.stopOnReturn; + + this._onBlur = this._onBlur.bind(this); + this._onKeyPress = this._onKeyPress.bind(this); + this._onInput = this._onInput.bind(this); + this._onKeyup = this._onKeyup.bind(this); + + this._createInput(); + this._autosize(); + + // Pull out character codes for advanceChars, listing the + // characters that should trigger a blur. + this._advanceCharCodes = {}; + let advanceChars = aOptions.advanceChars || ''; + for (let i = 0; i < advanceChars.length; i++) { + this._advanceCharCodes[advanceChars.charCodeAt(i)] = true; + } + + // Hide the provided element and add our editor. + this.originalDisplay = this.elt.style.display; + this.elt.style.display = "none"; + this.elt.parentNode.insertBefore(this.input, this.elt); + + if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) { + this.input.select(); + } + this.input.focus(); + + this.input.addEventListener("blur", this._onBlur, false); + this.input.addEventListener("keypress", this._onKeyPress, false); + this.input.addEventListener("input", this._onInput, false); + this.input.addEventListener("mousedown", function(aEvt) { + aEvt.stopPropagation(); + }, false); + + this.warning = aOptions.warning; + this.validate = aOptions.validate; + + if (this.warning && this.validate) { + this.input.addEventListener("keyup", this._onKeyup, false); + } + + if (aOptions.start) { + aOptions.start(this, aEvent); + } +} + +InplaceEditor.prototype = { + _createInput: function InplaceEditor_createEditor() + { + this.input = + this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input"); + this.input.inplaceEditor = this; + this.input.classList.add("styleinspector-propertyeditor"); + this.input.value = this.initial; + + copyTextStyles(this.elt, this.input); + }, + + /** + * Get rid of the editor. + */ + _clear: function InplaceEditor_clear() + { + if (!this.input) { + // Already cleared. + return; + } + + this.input.removeEventListener("blur", this._onBlur, false); + this.input.removeEventListener("keypress", this._onKeyPress, false); + this.input.removeEventListener("keyup", this._onKeyup, false); + this.input.removeEventListener("oninput", this._onInput, false); + this._stopAutosize(); + + this.elt.style.display = this.originalDisplay; + this.elt.focus(); + + if (this.destroy) { + this.destroy(); + } + + this.elt.parentNode.removeChild(this.input); + this.input = null; + + delete this.elt.inplaceEditor; + delete this.elt; + }, + + /** + * Keeps the editor close to the size of its input string. This is pretty + * crappy, suggestions for improvement welcome. + */ + _autosize: function InplaceEditor_autosize() + { + // Create a hidden, absolutely-positioned span to measure the text + // in the input. Boo. + + // We can't just measure the original element because a) we don't + // change the underlying element's text ourselves (we leave that + // up to the client), and b) without tweaking the style of the + // original element, it might wrap differently or something. + this._measurement = + this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span"); + this._measurement.className = "autosizer"; + this.elt.parentNode.appendChild(this._measurement); + let style = this._measurement.style; + style.visibility = "hidden"; + style.position = "absolute"; + style.top = "0"; + style.left = "0"; + copyTextStyles(this.input, this._measurement); + this._updateSize(); + }, + + /** + * Clean up the mess created by _autosize(). + */ + _stopAutosize: function InplaceEditor_stopAutosize() + { + if (!this._measurement) { + return; + } + this._measurement.parentNode.removeChild(this._measurement); + delete this._measurement; + }, + + /** + * Size the editor to fit its current contents. + */ + _updateSize: function InplaceEditor_updateSize() + { + // Replace spaces with non-breaking spaces. Otherwise setting + // the span's textContent will collapse spaces and the measurement + // will be wrong. + this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0'); + + // We add a bit of padding to the end. Should be enough to fit + // any letter that could be typed, otherwise we'll scroll before + // we get a chance to resize. Yuck. + let width = this._measurement.offsetWidth + 10; + + if (this.multiline) { + // Make sure there's some content in the current line. This is a hack to + // account for the fact that after adding a newline the <pre> doesn't grow + // unless there's text content on the line. + width += 15; + this._measurement.textContent += "M"; + this.input.style.height = this._measurement.offsetHeight + "px"; + } + + this.input.style.width = width + "px"; + }, + + /** + * Increment property values in rule view. + * + * @param {number} increment + * The amount to increase/decrease the property value. + * @return {bool} true if value has been incremented. + */ + _incrementValue: function InplaceEditor_incrementValue(increment) + { + let value = this.input.value; + let selectionStart = this.input.selectionStart; + let selectionEnd = this.input.selectionEnd; + + let newValue = this._incrementCSSValue(value, increment, selectionStart, + selectionEnd); + + if (!newValue) { + return false; + } + + this.input.value = newValue.value; + this.input.setSelectionRange(newValue.start, newValue.end); + + return true; + }, + + /** + * Increment the property value based on the property type. + * + * @param {string} value + * Property value. + * @param {number} increment + * Amount to increase/decrease the property value. + * @param {number} selStart + * Starting index of the value. + * @param {number} selEnd + * Ending index of the value. + * @return {object} object with properties 'value', 'start', and 'end'. + */ + _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment, + selStart, selEnd) + { + let range = this._parseCSSValue(value, selStart); + let type = (range && range.type) || ""; + let rawValue = (range ? value.substring(range.start, range.end) : ""); + let incrementedValue = null, selection; + + if (type === "num") { + let newValue = this._incrementRawValue(rawValue, increment); + if (newValue !== null) { + incrementedValue = newValue; + selection = [0, incrementedValue.length]; + } + } else if (type === "hex") { + let exprOffset = selStart - range.start; + let exprOffsetEnd = selEnd - range.start; + let newValue = this._incHexColor(rawValue, increment, exprOffset, + exprOffsetEnd); + if (newValue) { + incrementedValue = newValue.value; + selection = newValue.selection; + } + } else { + let info; + if (type === "rgb" || type === "hsl") { + info = {}; + let part = value.substring(range.start, selStart).split(",").length - 1; + if (part === 3) { // alpha + info.minValue = 0; + info.maxValue = 1; + } else if (type === "rgb") { + info.minValue = 0; + info.maxValue = 255; + } else if (part !== 0) { // hsl percentage + info.minValue = 0; + info.maxValue = 100; + + // select the previous number if the selection is at the end of a + // percentage sign. + if (value.charAt(selStart - 1) === "%") { + --selStart; + } + } + } + return this._incrementGenericValue(value, increment, selStart, selEnd, info); + } + + if (incrementedValue === null) { + return; + } + + let preRawValue = value.substr(0, range.start); + let postRawValue = value.substr(range.end); + + return { + value: preRawValue + incrementedValue + postRawValue, + start: range.start + selection[0], + end: range.start + selection[1] + }; + }, + + /** + * Parses the property value and type. + * + * @param {string} value + * Property value. + * @param {number} offset + * Starting index of value. + * @return {object} object with properties 'value', 'start', 'end', and 'type'. + */ + _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset) + { + const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/; + let start = 0; + let m; + + // retreive values from left to right until we find the one at our offset + while ((m = reSplitCSS.exec(value)) && + (m.index + m[0].length < offset)) { + value = value.substr(m.index + m[0].length); + start += m.index + m[0].length; + offset -= m.index + m[0].length; + } + + if (!m) { + return; + } + + let type; + if (m[1]) { + type = "url"; + } else if (m[2]) { + type = "rgb"; + } else if (m[3]) { + type = "hsl"; + } else if (m[4]) { + type = "hex"; + } else if (m[5]) { + type = "num"; + } + + return { + value: m[0], + start: start + m.index, + end: start + m.index + m[0].length, + type: type + }; + }, + + /** + * Increment the property value for types other than + * number or hex, such as rgb, hsl, and file names. + * + * @param {string} value + * Property value. + * @param {number} increment + * Amount to increment/decrement. + * @param {number} offset + * Starting index of the property value. + * @param {number} offsetEnd + * Ending index of the property value. + * @param {object} info + * Object with details about the property value. + * @return {object} object with properties 'value', 'start', and 'end'. + */ + _incrementGenericValue: + function InplaceEditor_incrementGenericValue(value, increment, offset, + offsetEnd, info) + { + // Try to find a number around the cursor to increment. + let start, end; + // Check if we are incrementing in a non-number context (such as a URL) + if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) && + !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) { + // We have a number selected, possibly with a suffix, and we are not in + // the disallowed case of just part of a known number being selected. + // Use that number. + start = offset; + end = offsetEnd; + } else { + // Parse periods as belonging to the number only if we are in a known number + // context. (This makes incrementing the 1 in 'image1.gif' work.) + let pattern = "[" + (info ? "0-9." : "0-9") + "]*"; + let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length; + let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length; + + start = offset - before; + end = offset + after; + + // Expand the number to contain an initial minus sign if it seems + // free-standing. + if (value.charAt(start - 1) === "-" && + (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) { + --start; + } + } + + if (start !== end) + { + // Include percentages as part of the incremented number (they are + // common enough). + if (value.charAt(end) === "%") { + ++end; + } + + let first = value.substr(0, start); + let mid = value.substring(start, end); + let last = value.substr(end); + + mid = this._incrementRawValue(mid, increment, info); + + if (mid !== null) { + return { + value: first + mid + last, + start: start, + end: start + mid.length + }; + } + } + }, + + /** + * Increment the property value for numbers. + * + * @param {string} rawValue + * Raw value to increment. + * @param {number} increment + * Amount to increase/decrease the raw value. + * @param {object} info + * Object with info about the property value. + * @return {string} the incremented value. + */ + _incrementRawValue: + function InplaceEditor_incrementRawValue(rawValue, increment, info) + { + let num = parseFloat(rawValue); + + if (isNaN(num)) { + return null; + } + + let number = /\d+(\.\d+)?/.exec(rawValue); + let units = rawValue.substr(number.index + number[0].length); + + // avoid rounding errors + let newValue = Math.round((num + increment) * 1000) / 1000; + + if (info && "minValue" in info) { + newValue = Math.max(newValue, info.minValue); + } + if (info && "maxValue" in info) { + newValue = Math.min(newValue, info.maxValue); + } + + newValue = newValue.toString(); + + return newValue + units; + }, + + /** + * Increment the property value for hex. + * + * @param {string} value + * Property value. + * @param {number} increment + * Amount to increase/decrease the property value. + * @param {number} offset + * Starting index of the property value. + * @param {number} offsetEnd + * Ending index of the property value. + * @return {object} object with properties 'value' and 'selection'. + */ + _incHexColor: + function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd) + { + // Return early if no part of the rawValue is selected. + if (offsetEnd > rawValue.length && offset >= rawValue.length) { + return; + } + if (offset < 1 && offsetEnd <= 1) { + return; + } + // Ignore the leading #. + rawValue = rawValue.substr(1); + --offset; + --offsetEnd; + + // Clamp the selection to within the actual value. + offset = Math.max(offset, 0); + offsetEnd = Math.min(offsetEnd, rawValue.length); + offsetEnd = Math.max(offsetEnd, offset); + + // Normalize #ABC -> #AABBCC. + if (rawValue.length === 3) { + rawValue = rawValue.charAt(0) + rawValue.charAt(0) + + rawValue.charAt(1) + rawValue.charAt(1) + + rawValue.charAt(2) + rawValue.charAt(2); + offset *= 2; + offsetEnd *= 2; + } + + if (rawValue.length !== 6) { + return; + } + + // If no selection, increment an adjacent color, preferably one to the left. + if (offset === offsetEnd) { + if (offset === 0) { + offsetEnd = 1; + } else { + offset = offsetEnd - 1; + } + } + + // Make the selection cover entire parts. + offset -= offset % 2; + offsetEnd += offsetEnd % 2; + + // Remap the increments from [0.1, 1, 10] to [1, 1, 16]. + if (-1 < increment && increment < 1) { + increment = (increment < 0 ? -1 : 1); + } + if (Math.abs(increment) === 10) { + increment = (increment < 0 ? -16 : 16); + } + + let isUpper = (rawValue.toUpperCase() === rawValue); + + for (let pos = offset; pos < offsetEnd; pos += 2) { + // Increment the part in [pos, pos+2). + let mid = rawValue.substr(pos, 2); + let value = parseInt(mid, 16); + + if (isNaN(value)) { + return; + } + + mid = Math.min(Math.max(value + increment, 0), 255).toString(16); + + while (mid.length < 2) { + mid = "0" + mid; + } + if (isUpper) { + mid = mid.toUpperCase(); + } + + rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2); + } + + return { + value: "#" + rawValue, + selection: [offset + 1, offsetEnd + 1] + }; + }, + + /** + * Call the client's done handler and clear out. + */ + _apply: function InplaceEditor_apply(aEvent) + { + if (this._applied) { + return; + } + + this._applied = true; + + if (this.done) { + let val = this.input.value.trim(); + return this.done(this.cancelled ? this.initial : val, !this.cancelled); + } + + return null; + }, + + /** + * Handle loss of focus by calling done if it hasn't been called yet. + */ + _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear) + { + this._apply(); + if (!aDoNotClear) { + this._clear(); + } + }, + + /** + * Handle the input field's keypress event. + */ + _onKeyPress: function InplaceEditor_onKeyPress(aEvent) + { + let prevent = false; + + const largeIncrement = 100; + const mediumIncrement = 10; + const smallIncrement = 0.1; + + let increment = 0; + + if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP + || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) { + increment = 1; + } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN + || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) { + increment = -1; + } + + if (aEvent.shiftKey && !aEvent.altKey) { + if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP + || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) { + increment *= largeIncrement; + } else { + increment *= mediumIncrement; + } + } else if (aEvent.altKey && !aEvent.shiftKey) { + increment *= smallIncrement; + } + + if (increment && this._incrementValue(increment) ) { + this._updateSize(); + prevent = true; + } + + if (this.multiline && + aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN && + aEvent.shiftKey) { + prevent = false; + } else if (aEvent.charCode in this._advanceCharCodes + || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN + || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) { + prevent = true; + + let direction = FOCUS_FORWARD; + if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB && + aEvent.shiftKey) { + this.cancelled = true; + direction = FOCUS_BACKWARD; + } + if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) { + direction = null; + } + + let input = this.input; + + this._apply(); + + if (direction !== null && focusManager.focusedElement === input) { + // If the focused element wasn't changed by the done callback, + // move the focus as requested. + let next = moveFocus(this.doc.defaultView, direction); + + // If the next node to be focused has been tagged as an editable + // node, send it a click event to trigger + if (next && next.ownerDocument === this.doc && next._editable) { + next.click(); + } + } + + this._clear(); + } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) { + // Cancel and blur ourselves. + prevent = true; + this.cancelled = true; + this._apply(); + this._clear(); + aEvent.stopPropagation(); + } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) { + // No need for leading spaces here. This is particularly + // noticable when adding a property: it's very natural to type + // <name>: (which advances to the next property) then spacebar. + prevent = !this.input.value; + } + + if (prevent) { + aEvent.preventDefault(); + } + }, + + /** + * Handle the input field's keyup event. + */ + _onKeyup: function(aEvent) { + // Validate the entered value. + this.warning.hidden = this.validate(this.input.value); + this._applied = false; + this._onBlur(null, true); + }, + + /** + * Handle changes to the input text. + */ + _onInput: function InplaceEditor_onInput(aEvent) + { + // Validate the entered value. + if (this.warning && this.validate) { + this.warning.hidden = this.validate(this.input.value); + } + + // Update size if we're autosizing. + if (this._measurement) { + this._updateSize(); + } + + // Call the user's change handler if available. + if (this.change) { + this.change(this.input.value.trim()); + } + } +}; + +/** + * Copy text-related styles from one element to another. + */ +function copyTextStyles(aFrom, aTo) +{ + let win = aFrom.ownerDocument.defaultView; + let style = win.getComputedStyle(aFrom); + aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText; + aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText; + aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText; + aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText; +} + +/** + * Trigger a focus change similar to pressing tab/shift-tab. + */ +function moveFocus(aWin, aDirection) +{ + return focusManager.moveFocus(aWin, null, aDirection, 0); +} + + +XPCOMUtils.defineLazyGetter(this, "focusManager", function() { + return Services.focus; +});
--- a/browser/devtools/styleinspector/CssRuleView.jsm +++ b/browser/devtools/styleinspector/CssRuleView.jsm @@ -8,39 +8,34 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; const HTML_NS = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD; -const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD; - /** * These regular expressions are adapted from firebug's css.js, and are * used to parse CSSStyleDeclaration's cssText attribute. */ // Used to split on css line separators const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g; // Used to parse a single property line. const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource:///modules/devtools/CssLogic.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/devtools/InplaceEditor.jsm"); this.EXPORTED_SYMBOLS = ["CssRuleView", - "_ElementStyle", - "editableItem", - "_editableField", - "_getInplaceEditorForSpan"]; + "_ElementStyle"]; /** * Our model looks like this: * * ElementStyle: * Responsible for keeping track of which properties are overridden. * Maintains a list of Rule objects that apply to the element. * Rule: @@ -1906,791 +1901,16 @@ TextPropertyEditor.prototype = { } finally { prefs.setBoolPref("layout.css.report_errors", prefVal); } return !!style.getPropertyValue(name); }, }; /** - * Mark a span editable. |editableField| will listen for the span to - * be focused and create an InlineEditor to handle text input. - * Changes will be committed when the InlineEditor's input is blurred - * or dropped when the user presses escape. - * - * @param {object} aOptions - * Options for the editable field, including: - * {Element} element: - * (required) The span to be edited on focus. - * {function} canEdit: - * Will be called before creating the inplace editor. Editor - * won't be created if canEdit returns false. - * {function} start: - * Will be called when the inplace editor is initialized. - * {function} change: - * Will be called when the text input changes. Will be called - * with the current value of the text input. - * {function} done: - * Called when input is committed or blurred. Called with - * current value and a boolean telling the caller whether to - * commit the change. This function is called before the editor - * has been torn down. - * {function} destroy: - * Called when the editor is destroyed and has been torn down. - * {string} advanceChars: - * If any characters in advanceChars are typed, focus will advance - * to the next element. - * {boolean} stopOnReturn: - * If true, the return key will not advance the editor to the next - * focusable element. - * {string} trigger: The DOM event that should trigger editing, - * defaults to "click" - */ -function editableField(aOptions) -{ - return editableItem(aOptions, function(aElement, aEvent) { - new InplaceEditor(aOptions, aEvent); - }); -} - -/** - * Handle events for an element that should respond to - * clicks and sit in the editing tab order, and call - * a callback when it is activated. - * - * @param {object} aOptions - * The options for this editor, including: - * {Element} element: The DOM element. - * {string} trigger: The DOM event that should trigger editing, - * defaults to "click" - * @param {function} aCallback - * Called when the editor is activated. - */ -this.editableItem = function editableItem(aOptions, aCallback) -{ - let trigger = aOptions.trigger || "click" - let element = aOptions.element; - element.addEventListener(trigger, function(evt) { - let win = this.ownerDocument.defaultView; - let selection = win.getSelection(); - if (trigger != "click" || selection.isCollapsed) { - aCallback(element, evt); - } - evt.stopPropagation(); - }, false); - - // If focused by means other than a click, start editing by - // pressing enter or space. - element.addEventListener("keypress", function(evt) { - if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN || - evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) { - aCallback(element); - } - }, true); - - // Ugly workaround - the element is focused on mousedown but - // the editor is activated on click/mouseup. This leads - // to an ugly flash of the focus ring before showing the editor. - // So hide the focus ring while the mouse is down. - element.addEventListener("mousedown", function(evt) { - let cleanup = function() { - element.style.removeProperty("outline-style"); - element.removeEventListener("mouseup", cleanup, false); - element.removeEventListener("mouseout", cleanup, false); - }; - element.style.setProperty("outline-style", "none"); - element.addEventListener("mouseup", cleanup, false); - element.addEventListener("mouseout", cleanup, false); - }, false); - - // Mark the element editable field for tab - // navigation while editing. - element._editable = true; -} - -this._editableField = editableField; - -function InplaceEditor(aOptions, aEvent) -{ - this.elt = aOptions.element; - let doc = this.elt.ownerDocument; - this.doc = doc; - this.elt.inplaceEditor = this; - - this.change = aOptions.change; - this.done = aOptions.done; - this.destroy = aOptions.destroy; - this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent; - this.multiline = aOptions.multiline || false; - this.stopOnReturn = !!aOptions.stopOnReturn; - - this._onBlur = this._onBlur.bind(this); - this._onKeyPress = this._onKeyPress.bind(this); - this._onInput = this._onInput.bind(this); - this._onKeyup = this._onKeyup.bind(this); - - this._createInput(); - this._autosize(); - - // Pull out character codes for advanceChars, listing the - // characters that should trigger a blur. - this._advanceCharCodes = {}; - let advanceChars = aOptions.advanceChars || ''; - for (let i = 0; i < advanceChars.length; i++) { - this._advanceCharCodes[advanceChars.charCodeAt(i)] = true; - } - - // Hide the provided element and add our editor. - this.originalDisplay = this.elt.style.display; - this.elt.style.display = "none"; - this.elt.parentNode.insertBefore(this.input, this.elt); - - if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) { - this.input.select(); - } - this.input.focus(); - - this.input.addEventListener("blur", this._onBlur, false); - this.input.addEventListener("keypress", this._onKeyPress, false); - this.input.addEventListener("input", this._onInput, false); - this.input.addEventListener("mousedown", function(aEvt) { aEvt.stopPropagation(); }, false); - - this.warning = aOptions.warning; - this.validate = aOptions.validate; - - if (this.warning && this.validate) { - this.input.addEventListener("keyup", this._onKeyup, false); - } - - if (aOptions.start) { - aOptions.start(this, aEvent); - } -} - -InplaceEditor.prototype = { - _createInput: function InplaceEditor_createEditor() - { - this.input = this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input"); - this.input.inplaceEditor = this; - this.input.classList.add("styleinspector-propertyeditor"); - this.input.value = this.initial; - - copyTextStyles(this.elt, this.input); - }, - - /** - * Get rid of the editor. - */ - _clear: function InplaceEditor_clear() - { - if (!this.input) { - // Already cleared. - return; - } - - this.input.removeEventListener("blur", this._onBlur, false); - this.input.removeEventListener("keypress", this._onKeyPress, false); - this.input.removeEventListener("keyup", this._onKeyup, false); - this.input.removeEventListener("oninput", this._onInput, false); - this._stopAutosize(); - - this.elt.style.display = this.originalDisplay; - this.elt.focus(); - - if (this.destroy) { - this.destroy(); - } - - this.elt.parentNode.removeChild(this.input); - this.input = null; - - delete this.elt.inplaceEditor; - delete this.elt; - }, - - /** - * Keeps the editor close to the size of its input string. This is pretty - * crappy, suggestions for improvement welcome. - */ - _autosize: function InplaceEditor_autosize() - { - // Create a hidden, absolutely-positioned span to measure the text - // in the input. Boo. - - // We can't just measure the original element because a) we don't - // change the underlying element's text ourselves (we leave that - // up to the client), and b) without tweaking the style of the - // original element, it might wrap differently or something. - this._measurement = this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span"); - this._measurement.className = "autosizer"; - this.elt.parentNode.appendChild(this._measurement); - let style = this._measurement.style; - style.visibility = "hidden"; - style.position = "absolute"; - style.top = "0"; - style.left = "0"; - copyTextStyles(this.input, this._measurement); - this._updateSize(); - }, - - /** - * Clean up the mess created by _autosize(). - */ - _stopAutosize: function InplaceEditor_stopAutosize() - { - if (!this._measurement) { - return; - } - this._measurement.parentNode.removeChild(this._measurement); - delete this._measurement; - }, - - /** - * Size the editor to fit its current contents. - */ - _updateSize: function InplaceEditor_updateSize() - { - // Replace spaces with non-breaking spaces. Otherwise setting - // the span's textContent will collapse spaces and the measurement - // will be wrong. - this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0'); - - // We add a bit of padding to the end. Should be enough to fit - // any letter that could be typed, otherwise we'll scroll before - // we get a chance to resize. Yuck. - let width = this._measurement.offsetWidth + 10; - - if (this.multiline) { - // Make sure there's some content in the current line. This is a hack to account - // for the fact that after adding a newline the <pre> doesn't grow unless there's - // text content on the line. - width += 15; - this._measurement.textContent += "M"; - this.input.style.height = this._measurement.offsetHeight + "px"; - } - - this.input.style.width = width + "px"; - }, - - /** - * Increment property values in rule view. - * - * @param {number} increment - * The amount to increase/decrease the property value. - * @return {bool} true if value has been incremented. - */ - _incrementValue: function InplaceEditor_incrementValue(increment) - { - let value = this.input.value; - let selectionStart = this.input.selectionStart; - let selectionEnd = this.input.selectionEnd; - - let newValue = this._incrementCSSValue(value, increment, selectionStart, selectionEnd); - - if (!newValue) { - return false; - } - - this.input.value = newValue.value; - this.input.setSelectionRange(newValue.start, newValue.end); - - return true; - }, - - /** - * Increment the property value based on the property type. - * - * @param {string} value - * Property value. - * @param {number} increment - * Amount to increase/decrease the property value. - * @param {number} selStart - * Starting index of the value. - * @param {number} selEnd - * Ending index of the value. - * @return {object} object with properties 'value', 'start', and 'end'. - */ - _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment, selStart, - selEnd) - { - let range = this._parseCSSValue(value, selStart); - let type = (range && range.type) || ""; - let rawValue = (range ? value.substring(range.start, range.end) : ""); - let incrementedValue = null, selection; - - if (type === "num") { - let newValue = this._incrementRawValue(rawValue, increment); - if (newValue !== null) { - incrementedValue = newValue; - selection = [0, incrementedValue.length]; - } - } else if (type === "hex") { - let exprOffset = selStart - range.start; - let exprOffsetEnd = selEnd - range.start; - let newValue = this._incHexColor(rawValue, increment, exprOffset, exprOffsetEnd); - if (newValue) { - incrementedValue = newValue.value; - selection = newValue.selection; - } - } else { - let info; - if (type === "rgb" || type === "hsl") { - info = {}; - let part = value.substring(range.start, selStart).split(",").length - 1; - if (part === 3) { // alpha - info.minValue = 0; - info.maxValue = 1; - } else if (type === "rgb") { - info.minValue = 0; - info.maxValue = 255; - } else if (part !== 0) { // hsl percentage - info.minValue = 0; - info.maxValue = 100; - - // select the previous number if the selection is at the end of a percentage sign - if (value.charAt(selStart - 1) === "%") { - --selStart; - } - } - } - return this._incrementGenericValue(value, increment, selStart, selEnd, info); - } - - if (incrementedValue === null) { - return; - } - - let preRawValue = value.substr(0, range.start); - let postRawValue = value.substr(range.end); - - return { - value: preRawValue + incrementedValue + postRawValue, - start: range.start + selection[0], - end: range.start + selection[1] - }; - }, - - /** - * Parses the property value and type. - * - * @param {string} value - * Property value. - * @param {number} offset - * Starting index of value. - * @return {object} object with properties 'value', 'start', 'end', and 'type'. - */ - _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset) - { - const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/; - let start = 0; - let m; - - // retreive values from left to right until we find the one at our offset - while ((m = reSplitCSS.exec(value)) && - (m.index + m[0].length < offset)) { - value = value.substr(m.index + m[0].length); - start += m.index + m[0].length; - offset -= m.index + m[0].length; - } - - if (!m) { - return; - } - - let type; - if (m[1]) { - type = "url"; - } else if (m[2]) { - type = "rgb"; - } else if (m[3]) { - type = "hsl"; - } else if (m[4]) { - type = "hex"; - } else if (m[5]) { - type = "num"; - } - - return { - value: m[0], - start: start + m.index, - end: start + m.index + m[0].length, - type: type - }; - }, - - /** - * Increment the property value for types other than - * number or hex, such as rgb, hsl, and file names. - * - * @param {string} value - * Property value. - * @param {number} increment - * Amount to increment/decrement. - * @param {number} offset - * Starting index of the property value. - * @param {number} offsetEnd - * Ending index of the property value. - * @param {object} info - * Object with details about the property value. - * @return {object} object with properties 'value', 'start', and 'end'. - */ - _incrementGenericValue: function InplaceEditor_incrementGenericValue(value, increment, offset, - offsetEnd, info) - { - // Try to find a number around the cursor to increment. - let start, end; - // Check if we are incrementing in a non-number context (such as a URL) - if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) && - !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) { - // We have a number selected, possibly with a suffix, and we are not in - // the disallowed case of just part of a known number being selected. - // Use that number. - start = offset; - end = offsetEnd; - } else { - // Parse periods as belonging to the number only if we are in a known number - // context. (This makes incrementing the 1 in 'image1.gif' work.) - let pattern = "[" + (info ? "0-9." : "0-9") + "]*"; - let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length; - let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length; - - start = offset - before; - end = offset + after; - - // Expand the number to contain an initial minus sign if it seems - // free-standing. - if (value.charAt(start - 1) === "-" && - (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) { - --start; - } - } - - if (start !== end) - { - // Include percentages as part of the incremented number (they are - // common enough). - if (value.charAt(end) === "%") { - ++end; - } - - let first = value.substr(0, start); - let mid = value.substring(start, end); - let last = value.substr(end); - - mid = this._incrementRawValue(mid, increment, info); - - if (mid !== null) { - return { - value: first + mid + last, - start: start, - end: start + mid.length - }; - } - } - }, - - /** - * Increment the property value for numbers. - * - * @param {string} rawValue - * Raw value to increment. - * @param {number} increment - * Amount to increase/decrease the raw value. - * @param {object} info - * Object with info about the property value. - * @return {string} the incremented value. - */ - _incrementRawValue: function InplaceEditor_incrementRawValue(rawValue, increment, info) - { - let num = parseFloat(rawValue); - - if (isNaN(num)) { - return null; - } - - let number = /\d+(\.\d+)?/.exec(rawValue); - let units = rawValue.substr(number.index + number[0].length); - - // avoid rounding errors - let newValue = Math.round((num + increment) * 1000) / 1000; - - if (info && "minValue" in info) { - newValue = Math.max(newValue, info.minValue); - } - if (info && "maxValue" in info) { - newValue = Math.min(newValue, info.maxValue); - } - - newValue = newValue.toString(); - - return newValue + units; - }, - - /** - * Increment the property value for hex. - * - * @param {string} value - * Property value. - * @param {number} increment - * Amount to increase/decrease the property value. - * @param {number} offset - * Starting index of the property value. - * @param {number} offsetEnd - * Ending index of the property value. - * @return {object} object with properties 'value' and 'selection'. - */ - _incHexColor: function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd) - { - // Return early if no part of the rawValue is selected. - if (offsetEnd > rawValue.length && offset >= rawValue.length) { - return; - } - if (offset < 1 && offsetEnd <= 1) { - return; - } - // Ignore the leading #. - rawValue = rawValue.substr(1); - --offset; - --offsetEnd; - - // Clamp the selection to within the actual value. - offset = Math.max(offset, 0); - offsetEnd = Math.min(offsetEnd, rawValue.length); - offsetEnd = Math.max(offsetEnd, offset); - - // Normalize #ABC -> #AABBCC. - if (rawValue.length === 3) { - rawValue = rawValue.charAt(0) + rawValue.charAt(0) + - rawValue.charAt(1) + rawValue.charAt(1) + - rawValue.charAt(2) + rawValue.charAt(2); - offset *= 2; - offsetEnd *= 2; - } - - if (rawValue.length !== 6) { - return; - } - - // If no selection, increment an adjacent color, preferably one to the left. - if (offset === offsetEnd) { - if (offset === 0) { - offsetEnd = 1; - } else { - offset = offsetEnd - 1; - } - } - - // Make the selection cover entire parts. - offset -= offset % 2; - offsetEnd += offsetEnd % 2; - - // Remap the increments from [0.1, 1, 10] to [1, 1, 16]. - if (-1 < increment && increment < 1) { - increment = (increment < 0 ? -1 : 1); - } - if (Math.abs(increment) === 10) { - increment = (increment < 0 ? -16 : 16); - } - - let isUpper = (rawValue.toUpperCase() === rawValue); - - for (let pos = offset; pos < offsetEnd; pos += 2) { - // Increment the part in [pos, pos+2). - let mid = rawValue.substr(pos, 2); - let value = parseInt(mid, 16); - - if (isNaN(value)) { - return; - } - - mid = Math.min(Math.max(value + increment, 0), 255).toString(16); - - while (mid.length < 2) { - mid = "0" + mid; - } - if (isUpper) { - mid = mid.toUpperCase(); - } - - rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2); - } - - return { - value: "#" + rawValue, - selection: [offset + 1, offsetEnd + 1] - }; - }, - - /** - * Call the client's done handler and clear out. - */ - _apply: function InplaceEditor_apply(aEvent) - { - if (this._applied) { - return; - } - - this._applied = true; - - if (this.done) { - let val = this.input.value.trim(); - return this.done(this.cancelled ? this.initial : val, !this.cancelled); - } - - return null; - }, - - /** - * Handle loss of focus by calling done if it hasn't been called yet. - */ - _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear) - { - this._apply(); - if (!aDoNotClear) { - this._clear(); - } - }, - - /** - * Handle the input field's keypress event. - */ - _onKeyPress: function InplaceEditor_onKeyPress(aEvent) - { - let prevent = false; - - const largeIncrement = 100; - const mediumIncrement = 10; - const smallIncrement = 0.1; - - let increment = 0; - - if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP - || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) { - increment = 1; - } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN - || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) { - increment = -1; - } - - if (aEvent.shiftKey && !aEvent.altKey) { - if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP - || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) { - increment *= largeIncrement; - } else { - increment *= mediumIncrement; - } - } else if (aEvent.altKey && !aEvent.shiftKey) { - increment *= smallIncrement; - } - - if (increment && this._incrementValue(increment) ) { - this._updateSize(); - prevent = true; - } - - if (this.multiline && - aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN && - aEvent.shiftKey) { - prevent = false; - } else if (aEvent.charCode in this._advanceCharCodes - || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN - || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) { - prevent = true; - - let direction = FOCUS_FORWARD; - if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB && - aEvent.shiftKey) { - this.cancelled = true; - direction = FOCUS_BACKWARD; - } - if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) { - direction = null; - } - - let input = this.input; - - this._apply(); - - let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); - if (direction !== null && fm.focusedElement === input) { - // If the focused element wasn't changed by the done callback, - // move the focus as requested. - let next = moveFocus(this.doc.defaultView, direction); - - // If the next node to be focused has been tagged as an editable - // node, send it a click event to trigger - if (next && next.ownerDocument === this.doc && next._editable) { - next.click(); - } - } - - this._clear(); - } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) { - // Cancel and blur ourselves. - prevent = true; - this.cancelled = true; - this._apply(); - this._clear(); - aEvent.stopPropagation(); - } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) { - // No need for leading spaces here. This is particularly - // noticable when adding a property: it's very natural to type - // <name>: (which advances to the next property) then spacebar. - prevent = !this.input.value; - } - - if (prevent) { - aEvent.preventDefault(); - } - }, - - /** - * Handle the input field's keyup event. - */ - _onKeyup: function(aEvent) { - // Validate the entered value. - this.warning.hidden = this.validate(this.input.value); - this._applied = false; - this._onBlur(null, true); - }, - - /** - * Handle changes to the input text. - */ - _onInput: function InplaceEditor_onInput(aEvent) - { - // Validate the entered value. - if (this.warning && this.validate) { - this.warning.hidden = this.validate(this.input.value); - } - - // Update size if we're autosizing. - if (this._measurement) { - this._updateSize(); - } - - // Call the user's change handler if available. - if (this.change) { - this.change(this.input.value.trim()); - } - } -}; - -/* - * Various API consumers (especially tests) sometimes want to grab the - * inplaceEditor expando off span elements. However, when each global has its - * own compartment, those expandos live on Xray wrappers that are only visible - * within this JSM. So we provide a little workaround here. - */ -this._getInplaceEditorForSpan = function _getInplaceEditorForSpan(aSpan) -{ - return aSpan.inplaceEditor; -}; - -/** * Store of CSSStyleDeclarations mapped to properties that have been changed by * the user. */ function UserProperties() { // FIXME: This should be a WeakMap once bug 753517 is fixed. // See Bug 777373 for details. this.map = new Map(); @@ -2811,38 +2031,16 @@ function createMenuItem(aMenu, aAttribut /** * Append a text node to an element. */ function appendText(aParent, aText) { aParent.appendChild(aParent.ownerDocument.createTextNode(aText)); } -/** - * Copy text-related styles from one element to another. - */ -function copyTextStyles(aFrom, aTo) -{ - let win = aFrom.ownerDocument.defaultView; - let style = win.getComputedStyle(aFrom); - aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText; - aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText; - aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText; - aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText; -} - -/** - * Trigger a focus change similar to pressing tab/shift-tab. - */ -function moveFocus(aWin, aDirection) -{ - let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); - return fm.moveFocus(aWin, null, aDirection, 0); -} - XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() { return Cc["@mozilla.org/widget/clipboardhelper;1"]. getService(Ci.nsIClipboardHelper); }); XPCOMUtils.defineLazyGetter(this, "_strings", function() { return Services.strings.createBundle( "chrome://browser/locale/devtools/styleinspector.properties");
--- a/browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js +++ b/browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js @@ -4,18 +4,16 @@ // Test that increasing/decreasing values in rule view using // arrow keys works correctly. let tempScope = {}; Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); let CssRuleView = tempScope.CssRuleView; let _ElementStyle = tempScope._ElementStyle; -let _editableField = tempScope._editableField; -let inplaceEditor = tempScope._getInplaceEditorForSpan; let doc; let ruleDialog; let ruleView; function setUpTests() { doc.body.innerHTML = '<div id="test" style="' +
--- a/browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js @@ -1,16 +1,13 @@ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let doc; -let tempScope = {}; -Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); -let inplaceEditor = tempScope._getInplaceEditorForSpan; let inspector; let win; XPCOMUtils.defineLazyGetter(this, "osString", function() { return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; }); function createDocument()
--- a/browser/devtools/styleinspector/test/browser_ruleview_editor.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_editor.js @@ -1,18 +1,16 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let tempScope = {} Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); let CssRuleView = tempScope.CssRuleView; let _ElementStyle = tempScope._ElementStyle; -let _editableField = tempScope._editableField; -let inplaceEditor = tempScope._getInplaceEditorForSpan; let doc = content.document; function expectDone(aValue, aCommit, aNext) { return function(aDoneValue, aDoneCommit) { dump("aDoneValue: " + aDoneValue + " commit: " + aDoneCommit + "\n"); @@ -35,50 +33,50 @@ function createSpan() doc.body.appendChild(span); return span; } function testReturnCommit() { clearBody(); let span = createSpan(); - _editableField({ + editableField({ element: span, initial: "explicit initial", start: function() { is(inplaceEditor(span).input.value, "explicit initial", "Explicit initial value should be used."); inplaceEditor(span).input.value = "Test Value"; EventUtils.sendKey("return"); }, done: expectDone("Test Value", true, testBlurCommit) }); span.click(); } function testBlurCommit() { clearBody(); let span = createSpan(); - _editableField({ + editableField({ element: span, start: function() { is(inplaceEditor(span).input.value, "Edit Me!", "textContent of the span used."); inplaceEditor(span).input.value = "Test Value"; inplaceEditor(span).input.blur(); }, done: expectDone("Test Value", true, testAdvanceCharCommit) }); span.click(); } function testAdvanceCharCommit() { clearBody(); let span = createSpan(); - _editableField({ + editableField({ element: span, advanceChars: ":", start: function() { let input = inplaceEditor(span).input; for each (let ch in "Test:") { EventUtils.sendChar(ch); } }, @@ -86,17 +84,17 @@ function testAdvanceCharCommit() }); span.click(); } function testEscapeCancel() { clearBody(); let span = createSpan(); - _editableField({ + editableField({ element: span, initial: "initial text", start: function() { inplaceEditor(span).input.value = "Test Value"; EventUtils.sendKey("escape"); }, done: expectDone("initial text", false, finishTest) });
--- a/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js @@ -1,18 +1,16 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let tempScope = {}; Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); let CssRuleView = tempScope.CssRuleView; let _ElementStyle = tempScope._ElementStyle; -let _editableField = tempScope._editableField; -let inplaceEditor = tempScope._getInplaceEditorForSpan; let doc; let ruleDialog; let ruleView; var gRuleViewChanged = false; function ruleViewChanged() { @@ -52,30 +50,28 @@ function startTest() waitForFocus(testCancelNew, ruleDialog); }, true); } function testCancelNew() { // Start at the beginning: start to add a rule to the element's style // declaration, but leave it empty. - let elementRuleEditor = ruleView.element.children[0]._ruleEditor; waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) { is(inplaceEditor(elementRuleEditor.newPropSpan), aEditor, "Next focused editor should be the new property editor."); let input = aEditor.input; waitForEditorBlur(aEditor, function () { ok(!gRuleViewChanged, "Shouldn't get a change event after a cancel."); is(elementRuleEditor.rule.textProps.length, 0, "Should have canceled creating a new text property."); ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties."); testCreateNew(); }); aEditor.input.blur(); }); - EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1, { }, ruleDialog); } function testCreateNew() { // Create a new property.
--- a/browser/devtools/styleinspector/test/browser_ruleview_focus.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_focus.js @@ -1,18 +1,15 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ // Test that focus doesn't leave the style editor when adding a property // (bug 719916) -let tempScope = {}; -Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); -let inplaceEditor = tempScope._getInplaceEditorForSpan; let doc; let inspector; let stylePanel; function openRuleView() { var target = TargetFactory.forTab(gBrowser.selectedTab); gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
--- a/browser/devtools/styleinspector/test/browser_ruleview_inherit.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_inherit.js @@ -1,17 +1,16 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let tempScope = {} Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); let CssRuleView = tempScope.CssRuleView; let _ElementStyle = tempScope._ElementStyle; -let _editableField = tempScope._editableField; let doc; function simpleInherit() { let style = '' + '#test2 {' + ' background-color: green;' +
--- a/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js @@ -1,17 +1,16 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let tempScope = {} Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); let CssRuleView = tempScope.CssRuleView; let _ElementStyle = tempScope._ElementStyle; -let _editableField = tempScope._editableField; let doc; function simpleOverride() { doc.body.innerHTML = '<div id="testid">Styled Node</div>'; let element = doc.getElementById("testid"); let elementStyle = new _ElementStyle(element);
--- a/browser/devtools/styleinspector/test/browser_ruleview_override.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_override.js @@ -1,17 +1,16 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let tempScope = {} Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); let CssRuleView = tempScope.CssRuleView; let _ElementStyle = tempScope._ElementStyle; -let _editableField = tempScope._editableField; let doc; function simpleOverride() { let style = '' + '#testid {' + ' background-color: blue;' +
--- a/browser/devtools/styleinspector/test/browser_ruleview_ui.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_ui.js @@ -1,18 +1,16 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let tempScope = {} Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); let CssRuleView = tempScope.CssRuleView; let _ElementStyle = tempScope._ElementStyle; -let _editableField = tempScope._editableField; -let inplaceEditor = tempScope._getInplaceEditorForSpan; let doc; let ruleDialog; let ruleView; var gRuleViewChanged = false; function ruleViewChanged() {
--- a/browser/devtools/styleinspector/test/browser_ruleview_update.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_update.js @@ -1,18 +1,16 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let tempScope = {} Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope); let CssRuleView = tempScope.CssRuleView; let _ElementStyle = tempScope._ElementStyle; -let _editableField = tempScope._editableField; -let inplaceEditor = tempScope._getInplaceEditorForSpan; let doc; let ruleDialog; let ruleView; let testElement; function startTest() {
--- a/browser/devtools/styleinspector/test/head.js +++ b/browser/devtools/styleinspector/test/head.js @@ -8,16 +8,20 @@ Cu.import("resource:///modules/devtools/ Cu.import("resource:///modules/devtools/CssHtmlTree.jsm", tempScope); Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope); let ConsoleUtils = tempScope.ConsoleUtils; let CssLogic = tempScope.CssLogic; let CssHtmlTree = tempScope.CssHtmlTree; let gDevTools = tempScope.gDevTools; Cu.import("resource:///modules/devtools/Target.jsm", tempScope); let TargetFactory = tempScope.TargetFactory; +let { + editableField, + getInplaceEditorForSpan: inplaceEditor +} = Cu.import("resource:///modules/devtools/InplaceEditor.jsm", {}); Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope); let console = tempScope.console; let browser, hudId, hud, hudBox, filterBox, outputNode, cs; function addTab(aURL) { gBrowser.selectedTab = gBrowser.addTab();