Bug 808371 - Allow adding new properties to objects in the VariablesView. r=vp, r=jryans

This commit is contained in:
Brandon Benvie 2013-12-02 20:07:13 -08:00
parent d8d91820da
commit b04c4c4053
16 changed files with 588 additions and 267 deletions

View File

@ -16,6 +16,7 @@ function ManifestEditor(project) {
this._onEval = this._onEval.bind(this);
this._onSwitch = this._onSwitch.bind(this);
this._onDelete = this._onDelete.bind(this);
this._onNew = this._onNew.bind(this);
}
ManifestEditor.prototype = {
@ -54,6 +55,7 @@ ManifestEditor.prototype = {
editor.eval = this._onEval;
editor.switch = this._onSwitch;
editor.delete = this._onDelete;
editor.new = this._onNew;
}
return this.update();
@ -87,6 +89,14 @@ ManifestEditor.prototype = {
eval(evalString);
},
_onNew: function(variable, newName, newValue) {
let manifest = this.manifest;
let symbolicName = variable.symbolicName + "['" + newName + "']";
let evalString = "manifest" + symbolicName + " = " + newValue + ";";
eval(evalString);
this.update();
},
update: function() {
this.editor.createHierarchy();
this.editor.rawObject = this.manifest;

View File

@ -6,6 +6,9 @@ const {Services} = Cu.import("resource://gre/modules/Services.jsm");
const MANIFEST_EDITOR_ENABLED = "devtools.appmanager.manifestEditor.enabled";
let gManifestWindow, gManifestEditor;
function test() {
waitForExplicitFinish();
@ -15,7 +18,14 @@ function test() {
yield selectProjectsPanel();
yield addSamplePackagedApp();
yield showSampleProjectDetails();
gManifestWindow = getManifestWindow();
gManifestEditor = getProjectsWindow().UI.manifestEditor;
yield changeManifestValue("name", "the best app");
yield addNewManifestProperty("developer", "foo", "bar");
gManifestWindow = null;
gManifestEditor = null;
yield removeSamplePackagedApp();
yield removeTab(tab);
Services.prefs.setBoolPref(MANIFEST_EDITOR_ENABLED, false);
@ -23,32 +33,73 @@ function test() {
});
}
// Wait until the animation from commitHierarchy has completed
function waitForUpdate() {
return waitForTime(gManifestEditor.editor.lazyEmptyDelay + 1);
}
function changeManifestValue(key, value) {
return Task.spawn(function() {
let manifestWindow = getManifestWindow();
let manifestEditor = getProjectsWindow().UI.manifestEditor;
let propElem = manifestWindow.document
let propElem = gManifestWindow.document
.querySelector("[id ^= '" + key + "']");
is(propElem.querySelector(".name").value, key,
"Key doesn't match expected value");
let valueElem = propElem.querySelector(".value");
EventUtils.sendMouseEvent({ type: "mousedown" }, valueElem, manifestWindow);
EventUtils.sendMouseEvent({ type: "mousedown" }, valueElem, gManifestWindow);
let valueInput = propElem.querySelector(".element-value-input");
valueInput.value = '"' + value + '"';
EventUtils.sendKey("RETURN", manifestWindow);
EventUtils.sendKey("RETURN", gManifestWindow);
// Wait until the animation from commitHierarchy has completed
yield waitForTime(manifestEditor.editor.lazyEmptyDelay + 1);
yield waitForUpdate();
// Elements have all been replaced, re-select them
propElem = manifestWindow.document.querySelector("[id ^= '" + key + "']");
propElem = gManifestWindow.document.querySelector("[id ^= '" + key + "']");
valueElem = propElem.querySelector(".value");
is(valueElem.value, '"' + value + '"',
"Value doesn't match expected value");
is(manifestEditor.manifest[key], value,
is(gManifestEditor.manifest[key], value,
"Manifest doesn't contain expected value");
});
}
function addNewManifestProperty(parent, key, value) {
return Task.spawn(function() {
let parentElem = gManifestWindow.document
.querySelector("[id ^= '" + parent + "']");
ok(parentElem,
"Found parent element");
let addPropertyElem = parentElem
.querySelector(".variables-view-add-property");
ok(addPropertyElem,
"Found add-property button");
EventUtils.sendMouseEvent({ type: "mousedown" }, addPropertyElem, gManifestWindow);
let nameInput = parentElem.querySelector(".element-name-input");
nameInput.value = key;
EventUtils.sendKey("TAB", gManifestWindow);
let valueInput = parentElem.querySelector(".element-value-input");
valueInput.value = '"' + value + '"';
EventUtils.sendKey("RETURN", gManifestWindow);
yield waitForUpdate();
let newElem = gManifestWindow.document.querySelector("[id ^= '" + key + "']");
let nameElem = newElem.querySelector(".name");
is(nameElem.value, key,
"Key doesn't match expected Key");
ok(key in gManifestEditor.manifest[parent],
"Manifest doesn't contain expected key");
let valueElem = newElem.querySelector(".value");
is(valueElem.value, '"' + value + '"',
"Value doesn't match expected value");
is(gManifestEditor.manifest[parent][key], value,
"Manifest doesn't contain expected value");
});
}

View File

@ -1,3 +1,6 @@
{
"name": "My packaged app"
"name": "My packaged app",
"developer": {
"name": "Foo Bar"
}
}

View File

@ -107,6 +107,44 @@ function test() {
is(testVar.get("someProp4").target.querySelector(".value").getAttribute("value"), "Object",
"The grip information for the variable wasn't set correctly (9).");
let parent = testVar.get("someProp2");
let child = parent.addItem("child", {
value: {
type: "null"
}
});
is(variables.getItemForNode(parent.target), parent,
"VariablesView should have a record of the parent.");
is(variables.getItemForNode(child.target), child,
"VariablesView should have a record of the child.");
is([...parent].length, 1,
"Parent should have one child.");
parent.remove();
is(variables.getItemForNode(parent.target), undefined,
"VariablesView should not have a record of the parent anymore.");
is(parent.target.parentNode, null,
"Parent element should not have a parent.")
is(variables.getItemForNode(child.target), undefined,
"VariablesView should not have a record of the child anymore.");
is(child.target.parentNode, null,
"Child element should not have a parent.")
is([...parent].length, 0,
"Parent should have zero children.");
testScope.remove();
is([...variables].length, 0,
"VariablesView should have been emptied.");
is(Cu.nondeterministicGetWeakMapKeys(variables._itemsByElement).length, 0,
"VariablesView _itemsByElement map has been emptied.");
is(variables._currHierarchy.size, 0,
"VariablesView _currHierarchy map has been emptied.");
is(variables._list.children.length, 0,
"VariablesView element should have no children.");
closeDebuggerAndFinish(aPanel);
});
}

View File

@ -59,6 +59,7 @@ function performTest() {
gVariablesView.eval = function() {};
gVariablesView.switch = function() {};
gVariablesView.delete = function() {};
gVariablesView.new = function() {};
gVariablesView.rawObject = test;
testHierarchy();
@ -503,8 +504,8 @@ function testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, ba
}
function testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty) {
is(fooScope.preventDisableOnChage, gVariablesView.preventDisableOnChage,
"The preventDisableOnChage property should persist from the view to all scopes.");
is(fooScope.preventDisableOnChange, gVariablesView.preventDisableOnChange,
"The preventDisableOnChange property should persist from the view to all scopes.");
is(fooScope.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers,
"The preventDescriptorModifiers property should persist from the view to all scopes.");
is(fooScope.editableNameTooltip, gVariablesView.editableNameTooltip,
@ -525,13 +526,15 @@ function testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar,
"The switch property should persist from the view to all scopes.");
is(fooScope.delete, gVariablesView.delete,
"The delete property should persist from the view to all scopes.");
is(fooScope.new, gVariablesView.new,
"The new property should persist from the view to all scopes.");
isnot(fooScope.eval, fooScope.switch,
"The eval and switch functions got mixed up in the scope.");
isnot(fooScope.switch, fooScope.delete,
"The eval and switch functions got mixed up in the scope.");
is(barVar.preventDisableOnChage, gVariablesView.preventDisableOnChage,
"The preventDisableOnChage property should persist from the view to all variables.");
is(barVar.preventDisableOnChange, gVariablesView.preventDisableOnChange,
"The preventDisableOnChange property should persist from the view to all variables.");
is(barVar.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers,
"The preventDescriptorModifiers property should persist from the view to all variables.");
is(barVar.editableNameTooltip, gVariablesView.editableNameTooltip,
@ -552,13 +555,15 @@ function testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar,
"The switch property should persist from the view to all variables.");
is(barVar.delete, gVariablesView.delete,
"The delete property should persist from the view to all variables.");
is(barVar.new, gVariablesView.new,
"The new property should persist from the view to all variables.");
isnot(barVar.eval, barVar.switch,
"The eval and switch functions got mixed up in the variable.");
isnot(barVar.switch, barVar.delete,
"The eval and switch functions got mixed up in the variable.");
is(bazProperty.preventDisableOnChage, gVariablesView.preventDisableOnChage,
"The preventDisableOnChage property should persist from the view to all properties.");
is(bazProperty.preventDisableOnChange, gVariablesView.preventDisableOnChange,
"The preventDisableOnChange property should persist from the view to all properties.");
is(bazProperty.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers,
"The preventDescriptorModifiers property should persist from the view to all properties.");
is(bazProperty.editableNameTooltip, gVariablesView.editableNameTooltip,
@ -579,6 +584,8 @@ function testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar,
"The switch property should persist from the view to all properties.");
is(bazProperty.delete, gVariablesView.delete,
"The delete property should persist from the view to all properties.");
is(bazProperty.new, gVariablesView.new,
"The new property should persist from the view to all properties.");
isnot(bazProperty.eval, bazProperty.switch,
"The eval and switch functions got mixed up in the property.");
isnot(bazProperty.switch, bazProperty.delete,

View File

@ -52,7 +52,7 @@ const GENERIC_VARIABLES_VIEW_SETTINGS = {
searchEnabled: true,
editableValueTooltip: "",
editableNameTooltip: "",
preventDisableOnChage: true,
preventDisableOnChange: true,
preventDescriptorModifiers: true,
eval: () => {},
switch: () => {}

View File

@ -263,11 +263,20 @@ VariablesView.prototype = {
*/
delete: null,
/**
* Function called each time a property is added via user interaction. If
* null, then property additions are disabled.
*
* This property is applied recursively onto each scope in this view and
* affects only the child nodes when they're created.
*/
new: null,
/**
* Specifies if after an eval or switch operation, the variable or property
* which has been edited should be disabled.
*/
preventDisableOnChage: false,
preventDisableOnChange: false,
/**
* Specifies if, whenever a variable or property descriptor is available,
@ -825,6 +834,10 @@ VariablesView.prototype = {
item._onDelete(e);
}
return;
case e.DOM_VK_INSERT:
item._onAddProperty(e);
return;
}
},
@ -911,12 +924,22 @@ VariablesView.prototype = {
}
},
/**
* Gets if action buttons (like delete) should be placed at the beginning or
* end of a line.
* @return boolean
*/
get actionsFirst() {
return this._actionsFirst;
},
/**
* Sets if action buttons (like delete) should be placed at the beginning or
* end of a line.
* @param boolean aFlag
*/
set actionsFirst(aFlag) {
this._actionsFirst = aFlag;
if (aFlag) {
this._parent.setAttribute("actions-first", "");
} else {
@ -956,6 +979,8 @@ VariablesView.prototype = {
_currHierarchy: null,
_enumVisible: true,
_nonEnumVisible: true,
_alignedValues: false,
_actionsFirst: false,
_parent: null,
_list: null,
_searchboxNode: null,
@ -1152,7 +1177,8 @@ function Scope(aView, aName, aFlags = {}) {
this.eval = aView.eval;
this.switch = aView.switch;
this.delete = aView.delete;
this.preventDisableOnChage = aView.preventDisableOnChage;
this.new = aView.new;
this.preventDisableOnChange = aView.preventDisableOnChange;
this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
this.editableNameTooltip = aView.editableNameTooltip;
this.editableValueTooltip = aView.editableValueTooltip;
@ -1259,6 +1285,20 @@ Scope.prototype = {
}
},
/**
* Remove this Scope from its parent and remove all children recursively.
*/
remove: function() {
let view = this._variablesView;
view._store.splice(view._store.indexOf(this), 1);
view._itemsByElement.delete(this._target);
view._currHierarchy.delete(this._nameString);
this._target.remove();
for (let variable of this._store.values()) {
variable.remove();
}
},
/**
* Gets the variable in this container having the specified name.
*
@ -1683,7 +1723,6 @@ Scope.prototype = {
*/
_onClick: function(e) {
if (e.button != 0 ||
e.target == this._inputNode ||
e.target == this._editNode ||
e.target == this._deleteNode) {
return;
@ -2037,6 +2076,7 @@ Scope.prototype = {
eval: null,
switch: null,
delete: null,
new: null,
editableValueTooltip: "",
editableNameTooltip: "",
editButtonTooltip: "",
@ -2130,6 +2170,19 @@ Variable.prototype = Heritage.extend(Scope.prototype, {
return new Property(this, aName, aDescriptor);
},
/**
* Remove this Variable from its parent and remove all children recursively.
*/
remove: function() {
this.ownerView._store.delete(this._nameString);
this._variablesView._itemsByElement.delete(this._target);
this._variablesView._currHierarchy.delete(this._absoluteName);
this._target.remove();
for (let property of this._store.values()) {
property.remove();
}
},
/**
* Populates this variable to contain all the properties of an object.
*
@ -2313,14 +2366,11 @@ Variable.prototype = Heritage.extend(Scope.prototype, {
this._idString = generateId(this._nameString = aName);
this._displayScope(aName, "variables-view-variable variable-or-property");
// Don't allow displaying variable information there's no name available.
if (this._nameString) {
this._displayVariable();
this._customizeVariable();
this._prepareTooltips();
this._setAttributes();
this._addEventListeners();
}
this._displayVariable();
this._customizeVariable();
this._prepareTooltips();
this._setAttributes();
this._addEventListeners();
this._onInit(this.ownerView._store.size < LAZY_APPEND_BATCH);
},
@ -2421,12 +2471,25 @@ Variable.prototype = Heritage.extend(Scope.prototype, {
editNode.addEventListener("mousedown", this._onEdit.bind(this), false);
this._title.insertBefore(editNode, this._spacer);
}
if (ownerView.delete) {
let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
deleteNode.className = "plain variables-view-delete";
deleteNode.addEventListener("click", this._onDelete.bind(this), false);
this._title.appendChild(deleteNode);
}
let { actionsFirst } = this._variablesView;
if (ownerView.new || actionsFirst) {
let addPropertyNode = this._addPropertyNode = this.document.createElement("toolbarbutton");
addPropertyNode.className = "plain variables-view-add-property";
addPropertyNode.addEventListener("mousedown", this._onAddProperty.bind(this), false);
if (actionsFirst && VariablesView.isPrimitive(descriptor)) {
addPropertyNode.setAttribute("invisible", "");
}
this._title.appendChild(addPropertyNode);
}
if (ownerView.contextMenuId) {
this._title.setAttribute("context", ownerView.contextMenuId);
}
@ -2582,149 +2645,43 @@ Variable.prototype = Heritage.extend(Scope.prototype, {
this._title.addEventListener("mousedown", this._onClick, false);
},
/**
* Creates a textbox node in place of a label.
*
* @param nsIDOMNode aLabel
* The label to be replaced with a textbox.
* @param string aClassName
* The class to be applied to the textbox.
* @param object aCallbacks
* An object containing the onKeypress and onBlur callbacks.
*/
_activateInput: function(aLabel, aClassName, aCallbacks) {
let initialString = aLabel.getAttribute("value");
// Create a texbox input element which will be shown in the current
// element's specified label location.
let input = this.document.createElement("textbox");
input.className = "plain " + aClassName;
input.setAttribute("value", initialString);
if (!this._variablesView.alignedValues) {
input.setAttribute("flex", "1");
}
// Replace the specified label with a textbox input element.
aLabel.parentNode.replaceChild(input, aLabel);
this._variablesView.boxObject.ensureElementIsVisible(input);
input.select();
// When the value is a string (displayed as "value"), then we probably want
// to change it to another string in the textbox, so to avoid typing the ""
// again, tackle with the selection bounds just a bit.
if (aLabel.getAttribute("value").match(/^".+"$/)) {
input.selectionEnd--;
input.selectionStart++;
}
input.addEventListener("keypress", aCallbacks.onKeypress, false);
input.addEventListener("blur", aCallbacks.onBlur, false);
this._prevExpandable = this.twisty;
this._prevExpanded = this.expanded;
this.collapse();
this.hideArrow();
this._locked = true;
this._inputNode = input;
this._stopThrobber();
},
/**
* Removes the textbox node in place of a label.
*
* @param nsIDOMNode aLabel
* The label which was replaced with a textbox.
* @param object aCallbacks
* An object containing the onKeypress and onBlur callbacks.
*/
_deactivateInput: function(aLabel, aInput, aCallbacks) {
aInput.parentNode.replaceChild(aLabel, aInput);
this._variablesView.boxObject.scrollBy(-this._target.clientWidth, 0);
aInput.removeEventListener("keypress", aCallbacks.onKeypress, false);
aInput.removeEventListener("blur", aCallbacks.onBlur, false);
this._locked = false;
this.twisty = this._prevExpandable;
this.expanded = this._prevExpanded;
this._inputNode = null;
this._stopThrobber();
},
/**
* Makes this variable's name editable.
*/
_activateNameInput: function(e) {
if (e && e.button != 0) {
// Only allow left-click to trigger this event.
return;
}
if (!this.ownerView.switch) {
return;
}
if (e) {
e.preventDefault();
e.stopPropagation();
if (!this._variablesView.alignedValues) {
this._separatorLabel.hidden = true;
this._valueLabel.hidden = true;
}
this._onNameInputKeyPress = this._onNameInputKeyPress.bind(this);
this._deactivateNameInput = this._deactivateNameInput.bind(this);
this._activateInput(this._name, "element-name-input", {
onKeypress: this._onNameInputKeyPress,
onBlur: this._deactivateNameInput
});
this._separatorLabel.hidden = true;
this._valueLabel.hidden = true;
},
/**
* Deactivates this variable's editable name mode.
*/
_deactivateNameInput: function(e) {
this._deactivateInput(this._name, e.target, {
onKeypress: this._onNameInputKeyPress,
onBlur: this._deactivateNameInput
});
this._separatorLabel.hidden = false;
this._valueLabel.hidden = false;
EditableName.create(this, {
onSave: aKey => {
if (!this._variablesView.preventDisableOnChange) {
this._disable();
}
this.ownerView.switch(this, aKey);
},
onCleanup: () => {
if (!this._variablesView.alignedValues) {
this._separatorLabel.hidden = false;
this._valueLabel.hidden = false;
}
}
}, e);
},
/**
* Makes this variable's value editable.
*/
_activateValueInput: function(e) {
if (e && e.button != 0) {
// Only allow left-click to trigger this event.
return;
}
if (!this.ownerView.eval) {
return;
}
if (e) {
e.preventDefault();
e.stopPropagation();
}
this._onValueInputKeyPress = this._onValueInputKeyPress.bind(this);
this._deactivateValueInput = this._deactivateValueInput.bind(this);
this._activateInput(this._valueLabel, "element-value-input", {
onKeypress: this._onValueInputKeyPress,
onBlur: this._deactivateValueInput
});
},
/**
* Deactivates this variable's editable value mode.
*/
_deactivateValueInput: function(e) {
this._deactivateInput(this._valueLabel, e.target, {
onKeypress: this._onValueInputKeyPress,
onBlur: this._deactivateValueInput
});
EditableValue.create(this, {
onSave: aString => {
if (!this._variablesView.preventDisableOnChange) {
this._disable();
}
this.ownerView.eval(this.evaluationMacro(this, aString));
}
}, e);
},
/**
@ -2742,85 +2699,12 @@ Variable.prototype = Heritage.extend(Scope.prototype, {
this._nonenum.hidden = true;
},
/**
* Deactivates this variable's editable mode and callbacks the new name.
*/
_saveNameInput: function(e) {
let input = e.target;
let initialString = this._name.getAttribute("value");
let currentString = input.value.trim();
this._deactivateNameInput(e);
if (initialString != currentString) {
if (!this._variablesView.preventDisableOnChage) {
this._disable();
this._name.value = currentString;
}
this.ownerView.switch(this, currentString);
}
},
/**
* Deactivates this variable's editable mode and evaluates the new value.
*/
_saveValueInput: function(e) {
let input = e.target;
let initialString = this._valueLabel.getAttribute("value");
let currentString = input.value.trim();
this._deactivateValueInput(e);
if (initialString != currentString) {
if (!this._variablesView.preventDisableOnChage) {
this._disable();
}
this.ownerView.eval(this.evaluationMacro(this, currentString.trim()));
}
},
/**
* The current macro used to generate the string evaluated when performing
* a variable or property value change.
*/
evaluationMacro: VariablesView.simpleValueEvalMacro,
/**
* The key press listener for this variable's editable name textbox.
*/
_onNameInputKeyPress: function(e) {
e.stopPropagation();
switch(e.keyCode) {
case e.DOM_VK_RETURN:
case e.DOM_VK_ENTER:
this._saveNameInput(e);
this.focus();
return;
case e.DOM_VK_ESCAPE:
this._deactivateNameInput(e);
this.focus();
return;
}
},
/**
* The key press listener for this variable's editable value textbox.
*/
_onValueInputKeyPress: function(e) {
e.stopPropagation();
switch(e.keyCode) {
case e.DOM_VK_RETURN:
case e.DOM_VK_ENTER:
this._saveValueInput(e);
this.focus();
return;
case e.DOM_VK_ESCAPE:
this._deactivateValueInput(e);
this.focus();
return;
}
},
/**
* The click listener for the edit button.
*/
@ -2852,15 +2736,48 @@ Variable.prototype = Heritage.extend(Scope.prototype, {
}
},
/**
* The click listener for the add property button.
*/
_onAddProperty: function(e) {
if ("button" in e && e.button != 0) {
return;
}
e.preventDefault();
e.stopPropagation();
this.expanded = true;
let item = this.addItem(" ", {
value: undefined,
configurable: true,
enumerable: true,
writable: true
}, true);
// Force showing the separator.
item._separatorLabel.hidden = false;
EditableNameAndValue.create(item, {
onSave: ([aKey, aValue]) => {
if (!this._variablesView.preventDisableOnChange) {
this._disable();
}
this.ownerView.new(this, aKey, aValue);
}
}, e);
},
_symbolicName: "",
_absoluteName: "",
_initialDescriptor: null,
_separatorLabel: null,
_spacer: null,
_valueLabel: null,
_inputNode: null,
_editNode: null,
_deleteNode: null,
_addPropertyNode: null,
_tooltip: null,
_valueGrip: null,
_valueString: "",
@ -2895,18 +2812,15 @@ Property.prototype = Heritage.extend(Variable.prototype, {
* @param object aDescriptor
* The property's descriptor.
*/
_init: function(aName, aDescriptor) {
_init: function(aName = "", aDescriptor) {
this._idString = generateId(this._nameString = aName);
this._displayScope(aName, "variables-view-property variable-or-property");
// Don't allow displaying property information there's no name available.
if (this._nameString) {
this._displayVariable();
this._customizeVariable();
this._prepareTooltips();
this._setAttributes();
this._addEventListeners();
}
this._displayVariable();
this._customizeVariable();
this._prepareTooltips();
this._setAttributes();
this._addEventListeners();
this._onInit(this.ownerView._store.size < LAZY_APPEND_BATCH);
},
@ -3263,3 +3177,262 @@ let generateId = (function() {
return aName.toLowerCase().trim().replace(/\s+/g, "-") + (++count);
};
})();
/**
* An Editable encapsulates the UI of an edit box that overlays a label,
* allowing the user to edit the value.
*
* @param Variable aVariable
* The Variable or Property to make editable.
* @param object aOptions
* - onSave
* The callback to call with the value when editing is complete.
* - onCleanup
* The callback to call when the editable is removed for any reason.
*/
function Editable(aVariable, aOptions) {
this._variable = aVariable;
this._onSave = aOptions.onSave;
this._onCleanup = aOptions.onCleanup;
}
Editable.create = function(aVariable, aOptions, aEvent) {
let editable = new this(aVariable, aOptions);
editable.activate(aEvent);
return editable;
};
Editable.prototype = {
/**
* The class name for targeting this Editable type's label element. Overridden
* by inheriting classes.
*/
className: null,
/**
* Boolean indicating whether this Editable should activate. Overridden by
* inheriting classes.
*/
shouldActivate: null,
/**
* The label element for this Editable. Overridden by inheriting classes.
*/
label: null,
/**
* Activate this editable by replacing the input box it overlays and
* initialize the handlers.
*
* @param Event e [optional]
* Optionally, the Event object that was used to activate the Editable.
*/
activate: function(e) {
if (!this.shouldActivate) {
return;
}
let { label } = this;
let initialString = label.getAttribute("value");
if (e) {
e.preventDefault();
e.stopPropagation();
}
// Create a texbox input element which will be shown in the current
// element's specified label location.
let input = this._input = this._variable.document.createElement("textbox");
input.className = "plain " + this.className;
input.setAttribute("value", initialString);
if (!this._variable._variablesView.alignedValues) {
input.setAttribute("flex", "1");
}
// Replace the specified label with a textbox input element.
label.parentNode.replaceChild(input, label);
this._variable._variablesView.boxObject.ensureElementIsVisible(input);
input.select();
// When the value is a string (displayed as "value"), then we probably want
// to change it to another string in the textbox, so to avoid typing the ""
// again, tackle with the selection bounds just a bit.
if (initialString.match(/^".+"$/)) {
input.selectionEnd--;
input.selectionStart++;
}
this._onKeypress = this._onKeypress.bind(this);
this._onBlur = this._onBlur.bind(this);
input.addEventListener("keypress", this._onKeypress);
input.addEventListener("blur", this._onBlur);
this._prevExpandable = this._variable.twisty;
this._prevExpanded = this._variable.expanded;
this._variable.collapse();
this._variable.hideArrow();
this._variable.locked = true;
this._variable._stopThrobber();
},
/**
* Remove the input box and restore the Variable or Property to its previous
* state.
*/
deactivate: function() {
this._input.removeEventListener("keypress", this._onKeypress);
this._input.removeEventListener("blur", this.deactivate);
this._input.parentNode.replaceChild(this.label, this._input);
this._input = null;
let { boxObject } = this._variable._variablesView;
boxObject.scrollBy(-this._variable._target, 0);
this._variable.locked = false;
this._variable.twisty = this._prevExpandable;
this._variable.expanded = this._prevExpanded;
this._variable._stopThrobber();
},
/**
* Save the current value and deactivate the Editable.
*/
_save: function() {
let initial = this.label.getAttribute("value");
let current = this._input.value.trim();
this.deactivate();
if (initial != current) {
this._onSave(current);
}
},
/**
* Called when tab is pressed, allowing subclasses to link different
* behavior to tabbing if desired.
*/
_next: function() {
this._save();
},
/**
* Called when escape is pressed, indicating a cancelling of editing without
* saving.
*/
_reset: function() {
this.deactivate();
this._variable.focus();
},
/**
* Event handler for when the input loses focus.
*/
_onBlur: function() {
this.deactivate();
},
/**
* Event handler for when the input receives a key press.
*/
_onKeypress: function(e) {
e.stopPropagation();
switch (e.keyCode) {
case e.DOM_VK_TAB:
this._next();
break;
case e.DOM_VK_RETURN:
case e.DOM_VK_ENTER:
this._save();
break;
case e.DOM_VK_ESCAPE:
this._reset();
break;
}
},
};
/**
* An Editable specific to editing the name of a Variable or Property.
*/
function EditableName(aVariable, aOptions) {
Editable.call(this, aVariable, aOptions);
}
EditableName.create = Editable.create;
EditableName.prototype = Heritage.extend(Editable.prototype, {
className: "element-name-input",
get label() {
return this._variable._name;
},
get shouldActivate() {
return !!this._variable.ownerView.switch;
},
});
/**
* An Editable specific to editing the value of a Variable or Property.
*/
function EditableValue(aVariable, aOptions) {
Editable.call(this, aVariable, aOptions);
}
EditableValue.create = Editable.create;
EditableValue.prototype = Heritage.extend(Editable.prototype, {
className: "element-value-input",
get label() {
return this._variable._valueLabel;
},
get shouldActivate() {
return !!this._variable.ownerView.eval;
},
});
/**
* An Editable specific to editing the key and value of a new property.
*/
function EditableNameAndValue(aVariable, aOptions) {
EditableName.call(this, aVariable, aOptions);
}
EditableNameAndValue.create = Editable.create;
EditableNameAndValue.prototype = Heritage.extend(EditableName.prototype, {
_reset: function(e) {
// Hide the Varible or Property if the user presses escape.
this._variable.remove();
this.deactivate();
},
_next: function(e) {
// Override _next so as to set both key and value at the same time.
let key = this._input.value;
this.label.setAttribute("value", key);
let valueEditable = EditableValue.create(this._variable, {
onSave: aValue => {
this._onSave([key, aValue]);
},
onCleanup: () => {
this._onCleanup();
}
});
valueEditable._reset = () => {
this._variable.remove();
valueEditable.deactivate();
};
},
_save: function(e) {
// Both _save and _next activate the value edit box.
this._next(e);
}
});

View File

@ -63,10 +63,16 @@
display: none;
}
*:not(:hover) .variables-view-delete {
*:not(:hover) .variables-view-delete,
*:not(:hover) .variables-view-add-property {
visibility: hidden;
}
.variables-view-delete > .toolbarbutton-text,
.variables-view-add-property > .toolbarbutton-text {
display: none;
}
.variables-view-container[aligned-values] .title > [optional-visibility] {
display: none;
}

View File

@ -615,10 +615,15 @@
/* Actions first */
.variables-view-container[actions-first] .variables-view-delete {
.variables-view-container[actions-first] .variables-view-delete,
.variables-view-container[actions-first] .variables-view-add-property {
-moz-box-ordinal-group: 0;
}
.variables-view-container[actions-first] [invisible] {
visibility: hidden;
}
/* Variables and properties tooltips */
.variable-or-property > tooltip > label {
@ -654,10 +659,6 @@
-moz-image-region: rect(0,48px,16px,32px);
}
.variables-view-delete > .toolbarbutton-text {
display: none;
}
.variables-view-edit {
background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat;
width: 20px;

View File

@ -258,6 +258,7 @@ browser.jar:
skin/classic/browser/devtools/app-manager/error.svg (../shared/devtools/app-manager/images/error.svg)
skin/classic/browser/devtools/app-manager/plus.svg (../shared/devtools/app-manager/images/plus.svg)
skin/classic/browser/devtools/app-manager/remove.svg (../shared/devtools/app-manager/images/remove.svg)
skin/classic/browser/devtools/app-manager/add.svg (../shared/devtools/app-manager/images/add.svg)
skin/classic/browser/devtools/app-manager/index-icons.svg (../shared/devtools/app-manager/images/index-icons.svg)
skin/classic/browser/devtools/app-manager/rocket.svg (../shared/devtools/app-manager/images/rocket.svg)
skin/classic/browser/devtools/app-manager/noise.png (../shared/devtools/app-manager/images/noise.png)

View File

@ -609,10 +609,15 @@
/* Actions first */
.variables-view-container[actions-first] .variables-view-delete {
.variables-view-container[actions-first] .variables-view-delete,
.variables-view-container[actions-first] .variables-view-add-property {
-moz-box-ordinal-group: 0;
}
.variables-view-container[actions-first] [invisible] {
visibility: hidden;
}
/* Variables and properties tooltips */
.variable-or-property > tooltip > label {
@ -648,10 +653,6 @@
-moz-image-region: rect(0,48px,16px,32px);
}
.variables-view-delete > .toolbarbutton-text {
display: none;
}
.variables-view-edit {
background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat;
width: 20px;

View File

@ -360,6 +360,7 @@ browser.jar:
skin/classic/browser/devtools/app-manager/error.svg (../shared/devtools/app-manager/images/error.svg)
skin/classic/browser/devtools/app-manager/plus.svg (../shared/devtools/app-manager/images/plus.svg)
skin/classic/browser/devtools/app-manager/remove.svg (../shared/devtools/app-manager/images/remove.svg)
skin/classic/browser/devtools/app-manager/add.svg (../shared/devtools/app-manager/images/add.svg)
skin/classic/browser/devtools/app-manager/index-icons.svg (../shared/devtools/app-manager/images/index-icons.svg)
skin/classic/browser/devtools/app-manager/rocket.svg (../shared/devtools/app-manager/images/rocket.svg)
skin/classic/browser/devtools/app-manager/noise.png (../shared/devtools/app-manager/images/noise.png)

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="64px" height="64px" viewBox="0 0 64 64">
<path fill="#00B2F7" d="M32.336,3.894c-15.74,0-28.5,12.76-28.5,28.5s12.76,28.5,28.5,28.5s28.5-12.76,28.5-28.5
S48.076,3.894,32.336,3.894z M44.86,36.966h-7.823v7.62c0,2.582-2.12,4.702-4.702,4.702c-2.584,0-4.704-2.12-4.704-4.702v-7.62
h-7.817c-2.52,0-4.572-2.056-4.572-4.572s2.053-4.572,4.572-4.572h7.817v-7.62c0-2.582,2.12-4.702,4.704-4.702
c2.582,0,4.702,2.12,4.702,4.702v7.62h7.823c2.514,0,4.57,2.056,4.57,4.572S47.374,36.966,44.86,36.966z"/>
</svg>

After

Width:  |  Height:  |  Size: 934 B

View File

@ -6,7 +6,7 @@
.variables-view-container.manifest-editor {
background-color: #F5F5F5;
padding: 20px 13px;
padding: 20px 2px;
}
.manifest-editor .variable-or-property:focus > .title {
@ -19,7 +19,8 @@
color: #27406A;
}
.manifest-editor .variable-or-property > .title > label {
.manifest-editor .variable-or-property > .title > label,
.manifest-editor textbox {
font-family: monospace;
}
@ -52,16 +53,28 @@
.manifest-editor .variables-view-delete,
.manifest-editor .variables-view-delete:hover,
.manifest-editor .variables-view-delete:active {
.manifest-editor .variables-view-delete:active,
.manifest-editor .variables-view-add-property,
.manifest-editor .variables-view-add-property:hover,
.manifest-editor .variables-view-add-property:active {
list-style-image: none;
-moz-image-region: initial;
}
.manifest-editor .variables-view-delete::before {
width: 12px;
height: 12px;
.manifest-editor .variables-view-delete::before,
.manifest-editor .variables-view-add-property::before {
width: 11px;
height: 11px;
content: "";
display: inline-block;
background-image: url("app-manager/remove.svg");
background-size: 12px auto;
background-size: 11px auto;
}
.manifest-editor .variables-view-delete::before {
background-image: url("app-manager/remove.svg");
}
.manifest-editor .variables-view-add-property::before {
background-image: url("app-manager/add.svg");
-moz-margin-end: 2px;
}

View File

@ -612,10 +612,15 @@
/* Actions first */
.variables-view-container[actions-first] .variables-view-delete {
.variables-view-container[actions-first] .variables-view-delete,
.variables-view-container[actions-first] .variables-view-add-property {
-moz-box-ordinal-group: 0;
}
.variables-view-container[actions-first] [invisible] {
visibility: hidden;
}
/* Variables and properties tooltips */
.variable-or-property > tooltip > label {
@ -651,10 +656,6 @@
-moz-image-region: rect(0,48px,16px,32px);
}
.variables-view-delete > .toolbarbutton-text {
display: none;
}
.variables-view-edit {
background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat;
width: 20px;

View File

@ -282,6 +282,7 @@ browser.jar:
skin/classic/browser/devtools/app-manager/error.svg (../shared/devtools/app-manager/images/error.svg)
skin/classic/browser/devtools/app-manager/plus.svg (../shared/devtools/app-manager/images/plus.svg)
skin/classic/browser/devtools/app-manager/remove.svg (../shared/devtools/app-manager/images/remove.svg)
skin/classic/browser/devtools/app-manager/add.svg (../shared/devtools/app-manager/images/add.svg)
skin/classic/browser/devtools/app-manager/index-icons.svg (../shared/devtools/app-manager/images/index-icons.svg)
skin/classic/browser/devtools/app-manager/rocket.svg (../shared/devtools/app-manager/images/rocket.svg)
skin/classic/browser/devtools/app-manager/noise.png (../shared/devtools/app-manager/images/noise.png)
@ -583,6 +584,7 @@ browser.jar:
skin/classic/aero/browser/devtools/app-manager/error.svg (../shared/devtools/app-manager/images/error.svg)
skin/classic/aero/browser/devtools/app-manager/plus.svg (../shared/devtools/app-manager/images/plus.svg)
skin/classic/aero/browser/devtools/app-manager/remove.svg (../shared/devtools/app-manager/images/remove.svg)
skin/classic/aero/browser/devtools/app-manager/add.svg (../shared/devtools/app-manager/images/add.svg)
skin/classic/aero/browser/devtools/app-manager/index-icons.svg (../shared/devtools/app-manager/images/index-icons.svg)
skin/classic/aero/browser/devtools/app-manager/rocket.svg (../shared/devtools/app-manager/images/rocket.svg)
skin/classic/aero/browser/devtools/app-manager/noise.png (../shared/devtools/app-manager/images/noise.png)