mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge mozilla-central into mozilla-inbound
This commit is contained in:
commit
32208f91c5
@ -1,76 +0,0 @@
|
|||||||
#ifdef 0
|
|
||||||
/* 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/. */
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class makes it easy to wait until a batch of callbacks has finished.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* let batch = new Batch(function () alert("finished"));
|
|
||||||
* let pop = batch.pop.bind(batch);
|
|
||||||
*
|
|
||||||
* for (let i = 0; i < 5; i++) {
|
|
||||||
* batch.push();
|
|
||||||
* setTimeout(pop, i * 1000);
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* batch.close();
|
|
||||||
*/
|
|
||||||
function Batch(aCallback) {
|
|
||||||
this._callback = aCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
Batch.prototype = {
|
|
||||||
/**
|
|
||||||
* The number of batch entries.
|
|
||||||
*/
|
|
||||||
_count: 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this batch is closed.
|
|
||||||
*/
|
|
||||||
_closed: false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increases the number of batch entries by one.
|
|
||||||
*/
|
|
||||||
push: function Batch_push() {
|
|
||||||
if (!this._closed)
|
|
||||||
this._count++;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decreases the number of batch entries by one.
|
|
||||||
*/
|
|
||||||
pop: function Batch_pop() {
|
|
||||||
if (this._count)
|
|
||||||
this._count--;
|
|
||||||
|
|
||||||
if (this._closed)
|
|
||||||
this._check();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the batch so that no new entries can be added.
|
|
||||||
*/
|
|
||||||
close: function Batch_close() {
|
|
||||||
if (this._closed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this._closed = true;
|
|
||||||
this._check();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the batch has finished.
|
|
||||||
*/
|
|
||||||
_check: function Batch_check() {
|
|
||||||
if (this._count == 0 && this._callback) {
|
|
||||||
this._callback();
|
|
||||||
this._callback = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -11,6 +11,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
Cu.import("resource://gre/modules/PageThumbs.jsm");
|
Cu.import("resource://gre/modules/PageThumbs.jsm");
|
||||||
Cu.import("resource://gre/modules/NewTabUtils.jsm");
|
Cu.import("resource://gre/modules/NewTabUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
|
||||||
|
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "Rect",
|
XPCOMUtils.defineLazyModuleGetter(this, "Rect",
|
||||||
"resource://gre/modules/Geometry.jsm");
|
"resource://gre/modules/Geometry.jsm");
|
||||||
@ -39,7 +40,6 @@ function inPrivateBrowsingMode() {
|
|||||||
|
|
||||||
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
||||||
|
|
||||||
#include batch.js
|
|
||||||
#include transformations.js
|
#include transformations.js
|
||||||
#include page.js
|
#include page.js
|
||||||
#include grid.js
|
#include grid.js
|
||||||
|
@ -169,37 +169,33 @@ let gTransformation = {
|
|||||||
* callback - the callback to call when finished
|
* callback - the callback to call when finished
|
||||||
*/
|
*/
|
||||||
rearrangeSites: function Transformation_rearrangeSites(aSites, aOptions) {
|
rearrangeSites: function Transformation_rearrangeSites(aSites, aOptions) {
|
||||||
let batch;
|
let batch = [];
|
||||||
let cells = gGrid.cells;
|
let cells = gGrid.cells;
|
||||||
let callback = aOptions && aOptions.callback;
|
let callback = aOptions && aOptions.callback;
|
||||||
let unfreeze = aOptions && aOptions.unfreeze;
|
let unfreeze = aOptions && aOptions.unfreeze;
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
batch = new Batch(callback);
|
|
||||||
callback = function () batch.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
aSites.forEach(function (aSite, aIndex) {
|
aSites.forEach(function (aSite, aIndex) {
|
||||||
// Do not re-arrange empty cells or the dragged site.
|
// Do not re-arrange empty cells or the dragged site.
|
||||||
if (!aSite || aSite == gDrag.draggedSite)
|
if (!aSite || aSite == gDrag.draggedSite)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (batch)
|
let deferred = Promise.defer();
|
||||||
batch.push();
|
batch.push(deferred.promise);
|
||||||
|
let cb = function () deferred.resolve();
|
||||||
|
|
||||||
if (!cells[aIndex])
|
if (!cells[aIndex])
|
||||||
// The site disappeared from the grid, hide it.
|
// The site disappeared from the grid, hide it.
|
||||||
this.hideSite(aSite, callback);
|
this.hideSite(aSite, cb);
|
||||||
else if (this._getNodeOpacity(aSite.node) != 1)
|
else if (this._getNodeOpacity(aSite.node) != 1)
|
||||||
// The site disappeared before but is now back, show it.
|
// The site disappeared before but is now back, show it.
|
||||||
this.showSite(aSite, callback);
|
this.showSite(aSite, cb);
|
||||||
else
|
else
|
||||||
// The site's position has changed, move it around.
|
// The site's position has changed, move it around.
|
||||||
this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: callback});
|
this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: cb});
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
if (batch)
|
let wait = Promise.promised(function () callback && callback());
|
||||||
batch.close();
|
wait.apply(null, batch);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,7 +126,7 @@ let gUpdater = {
|
|||||||
* @param aCallback The callback to call when finished.
|
* @param aCallback The callback to call when finished.
|
||||||
*/
|
*/
|
||||||
_removeLegacySites: function Updater_removeLegacySites(aSites, aCallback) {
|
_removeLegacySites: function Updater_removeLegacySites(aSites, aCallback) {
|
||||||
let batch = new Batch(aCallback);
|
let batch = [];
|
||||||
|
|
||||||
// Delete sites that were removed from the grid.
|
// Delete sites that were removed from the grid.
|
||||||
gGrid.sites.forEach(function (aSite) {
|
gGrid.sites.forEach(function (aSite) {
|
||||||
@ -134,7 +134,8 @@ let gUpdater = {
|
|||||||
if (!aSite || aSites.indexOf(aSite) != -1)
|
if (!aSite || aSites.indexOf(aSite) != -1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
batch.push();
|
let deferred = Promise.defer();
|
||||||
|
batch.push(deferred.promise);
|
||||||
|
|
||||||
// Fade out the to-be-removed site.
|
// Fade out the to-be-removed site.
|
||||||
gTransformation.hideSite(aSite, function () {
|
gTransformation.hideSite(aSite, function () {
|
||||||
@ -142,11 +143,12 @@ let gUpdater = {
|
|||||||
|
|
||||||
// Remove the site from the DOM.
|
// Remove the site from the DOM.
|
||||||
node.parentNode.removeChild(node);
|
node.parentNode.removeChild(node);
|
||||||
batch.pop();
|
deferred.resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
batch.close();
|
let wait = Promise.promised(aCallback);
|
||||||
|
wait.apply(null, batch);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -156,14 +158,15 @@ let gUpdater = {
|
|||||||
*/
|
*/
|
||||||
_fillEmptyCells: function Updater_fillEmptyCells(aLinks, aCallback) {
|
_fillEmptyCells: function Updater_fillEmptyCells(aLinks, aCallback) {
|
||||||
let {cells, sites} = gGrid;
|
let {cells, sites} = gGrid;
|
||||||
let batch = new Batch(aCallback);
|
let batch = [];
|
||||||
|
|
||||||
// Find empty cells and fill them.
|
// Find empty cells and fill them.
|
||||||
sites.forEach(function (aSite, aIndex) {
|
sites.forEach(function (aSite, aIndex) {
|
||||||
if (aSite || !aLinks[aIndex])
|
if (aSite || !aLinks[aIndex])
|
||||||
return;
|
return;
|
||||||
|
|
||||||
batch.push();
|
let deferred = Promise.defer();
|
||||||
|
batch.push(deferred.promise);
|
||||||
|
|
||||||
// Create the new site and fade it in.
|
// Create the new site and fade it in.
|
||||||
let site = gGrid.createSite(aLinks[aIndex], cells[aIndex]);
|
let site = gGrid.createSite(aLinks[aIndex], cells[aIndex]);
|
||||||
@ -174,9 +177,10 @@ let gUpdater = {
|
|||||||
// Flush all style changes for the dynamically inserted site to make
|
// Flush all style changes for the dynamically inserted site to make
|
||||||
// the fade-in transition work.
|
// the fade-in transition work.
|
||||||
window.getComputedStyle(site.node).opacity;
|
window.getComputedStyle(site.node).opacity;
|
||||||
gTransformation.showSite(site, function () batch.pop());
|
gTransformation.showSite(site, function () deferred.resolve());
|
||||||
});
|
});
|
||||||
|
|
||||||
batch.close();
|
let wait = Promise.promised(aCallback);
|
||||||
|
wait.apply(null, batch);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -204,13 +204,20 @@ let SessionFileInternal = {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return TaskUtils.spawn(function task() {
|
return TaskUtils.spawn(function task() {
|
||||||
TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
||||||
|
TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
|
||||||
|
|
||||||
let bytes = gEncoder.encode(aData);
|
let bytes = gEncoder.encode(aData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
yield OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"});
|
let promise = OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"});
|
||||||
|
// At this point, we measure how long we stop the main thread
|
||||||
|
TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
|
||||||
|
|
||||||
|
// Now wait for the result and measure how long we had to wait for the result
|
||||||
|
yield promise;
|
||||||
TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
|
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
|
||||||
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
|
||||||
Cu.reportError("Could not write session state file " + self.path
|
Cu.reportError("Could not write session state file " + self.path
|
||||||
+ ": " + aReason);
|
+ ": " + aReason);
|
||||||
|
@ -18,6 +18,7 @@ this.EXPORTED_SYMBOLS = ["MarkupView"];
|
|||||||
|
|
||||||
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
|
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
|
||||||
Cu.import("resource:///modules/devtools/CssRuleView.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/Templater.jsm");
|
||||||
Cu.import("resource:///modules/devtools/Undo.jsm");
|
Cu.import("resource:///modules/devtools/Undo.jsm");
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
@ -962,7 +963,7 @@ function TextEditor(aContainer, aNode, aTemplate)
|
|||||||
|
|
||||||
aContainer.markup.template(aTemplate, this);
|
aContainer.markup.template(aTemplate, this);
|
||||||
|
|
||||||
_editableField({
|
editableField({
|
||||||
element: this.value,
|
element: this.value,
|
||||||
stopOnReturn: true,
|
stopOnReturn: true,
|
||||||
trigger: "dblclick",
|
trigger: "dblclick",
|
||||||
@ -1031,7 +1032,7 @@ function ElementEditor(aContainer, aNode)
|
|||||||
// Make the tag name editable (unless this is a document element)
|
// Make the tag name editable (unless this is a document element)
|
||||||
if (aNode != aNode.ownerDocument.documentElement) {
|
if (aNode != aNode.ownerDocument.documentElement) {
|
||||||
this.tag.setAttribute("tabindex", "0");
|
this.tag.setAttribute("tabindex", "0");
|
||||||
_editableField({
|
editableField({
|
||||||
element: this.tag,
|
element: this.tag,
|
||||||
trigger: "dblclick",
|
trigger: "dblclick",
|
||||||
stopOnReturn: true,
|
stopOnReturn: true,
|
||||||
@ -1040,7 +1041,7 @@ function ElementEditor(aContainer, aNode)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make the new attribute space editable.
|
// Make the new attribute space editable.
|
||||||
_editableField({
|
editableField({
|
||||||
element: this.newAttr,
|
element: this.newAttr,
|
||||||
trigger: "dblclick",
|
trigger: "dblclick",
|
||||||
stopOnReturn: true,
|
stopOnReturn: true,
|
||||||
@ -1120,7 +1121,7 @@ ElementEditor.prototype = {
|
|||||||
this.attrList.insertBefore(attr, before);
|
this.attrList.insertBefore(attr, before);
|
||||||
|
|
||||||
// Make the attribute editable.
|
// Make the attribute editable.
|
||||||
_editableField({
|
editableField({
|
||||||
element: inner,
|
element: inner,
|
||||||
trigger: "dblclick",
|
trigger: "dblclick",
|
||||||
stopOnReturn: true,
|
stopOnReturn: true,
|
||||||
|
@ -18,10 +18,9 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
|
|||||||
|
|
||||||
function test() {
|
function test() {
|
||||||
let inspector;
|
let inspector;
|
||||||
let tempScope = {}
|
let {
|
||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
getInplaceEditorForSpan: inplaceEditor
|
||||||
|
} = Cu.import("resource:///modules/devtools/InplaceEditor.jsm", {});
|
||||||
let inplaceEditor = tempScope._getInplaceEditorForSpan;
|
|
||||||
|
|
||||||
waitForExplicitFinish();
|
waitForExplicitFinish();
|
||||||
|
|
||||||
|
849
browser/devtools/shared/InplaceEditor.jsm
Normal file
849
browser/devtools/shared/InplaceEditor.jsm
Normal file
@ -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;
|
||||||
|
});
|
@ -121,6 +121,44 @@ StyleEditor.prototype = {
|
|||||||
return this._styleSheet;
|
return this._styleSheet;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively traverse imported stylesheets to find the index
|
||||||
|
*
|
||||||
|
* @param number aIndex
|
||||||
|
* The index of the current sheet in the document.
|
||||||
|
* @param CSSStyleSheet aSheet
|
||||||
|
* A stylesheet we're going to browse to look for all imported sheets.
|
||||||
|
*/
|
||||||
|
_getImportedStyleSheetIndex: function SE__getImportedStyleSheetIndex(aIndex, aSheet)
|
||||||
|
{
|
||||||
|
let index = aIndex;
|
||||||
|
for (let j = 0; j < aSheet.cssRules.length; j++) {
|
||||||
|
let rule = aSheet.cssRules.item(j);
|
||||||
|
if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
|
||||||
|
// Associated styleSheet may be null if it has already been seen due to
|
||||||
|
// duplicate @imports for the same URL.
|
||||||
|
if (!rule.styleSheet) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.styleSheet == this.styleSheet) {
|
||||||
|
this._styleSheetIndex = index;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
index = this._getImportedStyleSheetIndex(index, rule.styleSheet);
|
||||||
|
|
||||||
|
if (this._styleSheetIndex != -1) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
} else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
|
||||||
|
// @import rules must precede all others except @charset
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the index (order) of stylesheet in the document.
|
* Retrieve the index (order) of stylesheet in the document.
|
||||||
*
|
*
|
||||||
@ -130,11 +168,20 @@ StyleEditor.prototype = {
|
|||||||
{
|
{
|
||||||
let document = this.contentDocument;
|
let document = this.contentDocument;
|
||||||
if (this._styleSheetIndex == -1) {
|
if (this._styleSheetIndex == -1) {
|
||||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
let index = 0;
|
||||||
if (document.styleSheets[i] == this.styleSheet) {
|
let sheetIndex = 0;
|
||||||
this._styleSheetIndex = i;
|
while (sheetIndex <= document.styleSheets.length) {
|
||||||
|
let sheet = document.styleSheets[sheetIndex];
|
||||||
|
if (sheet == this.styleSheet) {
|
||||||
|
this._styleSheetIndex = index;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
index++;
|
||||||
|
index = this._getImportedStyleSheetIndex(index, sheet);
|
||||||
|
if (this._styleSheetIndex != -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sheetIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this._styleSheetIndex;
|
return this._styleSheetIndex;
|
||||||
|
@ -267,6 +267,22 @@ StyleEditorChrome.prototype = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new style editor, add to the list of editors, and bind this
|
||||||
|
* object as an action listener.
|
||||||
|
* @param DOMDocument aDocument
|
||||||
|
* The document that the stylesheet is being referenced in.
|
||||||
|
* @param CSSStyleSheet aSheet
|
||||||
|
* Optional stylesheet to edit from the document.
|
||||||
|
* @return StyleEditor
|
||||||
|
*/
|
||||||
|
_createStyleEditor: function SEC__createStyleEditor(aDocument, aSheet) {
|
||||||
|
let editor = new StyleEditor(aDocument, aSheet);
|
||||||
|
this._editors.push(editor);
|
||||||
|
editor.addActionListener(this);
|
||||||
|
return editor;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up the chrome UI. Install event listeners and so on.
|
* Set up the chrome UI. Install event listeners and so on.
|
||||||
*/
|
*/
|
||||||
@ -274,16 +290,12 @@ StyleEditorChrome.prototype = {
|
|||||||
{
|
{
|
||||||
// wire up UI elements
|
// wire up UI elements
|
||||||
wire(this._view.rootElement, ".style-editor-newButton", function onNewButton() {
|
wire(this._view.rootElement, ".style-editor-newButton", function onNewButton() {
|
||||||
let editor = new StyleEditor(this.contentDocument);
|
let editor = this._createStyleEditor(this.contentDocument);
|
||||||
this._editors.push(editor);
|
|
||||||
editor.addActionListener(this);
|
|
||||||
editor.load();
|
editor.load();
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
wire(this._view.rootElement, ".style-editor-importButton", function onImportButton() {
|
wire(this._view.rootElement, ".style-editor-importButton", function onImportButton() {
|
||||||
let editor = new StyleEditor(this.contentDocument);
|
let editor = this._createStyleEditor(this.contentDocument);
|
||||||
this._editors.push(editor);
|
|
||||||
editor.addActionListener(this);
|
|
||||||
editor.importFromFile(this._mockImportFile || null, this._window);
|
editor.importFromFile(this._mockImportFile || null, this._window);
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
},
|
},
|
||||||
@ -307,6 +319,34 @@ StyleEditorChrome.prototype = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add all imported stylesheets to chrome UI, recursively
|
||||||
|
*
|
||||||
|
* @param CSSStyleSheet aSheet
|
||||||
|
* A stylesheet we're going to browse to look for all imported sheets.
|
||||||
|
*/
|
||||||
|
_showImportedStyleSheets: function SEC__showImportedStyleSheets(aSheet)
|
||||||
|
{
|
||||||
|
let document = this.contentDocument;
|
||||||
|
for (let j = 0; j < aSheet.cssRules.length; j++) {
|
||||||
|
let rule = aSheet.cssRules.item(j);
|
||||||
|
if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
|
||||||
|
// Associated styleSheet may be null if it has already been seen due to
|
||||||
|
// duplicate @imports for the same URL.
|
||||||
|
if (!rule.styleSheet) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._createStyleEditor(document, rule.styleSheet);
|
||||||
|
|
||||||
|
this._showImportedStyleSheets(rule.styleSheet);
|
||||||
|
} else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
|
||||||
|
// @import rules must precede all others except @charset
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate the chrome UI according to the content document.
|
* Populate the chrome UI according to the content document.
|
||||||
*
|
*
|
||||||
@ -323,9 +363,9 @@ StyleEditorChrome.prototype = {
|
|||||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||||
let styleSheet = document.styleSheets[i];
|
let styleSheet = document.styleSheets[i];
|
||||||
|
|
||||||
let editor = new StyleEditor(document, styleSheet);
|
this._createStyleEditor(document, styleSheet);
|
||||||
editor.addActionListener(this);
|
|
||||||
this._editors.push(editor);
|
this._showImportedStyleSheets(styleSheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue editors loading so that ContentAttach is consistently triggered
|
// Queue editors loading so that ContentAttach is consistently triggered
|
||||||
|
@ -17,6 +17,7 @@ _BROWSER_TEST_FILES = \
|
|||||||
browser_styleeditor_cmd_edit.js \
|
browser_styleeditor_cmd_edit.js \
|
||||||
browser_styleeditor_cmd_edit.html \
|
browser_styleeditor_cmd_edit.html \
|
||||||
browser_styleeditor_import.js \
|
browser_styleeditor_import.js \
|
||||||
|
browser_styleeditor_import_rule.js \
|
||||||
browser_styleeditor_init.js \
|
browser_styleeditor_init.js \
|
||||||
browser_styleeditor_loading.js \
|
browser_styleeditor_loading.js \
|
||||||
browser_styleeditor_new.js \
|
browser_styleeditor_new.js \
|
||||||
@ -33,6 +34,9 @@ _BROWSER_TEST_FILES = \
|
|||||||
four.html \
|
four.html \
|
||||||
head.js \
|
head.js \
|
||||||
helpers.js \
|
helpers.js \
|
||||||
|
import.css \
|
||||||
|
import.html \
|
||||||
|
import2.css \
|
||||||
longload.html \
|
longload.html \
|
||||||
media.html \
|
media.html \
|
||||||
media-small.css \
|
media-small.css \
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
/* vim: set ts=2 et sw=2 tw=80: */
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
// http rather than chrome to improve coverage
|
||||||
|
const TESTCASE_URI = TEST_BASE_HTTP + "import.html";
|
||||||
|
|
||||||
|
function test()
|
||||||
|
{
|
||||||
|
waitForExplicitFinish();
|
||||||
|
|
||||||
|
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
|
||||||
|
run(aChrome);
|
||||||
|
});
|
||||||
|
|
||||||
|
content.location = TESTCASE_URI;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(aChrome)
|
||||||
|
{
|
||||||
|
is(aChrome.editors.length, 3,
|
||||||
|
"there are 3 stylesheets after loading @imports");
|
||||||
|
|
||||||
|
is(aChrome.editors[0]._styleSheet.href, TEST_BASE_HTTP + "simple.css",
|
||||||
|
"stylesheet 1 is simple.css");
|
||||||
|
|
||||||
|
is(aChrome.editors[1]._styleSheet.href, TEST_BASE_HTTP + "import.css",
|
||||||
|
"stylesheet 2 is import.css");
|
||||||
|
|
||||||
|
is(aChrome.editors[2]._styleSheet.href, TEST_BASE_HTTP + "import2.css",
|
||||||
|
"stylesheet 3 is import2.css");
|
||||||
|
|
||||||
|
finish();
|
||||||
|
}
|
10
browser/devtools/styleeditor/test/import.css
vendored
Normal file
10
browser/devtools/styleeditor/test/import.css
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* vim: set ts=2 et sw=2 tw=80: */
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
@import url(import2.css);
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
11
browser/devtools/styleeditor/test/import.html
Normal file
11
browser/devtools/styleeditor/test/import.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>import testcase</title>
|
||||||
|
<link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="simple.css"/>
|
||||||
|
<link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="import.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>import <span>testcase</span></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
browser/devtools/styleeditor/test/import2.css
Normal file
10
browser/devtools/styleeditor/test/import2.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* vim: set ts=2 et sw=2 tw=80: */
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
@import url(import.css);
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
@ -13,9 +13,6 @@ const Cu = Components.utils;
|
|||||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
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
|
* These regular expressions are adapted from firebug's css.js, and are
|
||||||
* used to parse CSSStyleDeclaration's cssText attribute.
|
* used to parse CSSStyleDeclaration's cssText attribute.
|
||||||
@ -30,12 +27,10 @@ const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/;
|
|||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
Cu.import("resource:///modules/devtools/CssLogic.jsm");
|
Cu.import("resource:///modules/devtools/CssLogic.jsm");
|
||||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
Cu.import("resource:///modules/devtools/InplaceEditor.jsm");
|
||||||
|
|
||||||
this.EXPORTED_SYMBOLS = ["CssRuleView",
|
this.EXPORTED_SYMBOLS = ["CssRuleView",
|
||||||
"_ElementStyle",
|
"_ElementStyle"];
|
||||||
"editableItem",
|
|
||||||
"_editableField",
|
|
||||||
"_getInplaceEditorForSpan"];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Our model looks like this:
|
* Our model looks like this:
|
||||||
@ -1910,781 +1905,6 @@ TextPropertyEditor.prototype = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* Store of CSSStyleDeclarations mapped to properties that have been changed by
|
||||||
* the user.
|
* the user.
|
||||||
@ -2816,28 +2036,6 @@ function appendText(aParent, aText)
|
|||||||
aParent.appendChild(aParent.ownerDocument.createTextNode(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() {
|
XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
|
||||||
return Cc["@mozilla.org/widget/clipboardhelper;1"].
|
return Cc["@mozilla.org/widget/clipboardhelper;1"].
|
||||||
getService(Ci.nsIClipboardHelper);
|
getService(Ci.nsIClipboardHelper);
|
||||||
|
@ -9,8 +9,6 @@ let tempScope = {};
|
|||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
||||||
let CssRuleView = tempScope.CssRuleView;
|
let CssRuleView = tempScope.CssRuleView;
|
||||||
let _ElementStyle = tempScope._ElementStyle;
|
let _ElementStyle = tempScope._ElementStyle;
|
||||||
let _editableField = tempScope._editableField;
|
|
||||||
let inplaceEditor = tempScope._getInplaceEditorForSpan;
|
|
||||||
|
|
||||||
let doc;
|
let doc;
|
||||||
let ruleDialog;
|
let ruleDialog;
|
||||||
|
@ -3,9 +3,6 @@
|
|||||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
let doc;
|
let doc;
|
||||||
let tempScope = {};
|
|
||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
|
||||||
let inplaceEditor = tempScope._getInplaceEditorForSpan;
|
|
||||||
let inspector;
|
let inspector;
|
||||||
let win;
|
let win;
|
||||||
|
|
||||||
|
@ -6,8 +6,6 @@ let tempScope = {}
|
|||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
||||||
let CssRuleView = tempScope.CssRuleView;
|
let CssRuleView = tempScope.CssRuleView;
|
||||||
let _ElementStyle = tempScope._ElementStyle;
|
let _ElementStyle = tempScope._ElementStyle;
|
||||||
let _editableField = tempScope._editableField;
|
|
||||||
let inplaceEditor = tempScope._getInplaceEditorForSpan;
|
|
||||||
|
|
||||||
let doc = content.document;
|
let doc = content.document;
|
||||||
|
|
||||||
@ -40,7 +38,7 @@ function testReturnCommit()
|
|||||||
{
|
{
|
||||||
clearBody();
|
clearBody();
|
||||||
let span = createSpan();
|
let span = createSpan();
|
||||||
_editableField({
|
editableField({
|
||||||
element: span,
|
element: span,
|
||||||
initial: "explicit initial",
|
initial: "explicit initial",
|
||||||
start: function() {
|
start: function() {
|
||||||
@ -57,7 +55,7 @@ function testBlurCommit()
|
|||||||
{
|
{
|
||||||
clearBody();
|
clearBody();
|
||||||
let span = createSpan();
|
let span = createSpan();
|
||||||
_editableField({
|
editableField({
|
||||||
element: span,
|
element: span,
|
||||||
start: function() {
|
start: function() {
|
||||||
is(inplaceEditor(span).input.value, "Edit Me!", "textContent of the span used.");
|
is(inplaceEditor(span).input.value, "Edit Me!", "textContent of the span used.");
|
||||||
@ -73,7 +71,7 @@ function testAdvanceCharCommit()
|
|||||||
{
|
{
|
||||||
clearBody();
|
clearBody();
|
||||||
let span = createSpan();
|
let span = createSpan();
|
||||||
_editableField({
|
editableField({
|
||||||
element: span,
|
element: span,
|
||||||
advanceChars: ":",
|
advanceChars: ":",
|
||||||
start: function() {
|
start: function() {
|
||||||
@ -91,7 +89,7 @@ function testEscapeCancel()
|
|||||||
{
|
{
|
||||||
clearBody();
|
clearBody();
|
||||||
let span = createSpan();
|
let span = createSpan();
|
||||||
_editableField({
|
editableField({
|
||||||
element: span,
|
element: span,
|
||||||
initial: "initial text",
|
initial: "initial text",
|
||||||
start: function() {
|
start: function() {
|
||||||
|
@ -6,8 +6,6 @@ let tempScope = {};
|
|||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
||||||
let CssRuleView = tempScope.CssRuleView;
|
let CssRuleView = tempScope.CssRuleView;
|
||||||
let _ElementStyle = tempScope._ElementStyle;
|
let _ElementStyle = tempScope._ElementStyle;
|
||||||
let _editableField = tempScope._editableField;
|
|
||||||
let inplaceEditor = tempScope._getInplaceEditorForSpan;
|
|
||||||
|
|
||||||
let doc;
|
let doc;
|
||||||
let ruleDialog;
|
let ruleDialog;
|
||||||
@ -57,7 +55,6 @@ function testCancelNew()
|
|||||||
{
|
{
|
||||||
// Start at the beginning: start to add a rule to the element's style
|
// Start at the beginning: start to add a rule to the element's style
|
||||||
// declaration, but leave it empty.
|
// declaration, but leave it empty.
|
||||||
|
|
||||||
let elementRuleEditor = ruleView.element.children[0]._ruleEditor;
|
let elementRuleEditor = ruleView.element.children[0]._ruleEditor;
|
||||||
waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) {
|
waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) {
|
||||||
is(inplaceEditor(elementRuleEditor.newPropSpan), aEditor, "Next focused editor should be the new property editor.");
|
is(inplaceEditor(elementRuleEditor.newPropSpan), aEditor, "Next focused editor should be the new property editor.");
|
||||||
@ -70,7 +67,6 @@ function testCancelNew()
|
|||||||
});
|
});
|
||||||
aEditor.input.blur();
|
aEditor.input.blur();
|
||||||
});
|
});
|
||||||
|
|
||||||
EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1,
|
EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1,
|
||||||
{ },
|
{ },
|
||||||
ruleDialog);
|
ruleDialog);
|
||||||
|
@ -5,9 +5,6 @@
|
|||||||
// Test that focus doesn't leave the style editor when adding a property
|
// Test that focus doesn't leave the style editor when adding a property
|
||||||
// (bug 719916)
|
// (bug 719916)
|
||||||
|
|
||||||
let tempScope = {};
|
|
||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
|
||||||
let inplaceEditor = tempScope._getInplaceEditorForSpan;
|
|
||||||
let doc;
|
let doc;
|
||||||
let inspector;
|
let inspector;
|
||||||
let stylePanel;
|
let stylePanel;
|
||||||
|
@ -6,7 +6,6 @@ let tempScope = {}
|
|||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
||||||
let CssRuleView = tempScope.CssRuleView;
|
let CssRuleView = tempScope.CssRuleView;
|
||||||
let _ElementStyle = tempScope._ElementStyle;
|
let _ElementStyle = tempScope._ElementStyle;
|
||||||
let _editableField = tempScope._editableField;
|
|
||||||
|
|
||||||
let doc;
|
let doc;
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ let tempScope = {}
|
|||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
||||||
let CssRuleView = tempScope.CssRuleView;
|
let CssRuleView = tempScope.CssRuleView;
|
||||||
let _ElementStyle = tempScope._ElementStyle;
|
let _ElementStyle = tempScope._ElementStyle;
|
||||||
let _editableField = tempScope._editableField;
|
|
||||||
|
|
||||||
let doc;
|
let doc;
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ let tempScope = {}
|
|||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
||||||
let CssRuleView = tempScope.CssRuleView;
|
let CssRuleView = tempScope.CssRuleView;
|
||||||
let _ElementStyle = tempScope._ElementStyle;
|
let _ElementStyle = tempScope._ElementStyle;
|
||||||
let _editableField = tempScope._editableField;
|
|
||||||
|
|
||||||
let doc;
|
let doc;
|
||||||
|
|
||||||
|
@ -6,8 +6,6 @@ let tempScope = {}
|
|||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
||||||
let CssRuleView = tempScope.CssRuleView;
|
let CssRuleView = tempScope.CssRuleView;
|
||||||
let _ElementStyle = tempScope._ElementStyle;
|
let _ElementStyle = tempScope._ElementStyle;
|
||||||
let _editableField = tempScope._editableField;
|
|
||||||
let inplaceEditor = tempScope._getInplaceEditorForSpan;
|
|
||||||
|
|
||||||
let doc;
|
let doc;
|
||||||
let ruleDialog;
|
let ruleDialog;
|
||||||
|
@ -6,8 +6,6 @@ let tempScope = {}
|
|||||||
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
|
||||||
let CssRuleView = tempScope.CssRuleView;
|
let CssRuleView = tempScope.CssRuleView;
|
||||||
let _ElementStyle = tempScope._ElementStyle;
|
let _ElementStyle = tempScope._ElementStyle;
|
||||||
let _editableField = tempScope._editableField;
|
|
||||||
let inplaceEditor = tempScope._getInplaceEditorForSpan;
|
|
||||||
|
|
||||||
let doc;
|
let doc;
|
||||||
let ruleDialog;
|
let ruleDialog;
|
||||||
|
@ -13,6 +13,10 @@ let CssHtmlTree = tempScope.CssHtmlTree;
|
|||||||
let gDevTools = tempScope.gDevTools;
|
let gDevTools = tempScope.gDevTools;
|
||||||
Cu.import("resource:///modules/devtools/Target.jsm", tempScope);
|
Cu.import("resource:///modules/devtools/Target.jsm", tempScope);
|
||||||
let TargetFactory = tempScope.TargetFactory;
|
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);
|
Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
|
||||||
let console = tempScope.console;
|
let console = tempScope.console;
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ Cu.import("resource://gre/modules/Services.jsm");
|
|||||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
|
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
|
||||||
|
|
||||||
|
const STACK_THICKNESS = 15;
|
||||||
|
|
||||||
this.EXPORTED_SYMBOLS = ["TiltUtils"];
|
this.EXPORTED_SYMBOLS = ["TiltUtils"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -377,6 +379,47 @@ TiltUtils.DOM = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the position and depth to display a node, this can be overriden
|
||||||
|
* to change the visualization.
|
||||||
|
*
|
||||||
|
* @param {Window} aContentWindow
|
||||||
|
* the window content holding the document
|
||||||
|
* @param {Node} aNode
|
||||||
|
* the node to get the position for
|
||||||
|
* @param {Object} aParentPosition
|
||||||
|
* the position of the parent node, as returned by this
|
||||||
|
* function
|
||||||
|
*
|
||||||
|
* @return {Object} an object describing the node's position in 3D space
|
||||||
|
* containing the following properties:
|
||||||
|
* {Number} top
|
||||||
|
* distance along the x axis
|
||||||
|
* {Number} left
|
||||||
|
* distance along the y axis
|
||||||
|
* {Number} depth
|
||||||
|
* distance along the z axis
|
||||||
|
* {Number} width
|
||||||
|
* width of the node
|
||||||
|
* {Number} height
|
||||||
|
* height of the node
|
||||||
|
* {Number} thickness
|
||||||
|
* thickness of the node
|
||||||
|
*/
|
||||||
|
getNodePosition: function TUD_getNodePosition(aContentWindow, aNode,
|
||||||
|
aParentPosition) {
|
||||||
|
// get the x, y, width and height coordinates of the node
|
||||||
|
let coord = LayoutHelpers.getRect(aNode, aContentWindow);
|
||||||
|
if (!coord) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
coord.depth = aParentPosition ? (aParentPosition.depth + aParentPosition.thickness) : 0;
|
||||||
|
coord.thickness = STACK_THICKNESS;
|
||||||
|
|
||||||
|
return coord;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traverses a document object model & calculates useful info for each node.
|
* Traverses a document object model & calculates useful info for each node.
|
||||||
*
|
*
|
||||||
@ -393,7 +436,7 @@ TiltUtils.DOM = {
|
|||||||
* {Number} maxY
|
* {Number} maxY
|
||||||
* the maximum top position of an element
|
* the maximum top position of an element
|
||||||
*
|
*
|
||||||
* @return {Array} list containing nodes depths, coordinates and local names
|
* @return {Array} list containing nodes positions and local names
|
||||||
*/
|
*/
|
||||||
traverse: function TUD_traverse(aContentWindow, aProperties)
|
traverse: function TUD_traverse(aContentWindow, aProperties)
|
||||||
{
|
{
|
||||||
@ -409,20 +452,21 @@ TiltUtils.DOM = {
|
|||||||
let store = { info: [], nodes: [] };
|
let store = { info: [], nodes: [] };
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
|
|
||||||
while (nodes.length) {
|
let queue = [
|
||||||
let queue = [];
|
{ parentPosition: null, nodes: aContentWindow.document.childNodes }
|
||||||
|
]
|
||||||
|
|
||||||
for (let i = 0, len = nodes.length; i < len; i++) {
|
while (queue.length) {
|
||||||
let node = nodes[i];
|
let { nodes, parentPosition } = queue.shift();
|
||||||
|
|
||||||
|
for (let node of nodes) {
|
||||||
// skip some nodes to avoid visualization meshes that are too bloated
|
// skip some nodes to avoid visualization meshes that are too bloated
|
||||||
let name = node.localName;
|
let name = node.localName;
|
||||||
if (!name || aInvisibleElements[name]) {
|
if (!name || aInvisibleElements[name]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the x, y, width and height coordinates of the node
|
let coord = this.getNodePosition(aContentWindow, node, parentPosition);
|
||||||
let coord = LayoutHelpers.getRect(node, aContentWindow);
|
|
||||||
if (!coord) {
|
if (!coord) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -436,17 +480,14 @@ TiltUtils.DOM = {
|
|||||||
if (coord.width > aMinSize && coord.height > aMinSize) {
|
if (coord.width > aMinSize && coord.height > aMinSize) {
|
||||||
|
|
||||||
// save the necessary details into a list to be returned later
|
// save the necessary details into a list to be returned later
|
||||||
store.info.push({ depth: depth, coord: coord, name: name });
|
store.info.push({ coord: coord, name: name });
|
||||||
store.nodes.push(node);
|
store.nodes.push(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare the queue array
|
let childNodes = (name === "iframe" || name === "frame") ? node.contentDocument.childNodes : node.childNodes;
|
||||||
Array.prototype.push.apply(queue, name === "iframe" || name === "frame" ?
|
if (childNodes.length > 0)
|
||||||
node.contentDocument.childNodes :
|
queue.push({ parentPosition: coord, nodes: childNodes });
|
||||||
node.childNodes);
|
|
||||||
}
|
}
|
||||||
nodes = queue;
|
|
||||||
depth++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return store;
|
return store;
|
||||||
|
@ -29,7 +29,6 @@ const INVISIBLE_ELEMENTS = {
|
|||||||
// weird things may happen; thus, when necessary, we'll split into groups
|
// weird things may happen; thus, when necessary, we'll split into groups
|
||||||
const MAX_GROUP_NODES = Math.pow(2, Uint16Array.BYTES_PER_ELEMENT * 8) / 12 - 1;
|
const MAX_GROUP_NODES = Math.pow(2, Uint16Array.BYTES_PER_ELEMENT * 8) / 12 - 1;
|
||||||
|
|
||||||
const STACK_THICKNESS = 15;
|
|
||||||
const WIREFRAME_COLOR = [0, 0, 0, 0.25];
|
const WIREFRAME_COLOR = [0, 0, 0, 0.25];
|
||||||
const INTRO_TRANSITION_DURATION = 1000;
|
const INTRO_TRANSITION_DURATION = 1000;
|
||||||
const OUTRO_TRANSITION_DURATION = 800;
|
const OUTRO_TRANSITION_DURATION = 800;
|
||||||
@ -735,7 +734,6 @@ TiltVisualizer.Presenter.prototype = {
|
|||||||
// etc. in a separate thread, as this process may take a while
|
// etc. in a separate thread, as this process may take a while
|
||||||
worker.postMessage({
|
worker.postMessage({
|
||||||
maxGroupNodes: MAX_GROUP_NODES,
|
maxGroupNodes: MAX_GROUP_NODES,
|
||||||
thickness: STACK_THICKNESS,
|
|
||||||
style: TiltVisualizerStyle.nodes,
|
style: TiltVisualizerStyle.nodes,
|
||||||
texWidth: this._texture.width,
|
texWidth: this._texture.width,
|
||||||
texHeight: this._texture.height,
|
texHeight: this._texture.height,
|
||||||
@ -870,12 +868,12 @@ TiltVisualizer.Presenter.prototype = {
|
|||||||
let y = info.coord.top;
|
let y = info.coord.top;
|
||||||
let w = info.coord.width;
|
let w = info.coord.width;
|
||||||
let h = info.coord.height;
|
let h = info.coord.height;
|
||||||
let z = info.depth;
|
let z = info.coord.depth + info.coord.thickness;
|
||||||
|
|
||||||
vec3.set([x, y, z * STACK_THICKNESS], highlight.v0);
|
vec3.set([x, y, z], highlight.v0);
|
||||||
vec3.set([x + w, y, z * STACK_THICKNESS], highlight.v1);
|
vec3.set([x + w, y, z], highlight.v1);
|
||||||
vec3.set([x + w, y + h, z * STACK_THICKNESS], highlight.v2);
|
vec3.set([x + w, y + h, z], highlight.v2);
|
||||||
vec3.set([x, y + h, z * STACK_THICKNESS], highlight.v3);
|
vec3.set([x, y + h, z], highlight.v3);
|
||||||
|
|
||||||
this._currentSelection = aNodeIndex;
|
this._currentSelection = aNodeIndex;
|
||||||
|
|
||||||
@ -972,7 +970,6 @@ TiltVisualizer.Presenter.prototype = {
|
|||||||
// to the far clipping plane, to check for intersections with the mesh,
|
// to the far clipping plane, to check for intersections with the mesh,
|
||||||
// and do all the heavy lifting in a separate thread
|
// and do all the heavy lifting in a separate thread
|
||||||
worker.postMessage({
|
worker.postMessage({
|
||||||
thickness: STACK_THICKNESS,
|
|
||||||
vertices: this._meshData.allVertices,
|
vertices: this._meshData.allVertices,
|
||||||
|
|
||||||
// create the ray destined for 3D picking
|
// create the ray destined for 3D picking
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given the initialization data (thickness, sizes and information about
|
* Given the initialization data (sizes and information about
|
||||||
* each DOM node) this worker sends back the arrays representing
|
* each DOM node) this worker sends back the arrays representing
|
||||||
* vertices, texture coords, colors, indices and all the needed data for
|
* vertices, texture coords, colors, indices and all the needed data for
|
||||||
* rendering the DOM visualization mesh.
|
* rendering the DOM visualization mesh.
|
||||||
@ -17,7 +17,6 @@ self.onmessage = function TWC_onMessage(event)
|
|||||||
{
|
{
|
||||||
let data = event.data;
|
let data = event.data;
|
||||||
let maxGroupNodes = parseInt(data.maxGroupNodes);
|
let maxGroupNodes = parseInt(data.maxGroupNodes);
|
||||||
let thickness = data.thickness;
|
|
||||||
let style = data.style;
|
let style = data.style;
|
||||||
let texWidth = data.texWidth;
|
let texWidth = data.texWidth;
|
||||||
let texHeight = data.texHeight;
|
let texHeight = data.texHeight;
|
||||||
@ -55,11 +54,10 @@ self.onmessage = function TWC_onMessage(event)
|
|||||||
}
|
}
|
||||||
|
|
||||||
let info = nodesInfo[n];
|
let info = nodesInfo[n];
|
||||||
let depth = info.depth;
|
|
||||||
let coord = info.coord;
|
let coord = info.coord;
|
||||||
|
|
||||||
// calculate the stack x, y, z, width and height coordinates
|
// calculate the stack x, y, z, width and height coordinates
|
||||||
let z = depth * thickness;
|
let z = coord.depth + coord.thickness;
|
||||||
let y = coord.top;
|
let y = coord.top;
|
||||||
let x = coord.left;
|
let x = coord.left;
|
||||||
let w = coord.width;
|
let w = coord.width;
|
||||||
@ -80,7 +78,7 @@ self.onmessage = function TWC_onMessage(event)
|
|||||||
|
|
||||||
let xpw = x + w;
|
let xpw = x + w;
|
||||||
let yph = y + h;
|
let yph = y + h;
|
||||||
let zmt = z - thickness;
|
let zmt = coord.depth;
|
||||||
|
|
||||||
let xotw = x / texWidth;
|
let xotw = x / texWidth;
|
||||||
let yoth = y / texHeight;
|
let yoth = y / texHeight;
|
||||||
@ -157,7 +155,7 @@ self.onmessage = function TWC_onMessage(event)
|
|||||||
ip5, ip9, ip10, ip5, ip10, ip6);
|
ip5, ip9, ip10, ip5, ip10, ip6);
|
||||||
|
|
||||||
// compute the wireframe indices
|
// compute the wireframe indices
|
||||||
if (depth !== 0) {
|
if (coord.thickness !== 0) {
|
||||||
wireframeIndices.unshift(i, ip1, ip1, ip2,
|
wireframeIndices.unshift(i, ip1, ip1, ip2,
|
||||||
ip2, ip3, ip3, i,
|
ip2, ip3, ip3, i,
|
||||||
ip8, i, ip9, ip1,
|
ip8, i, ip9, ip1,
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
self.onmessage = function TWP_onMessage(event)
|
self.onmessage = function TWP_onMessage(event)
|
||||||
{
|
{
|
||||||
let data = event.data;
|
let data = event.data;
|
||||||
let thickness = data.thickness;
|
|
||||||
let vertices = data.vertices;
|
let vertices = data.vertices;
|
||||||
let ray = data.ray;
|
let ray = data.ray;
|
||||||
|
|
||||||
@ -35,16 +34,16 @@ self.onmessage = function TWP_onMessage(event)
|
|||||||
for (let i = 0, len = vertices.length; i < len; i += 36) {
|
for (let i = 0, len = vertices.length; i < len; i += 36) {
|
||||||
|
|
||||||
// the front quad
|
// the front quad
|
||||||
let v0f = [vertices[i], vertices[i + 1], vertices[i + 2]];
|
let v0f = [vertices[i], vertices[i + 1], vertices[i + 2]];
|
||||||
let v1f = [vertices[i + 3], vertices[i + 4], vertices[i + 5]];
|
let v1f = [vertices[i + 3], vertices[i + 4], vertices[i + 5]];
|
||||||
let v2f = [vertices[i + 6], vertices[i + 7], vertices[i + 8]];
|
let v2f = [vertices[i + 6], vertices[i + 7], vertices[i + 8]];
|
||||||
let v3f = [vertices[i + 9], vertices[i + 10], vertices[i + 11]];
|
let v3f = [vertices[i + 9], vertices[i + 10], vertices[i + 11]];
|
||||||
|
|
||||||
// the back quad
|
// the back quad
|
||||||
let v0b = [v0f[0], v0f[1], v0f[2] - thickness];
|
let v0b = [vertices[i + 24], vertices[i + 25], vertices[i + 26]];
|
||||||
let v1b = [v1f[0], v1f[1], v1f[2] - thickness];
|
let v1b = [vertices[i + 27], vertices[i + 28], vertices[i + 29]];
|
||||||
let v2b = [v2f[0], v2f[1], v2f[2] - thickness];
|
let v2b = [vertices[i + 30], vertices[i + 31], vertices[i + 32]];
|
||||||
let v3b = [v3f[0], v3f[1], v3f[2] - thickness];
|
let v3b = [vertices[i + 33], vertices[i + 34], vertices[i + 35]];
|
||||||
|
|
||||||
// don't do anything with degenerate quads
|
// don't do anything with degenerate quads
|
||||||
if (!v0f[0] && !v1f[0] && !v2f[0] && !v3f[0]) {
|
if (!v0f[0] && !v1f[0] && !v2f[0] && !v3f[0]) {
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const STACK_THICKNESS = 15;
|
||||||
|
|
||||||
function init(callback) {
|
function init(callback) {
|
||||||
let iframe = gBrowser.ownerDocument.createElement("iframe");
|
let iframe = gBrowser.ownerDocument.createElement("iframe");
|
||||||
|
|
||||||
@ -71,37 +73,26 @@ function test() {
|
|||||||
|
|
||||||
let store = dom.traverse(iframe.contentWindow);
|
let store = dom.traverse(iframe.contentWindow);
|
||||||
|
|
||||||
is(store.nodes.length, 7,
|
let expected = [
|
||||||
|
{ name: "html", depth: 0 * STACK_THICKNESS },
|
||||||
|
{ name: "head", depth: 1 * STACK_THICKNESS },
|
||||||
|
{ name: "body", depth: 1 * STACK_THICKNESS },
|
||||||
|
{ name: "style", depth: 2 * STACK_THICKNESS },
|
||||||
|
{ name: "script", depth: 2 * STACK_THICKNESS },
|
||||||
|
{ name: "div", depth: 2 * STACK_THICKNESS },
|
||||||
|
{ name: "span", depth: 3 * STACK_THICKNESS },
|
||||||
|
];
|
||||||
|
|
||||||
|
is(store.nodes.length, expected.length,
|
||||||
"The traverse() function didn't walk the correct number of nodes.");
|
"The traverse() function didn't walk the correct number of nodes.");
|
||||||
is(store.info.length, 7,
|
is(store.info.length, expected.length,
|
||||||
"The traverse() function didn't examine the correct number of nodes.");
|
"The traverse() function didn't examine the correct number of nodes.");
|
||||||
is(store.info[0].name, "html",
|
|
||||||
"the 1st traversed node isn't the expected one.");
|
for (let i = 0; i < expected.length; i++) {
|
||||||
is(store.info[0].depth, 0,
|
is(store.info[i].name, expected[i].name,
|
||||||
"the 1st traversed node doesn't have the expected depth.");
|
"traversed node " + (i + 1) + " isn't the expected one.");
|
||||||
is(store.info[1].name, "head",
|
is(store.info[i].coord.depth, expected[i].depth,
|
||||||
"the 2nd traversed node isn't the expected one.");
|
"traversed node " + (i + 1) + " doesn't have the expected depth.");
|
||||||
is(store.info[1].depth, 1,
|
}
|
||||||
"the 2nd traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[2].name, "body",
|
|
||||||
"the 3rd traversed node isn't the expected one.");
|
|
||||||
is(store.info[2].depth, 1,
|
|
||||||
"the 3rd traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[3].name, "style",
|
|
||||||
"the 4th traversed node isn't the expected one.");
|
|
||||||
is(store.info[3].depth, 2,
|
|
||||||
"the 4th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[4].name, "script",
|
|
||||||
"the 5th traversed node isn't the expected one.");
|
|
||||||
is(store.info[4].depth, 2,
|
|
||||||
"the 5th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[5].name, "div",
|
|
||||||
"the 6th traversed node isn't the expected one.");
|
|
||||||
is(store.info[5].depth, 2,
|
|
||||||
"the 6th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[6].name, "span",
|
|
||||||
"the 7th traversed node isn't the expected one.");
|
|
||||||
is(store.info[6].depth, 3,
|
|
||||||
"the 7th traversed node doesn't have the expected depth.");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const STACK_THICKNESS = 15;
|
||||||
|
|
||||||
function init(callback) {
|
function init(callback) {
|
||||||
let iframe = gBrowser.ownerDocument.createElement("iframe");
|
let iframe = gBrowser.ownerDocument.createElement("iframe");
|
||||||
|
|
||||||
@ -120,73 +122,35 @@ function test() {
|
|||||||
|
|
||||||
let store = dom.traverse(iframe.contentWindow);
|
let store = dom.traverse(iframe.contentWindow);
|
||||||
|
|
||||||
is(store.nodes.length, 16,
|
let expected = [
|
||||||
|
{ name: "html", depth: 0 * STACK_THICKNESS },
|
||||||
|
{ name: "head", depth: 1 * STACK_THICKNESS },
|
||||||
|
{ name: "body", depth: 1 * STACK_THICKNESS },
|
||||||
|
{ name: "div", depth: 2 * STACK_THICKNESS },
|
||||||
|
{ name: "span", depth: 2 * STACK_THICKNESS },
|
||||||
|
{ name: "iframe", depth: 2 * STACK_THICKNESS },
|
||||||
|
{ name: "span", depth: 2 * STACK_THICKNESS },
|
||||||
|
{ name: "iframe", depth: 2 * STACK_THICKNESS },
|
||||||
|
{ name: "html", depth: 3 * STACK_THICKNESS },
|
||||||
|
{ name: "html", depth: 3 * STACK_THICKNESS },
|
||||||
|
{ name: "head", depth: 4 * STACK_THICKNESS },
|
||||||
|
{ name: "body", depth: 4 * STACK_THICKNESS },
|
||||||
|
{ name: "head", depth: 4 * STACK_THICKNESS },
|
||||||
|
{ name: "body", depth: 4 * STACK_THICKNESS },
|
||||||
|
{ name: "span", depth: 5 * STACK_THICKNESS },
|
||||||
|
{ name: "div", depth: 5 * STACK_THICKNESS },
|
||||||
|
];
|
||||||
|
|
||||||
|
is(store.nodes.length, expected.length,
|
||||||
"The traverse() function didn't walk the correct number of nodes.");
|
"The traverse() function didn't walk the correct number of nodes.");
|
||||||
is(store.info.length, 16,
|
is(store.info.length, expected.length,
|
||||||
"The traverse() function didn't examine the correct number of nodes.");
|
"The traverse() function didn't examine the correct number of nodes.");
|
||||||
is(store.info[0].name, "html",
|
|
||||||
"the 1st traversed node isn't the expected one.");
|
for (let i = 0; i < expected.length; i++) {
|
||||||
is(store.info[0].depth, 0,
|
is(store.info[i].name, expected[i].name,
|
||||||
"the 1st traversed node doesn't have the expected depth.");
|
"traversed node " + (i + 1) + " isn't the expected one.");
|
||||||
is(store.info[1].name, "head",
|
is(store.info[i].coord.depth, expected[i].depth,
|
||||||
"the 2nd traversed node isn't the expected one.");
|
"traversed node " + (i + 1) + " doesn't have the expected depth.");
|
||||||
is(store.info[1].depth, 1,
|
}
|
||||||
"the 2nd traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[2].name, "body",
|
|
||||||
"the 3rd traversed node isn't the expected one.");
|
|
||||||
is(store.info[2].depth, 1,
|
|
||||||
"the 3rd traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[3].name, "div",
|
|
||||||
"the 4th traversed node isn't the expected one.");
|
|
||||||
is(store.info[3].depth, 2,
|
|
||||||
"the 4th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[4].name, "span",
|
|
||||||
"the 5th traversed node isn't the expected one.");
|
|
||||||
is(store.info[4].depth, 2,
|
|
||||||
"the 5th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[5].name, "iframe",
|
|
||||||
"the 6th traversed node isn't the expected one.");
|
|
||||||
is(store.info[5].depth, 2,
|
|
||||||
"the 6th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[6].name, "span",
|
|
||||||
"the 7th traversed node isn't the expected one.");
|
|
||||||
is(store.info[6].depth, 2,
|
|
||||||
"the 7th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[7].name, "iframe",
|
|
||||||
"the 8th traversed node isn't the expected one.");
|
|
||||||
is(store.info[7].depth, 2,
|
|
||||||
"the 8th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[8].name, "html",
|
|
||||||
"the 9th traversed node isn't the expected one.");
|
|
||||||
is(store.info[8].depth, 3,
|
|
||||||
"the 9th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[9].name, "html",
|
|
||||||
"the 10th traversed node isn't the expected one.");
|
|
||||||
is(store.info[9].depth, 3,
|
|
||||||
"the 10th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[10].name, "head",
|
|
||||||
"the 11th traversed node isn't the expected one.");
|
|
||||||
is(store.info[10].depth, 4,
|
|
||||||
"the 11th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[11].name, "body",
|
|
||||||
"the 12th traversed node isn't the expected one.");
|
|
||||||
is(store.info[11].depth, 4,
|
|
||||||
"the 12th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[12].name, "head",
|
|
||||||
"the 13th traversed node isn't the expected one.");
|
|
||||||
is(store.info[12].depth, 4,
|
|
||||||
"the 13th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[13].name, "body",
|
|
||||||
"the 14th traversed node isn't the expected one.");
|
|
||||||
is(store.info[13].depth, 4,
|
|
||||||
"the 14th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[14].name, "span",
|
|
||||||
"the 15th traversed node isn't the expected one.");
|
|
||||||
is(store.info[14].depth, 5,
|
|
||||||
"the 15th traversed node doesn't have the expected depth.");
|
|
||||||
is(store.info[15].name, "div",
|
|
||||||
"the 16th traversed node isn't the expected one.");
|
|
||||||
is(store.info[15].depth, 5,
|
|
||||||
"the 16th traversed node doesn't have the expected depth.");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -150,10 +150,9 @@
|
|||||||
|
|
||||||
.devtools-tab {
|
.devtools-tab {
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
width: 47px;
|
min-width: 32px;
|
||||||
min-width: 47px;
|
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
max-width: 137px;
|
max-width: 127px;
|
||||||
color: #b6babf;
|
color: #b6babf;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -163,13 +162,15 @@
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: left, right;
|
background-position: left, right;
|
||||||
border-right: 1px solid hsla(206,37%,4%,.45);
|
border-right: 1px solid hsla(206,37%,4%,.45);
|
||||||
|
-moz-box-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.devtools-tab > image {
|
.devtools-tab > image {
|
||||||
border: none;
|
border: none;
|
||||||
-moz-margin-end: 6px;
|
-moz-margin-end: 0;
|
||||||
-moz-margin-start: 16px;
|
-moz-margin-start: 8px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
max-height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.devtools-tab > label {
|
.devtools-tab > label {
|
||||||
|
@ -102,6 +102,7 @@
|
|||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 85px;
|
min-width: 85px;
|
||||||
|
max-width: 250px;
|
||||||
min-height: 25px;
|
min-height: 25px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 1px 13px 2px 13px;
|
border-width: 1px 13px 2px 13px;
|
||||||
|
@ -139,10 +139,9 @@
|
|||||||
|
|
||||||
.devtools-tab {
|
.devtools-tab {
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
width: 47px;
|
min-width: 32px;
|
||||||
min-width: 47px;
|
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
max-width: 137px;
|
max-width: 110px;
|
||||||
color: #b6babf;
|
color: #b6babf;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -152,11 +151,12 @@
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: left, right;
|
background-position: left, right;
|
||||||
border-right: 1px solid hsla(206,37%,4%,.45);
|
border-right: 1px solid hsla(206,37%,4%,.45);
|
||||||
|
-moz-box-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.devtools-tab > image {
|
.devtools-tab > image {
|
||||||
-moz-margin-end: 6px;
|
-moz-margin-end: 0;
|
||||||
-moz-margin-start: 16px;
|
-moz-margin-start: 8px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,6 +102,7 @@
|
|||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 85px;
|
min-width: 85px;
|
||||||
|
max-width: 250px;
|
||||||
min-height: 25px;
|
min-height: 25px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 1px 13px 2px 13px;
|
border-width: 1px 13px 2px 13px;
|
||||||
|
@ -151,10 +151,9 @@
|
|||||||
|
|
||||||
.devtools-tab {
|
.devtools-tab {
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
width: 47px;
|
min-width: 32px;
|
||||||
min-width: 47px;
|
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
max-width: 137px;
|
max-width: 110px;
|
||||||
color: #b6babf;
|
color: #b6babf;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -165,11 +164,12 @@
|
|||||||
background-position: left, right;
|
background-position: left, right;
|
||||||
border-top: 1px solid #060a0d;
|
border-top: 1px solid #060a0d;
|
||||||
border-right: 1px solid hsla(206,37%,4%,.45);
|
border-right: 1px solid hsla(206,37%,4%,.45);
|
||||||
|
-moz-box-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.devtools-tab > image {
|
.devtools-tab > image {
|
||||||
-moz-margin-end: 6px;
|
-moz-margin-end: 0;
|
||||||
-moz-margin-start: 16px;
|
-moz-margin-start: 8px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,6 +102,7 @@
|
|||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 85px;
|
min-width: 85px;
|
||||||
|
max-width: 250px;
|
||||||
min-height: 25px;
|
min-height: 25px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 2px 13px;
|
border-width: 2px 13px;
|
||||||
|
@ -2489,6 +2489,13 @@
|
|||||||
"extended_statistics_ok": true,
|
"extended_statistics_ok": true,
|
||||||
"description": "Session restore: Time to write the session data to the file on disk (ms)"
|
"description": "Session restore: Time to write the session data to the file on disk (ms)"
|
||||||
},
|
},
|
||||||
|
"FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS": {
|
||||||
|
"kind": "exponential",
|
||||||
|
"high": "3000",
|
||||||
|
"n_buckets": 10,
|
||||||
|
"extended_statistics_ok": true,
|
||||||
|
"description": "Session restore: Duration of the longest uninterruptible operation while writing session data (ms)"
|
||||||
|
},
|
||||||
"FX_SESSION_RESTORE_CORRUPT_FILE": {
|
"FX_SESSION_RESTORE_CORRUPT_FILE": {
|
||||||
"kind": "boolean",
|
"kind": "boolean",
|
||||||
"description": "Session restore: Whether the file read on startup contained parse-able JSON"
|
"description": "Session restore: Whether the file read on startup contained parse-able JSON"
|
||||||
|
@ -244,7 +244,14 @@ this.PageThumbs = {
|
|||||||
let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||||
.getInterface(Ci.nsIDOMWindowUtils);
|
.getInterface(Ci.nsIDOMWindowUtils);
|
||||||
let sbWidth = {}, sbHeight = {};
|
let sbWidth = {}, sbHeight = {};
|
||||||
utils.getScrollbarSize(false, sbWidth, sbHeight);
|
|
||||||
|
try {
|
||||||
|
utils.getScrollbarSize(false, sbWidth, sbHeight);
|
||||||
|
} catch (e) {
|
||||||
|
// This might fail if the window does not have a presShell.
|
||||||
|
Cu.reportError("Unable to get scrollbar size in _determineCropSize.");
|
||||||
|
sbWidth.value = sbHeight.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Even in RTL mode, scrollbars are always on the right.
|
// Even in RTL mode, scrollbars are always on the right.
|
||||||
// So there's no need to determine a left offset.
|
// So there's no need to determine a left offset.
|
||||||
|
@ -33,26 +33,6 @@ function ThreadActor(aHooks, aGlobal)
|
|||||||
this._sources = {};
|
this._sources = {};
|
||||||
this.global = aGlobal;
|
this.global = aGlobal;
|
||||||
|
|
||||||
/**
|
|
||||||
* A script cache that maps script URLs to arrays of different Debugger.Script
|
|
||||||
* instances that have the same URL. For example, when an inline <script> tag
|
|
||||||
* in a web page contains a function declaration, the JS engine creates two
|
|
||||||
* Debugger.Script objects, one for the function and one for the script tag
|
|
||||||
* as a whole. The two objects will usually have different startLine and/or
|
|
||||||
* lineCount properties. For the edge case where two scripts are contained in
|
|
||||||
* the same line we need column support.
|
|
||||||
*
|
|
||||||
* The sparse array that is mapped to each URL serves as an additional mapping
|
|
||||||
* from startLine numbers to Debugger.Script objects, facilitating retrieval
|
|
||||||
* of the scripts that contain a particular line number. For example, if a
|
|
||||||
* cache holds two scripts with the URL http://foo.com/ starting at lines 4
|
|
||||||
* and 10, then the corresponding cache will be:
|
|
||||||
* this._scripts: {
|
|
||||||
* 'http://foo.com/': [,,,,[Debugger.Script],,,,,,[Debugger.Script]]
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
this._scripts = {};
|
|
||||||
|
|
||||||
// A cache of prototype chains for objects that have received a
|
// A cache of prototype chains for objects that have received a
|
||||||
// prototypeAndProperties request. Due to the way the debugger frontend works,
|
// prototypeAndProperties request. Due to the way the debugger frontend works,
|
||||||
// this corresponds to a cache of prototype chains that the user has been
|
// this corresponds to a cache of prototype chains that the user has been
|
||||||
@ -99,7 +79,6 @@ ThreadActor.prototype = {
|
|||||||
}
|
}
|
||||||
this.conn.removeActorPool(this._threadLifetimePool || undefined);
|
this.conn.removeActorPool(this._threadLifetimePool || undefined);
|
||||||
this._threadLifetimePool = null;
|
this._threadLifetimePool = null;
|
||||||
this._scripts = {};
|
|
||||||
this._sources = {};
|
this._sources = {};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -490,7 +469,7 @@ ThreadActor.prototype = {
|
|||||||
|
|
||||||
let location = aRequest.location;
|
let location = aRequest.location;
|
||||||
let line = location.line;
|
let line = location.line;
|
||||||
if (!this._scripts[location.url] || line < 0) {
|
if (this.dbg.findScripts({ url: location.url }).length == 0 || line < 0) {
|
||||||
return { error: "noScript" };
|
return { error: "noScript" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -613,35 +592,6 @@ ThreadActor.prototype = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* A recursive generator function for iterating over the scripts that contain
|
|
||||||
* the specified line, by looking through child scripts of the supplied
|
|
||||||
* script. As an example, an inline <script> tag has the top-level functions
|
|
||||||
* declared in it as its children.
|
|
||||||
*
|
|
||||||
* @param aScript Debugger.Script
|
|
||||||
* The source script.
|
|
||||||
* @param aLine number
|
|
||||||
* The line number.
|
|
||||||
*/
|
|
||||||
_getContainers: function TA__getContainers(aScript, aLine) {
|
|
||||||
let children = aScript.getChildScripts();
|
|
||||||
if (children.length > 0) {
|
|
||||||
for (let i = 0; i < children.length; i++) {
|
|
||||||
let child = children[i];
|
|
||||||
// Iterate over the children that contain this location.
|
|
||||||
if (child.startLine <= aLine &&
|
|
||||||
child.startLine + child.lineCount > aLine) {
|
|
||||||
for (let j of this._getContainers(child, aLine)) {
|
|
||||||
yield j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Include this script in the iteration, too.
|
|
||||||
yield aScript;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the script and source lists from the debugger.
|
* Get the script and source lists from the debugger.
|
||||||
*/
|
*/
|
||||||
@ -1188,13 +1138,6 @@ ThreadActor.prototype = {
|
|||||||
// inferring them through scripts.
|
// inferring them through scripts.
|
||||||
this._addSource(aScript.url);
|
this._addSource(aScript.url);
|
||||||
|
|
||||||
// Use a sparse array for storing the scripts for each URL in order to
|
|
||||||
// optimize retrieval.
|
|
||||||
if (!this._scripts[aScript.url]) {
|
|
||||||
this._scripts[aScript.url] = [];
|
|
||||||
}
|
|
||||||
this._scripts[aScript.url][aScript.startLine] = aScript;
|
|
||||||
|
|
||||||
// Set any stored breakpoints.
|
// Set any stored breakpoints.
|
||||||
let existing = this._breakpointStore[aScript.url];
|
let existing = this._breakpointStore[aScript.url];
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
@ -33,20 +33,17 @@ function createRootActor()
|
|||||||
this._discoverScriptsAndSources();
|
this._discoverScriptsAndSources();
|
||||||
|
|
||||||
let scripts = [];
|
let scripts = [];
|
||||||
for (let url in this._scripts) {
|
for (let s of this.dbg.findScripts()) {
|
||||||
for (let i = 0; i < this._scripts[url].length; i++) {
|
if (!s.url) {
|
||||||
if (!this._scripts[url][i]) {
|
continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let script = {
|
|
||||||
url: url,
|
|
||||||
startLine: i,
|
|
||||||
lineCount: this._scripts[url][i].lineCount,
|
|
||||||
source: this._getSource(url).form()
|
|
||||||
};
|
|
||||||
scripts.push(script);
|
|
||||||
}
|
}
|
||||||
|
let script = {
|
||||||
|
url: s.url,
|
||||||
|
startLine: s.startLine,
|
||||||
|
lineCount: s.lineCount,
|
||||||
|
source: this._getSource(s.url).form()
|
||||||
|
};
|
||||||
|
scripts.push(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Loading…
Reference in New Issue
Block a user