Bug 816967 - Remotable Style Editor; r=dcamp

This commit is contained in:
Heather Arthur 2013-04-25 09:46:13 -07:00
parent 560a28af24
commit 76a6f2869e
39 changed files with 2607 additions and 3012 deletions

View File

@ -143,7 +143,7 @@ let styleEditorDefinition = {
tooltip: l10n("ToolboxStyleEditor.tooltip", styleEditorStrings),
isTargetSupported: function(target) {
return !target.isRemote;
return true;
},
build: function(iframeWindow, toolbox) {

File diff suppressed because it is too large Load Diff

View File

@ -1,627 +0,0 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["StyleEditorChrome"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource:///modules/devtools/StyleEditor.jsm");
Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
Cu.import("resource:///modules/devtools/SplitView.jsm");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
const STYLE_EDITOR_TEMPLATE = "stylesheet";
/**
* StyleEditorChrome constructor.
*
* The 'chrome' of the Style Editor is all the around the actual editor (textbox).
* Manages the sheet selector, history, and opened editor(s) for the attached
* content window.
*
* @param DOMElement aRoot
* Element that owns the chrome UI.
* @param DOMWindow aContentWindow
* Content DOMWindow to attach to this chrome.
*/
this.StyleEditorChrome = function StyleEditorChrome(aRoot, aContentWindow)
{
assert(aRoot, "Argument 'aRoot' is required to initialize StyleEditorChrome.");
this._root = aRoot;
this._document = this._root.ownerDocument;
this._window = this._document.defaultView;
this._editors = [];
this._listeners = []; // @see addChromeListener
// Store the content window so that we can call the real contentWindow setter
// in the open method.
this._contentWindowTemp = aContentWindow;
this._contentWindow = null;
}
StyleEditorChrome.prototype = {
_styleSheetToSelect: null,
open: function() {
let deferred = Promise.defer();
let initializeUI = function (aEvent) {
if (aEvent) {
this._window.removeEventListener("load", initializeUI, false);
}
let viewRoot = this._root.parentNode.querySelector(".splitview-root");
this._view = new SplitView(viewRoot);
this._setupChrome();
// We need to juggle arount the contentWindow items because we need to
// trigger the setter at the appropriate time.
this.contentWindow = this._contentWindowTemp; // calls setter
this._contentWindowTemp = null;
deferred.resolve();
}.bind(this);
if (this._document.readyState == "complete") {
initializeUI();
} else {
this._window.addEventListener("load", initializeUI, false);
}
return deferred.promise;
},
/**
* Retrieve the content window attached to this chrome.
*
* @return DOMWindow
* Content window or null if no content window is attached.
*/
get contentWindow() this._contentWindow,
/**
* Retrieve the ID of the content window attached to this chrome.
*
* @return number
* Window ID or -1 if no content window is attached.
*/
get contentWindowID()
{
try {
return this._contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
} catch (ex) {
return -1;
}
},
/**
* Set the content window attached to this chrome.
* Content attach or detach events/notifications are triggered after the
* operation is complete (possibly asynchronous if the content is not fully
* loaded yet).
*
* @param DOMWindow aContentWindow
* @see addChromeListener
*/
set contentWindow(aContentWindow)
{
if (this._contentWindow == aContentWindow) {
return; // no change
}
this._contentWindow = aContentWindow;
if (!aContentWindow) {
this._disableChrome();
return;
}
let onContentUnload = function () {
aContentWindow.removeEventListener("unload", onContentUnload, false);
if (this.contentWindow == aContentWindow) {
this.contentWindow = null; // detach
}
}.bind(this);
aContentWindow.addEventListener("unload", onContentUnload, false);
if (aContentWindow.document.readyState == "complete") {
this._root.classList.remove("loading");
this._populateChrome();
return;
} else {
this._root.classList.add("loading");
let onContentReady = function () {
aContentWindow.removeEventListener("load", onContentReady, false);
this._root.classList.remove("loading");
this._populateChrome();
}.bind(this);
aContentWindow.addEventListener("load", onContentReady, false);
}
},
/**
* Retrieve the content document attached to this chrome.
*
* @return DOMDocument
*/
get contentDocument()
{
return this._contentWindow ? this._contentWindow.document : null;
},
/**
* Retrieve an array with the StyleEditor instance for each live style sheet,
* ordered by style sheet index.
*
* @return Array<StyleEditor>
*/
get editors()
{
let editors = [];
this._editors.forEach(function (aEditor) {
if (aEditor.styleSheetIndex >= 0) {
editors[aEditor.styleSheetIndex] = aEditor;
}
});
return editors;
},
/**
* Get whether any of the editors have unsaved changes.
*
* @return boolean
*/
get isDirty()
{
if (this._markedDirty === true) {
return true;
}
return this.editors.some(function(editor) {
return editor.sourceEditor && editor.sourceEditor.dirty;
});
},
/*
* Mark the style editor as having unsaved changes.
*/
markDirty: function SEC_markDirty() {
this._markedDirty = true;
},
/**
* Add a listener for StyleEditorChrome events.
*
* The listener implements IStyleEditorChromeListener := {
* onContentDetach: Called when the content window has been detached.
* Arguments: (StyleEditorChrome aChrome)
* @see contentWindow
*
* onEditorAdded: Called when a stylesheet (therefore a StyleEditor
* instance) has been added to the UI.
* Arguments (StyleEditorChrome aChrome,
* StyleEditor aEditor)
* }
*
* All listener methods are optional.
*
* @param IStyleEditorChromeListener aListener
* @see removeChromeListener
*/
addChromeListener: function SEC_addChromeListener(aListener)
{
this._listeners.push(aListener);
},
/**
* Remove a listener for Chrome events from the current list of listeners.
*
* @param IStyleEditorChromeListener aListener
* @see addChromeListener
*/
removeChromeListener: function SEC_removeChromeListener(aListener)
{
let index = this._listeners.indexOf(aListener);
if (index != -1) {
this._listeners.splice(index, 1);
}
},
/**
* Trigger named handlers in StyleEditorChrome listeners.
*
* @param string aName
* Name of the event to trigger.
* @param Array aArgs
* Optional array of arguments to pass to the listener(s).
* @see addActionListener
*/
_triggerChromeListeners: function SE__triggerChromeListeners(aName, aArgs)
{
// insert the origin Chrome instance as first argument
if (!aArgs) {
aArgs = [this];
} else {
aArgs.unshift(this);
}
// copy the list of listeners to allow adding/removing listeners in handlers
let listeners = this._listeners.concat();
// trigger all listeners that have this named handler.
for (let i = 0; i < listeners.length; i++) {
let listener = listeners[i];
let handler = listener["on" + aName];
if (handler) {
handler.apply(listener, aArgs);
}
}
},
/**
* 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.
*/
_setupChrome: function SEC__setupChrome()
{
// wire up UI elements
wire(this._view.rootElement, ".style-editor-newButton", function onNewButton() {
let editor = this._createStyleEditor(this.contentDocument);
editor.load();
}.bind(this));
wire(this._view.rootElement, ".style-editor-importButton", function onImportButton() {
let editor = this._createStyleEditor(this.contentDocument);
editor.importFromFile(this._mockImportFile || null, this._window);
}.bind(this));
},
/**
* Reset the chrome UI to an empty and ready state.
*/
resetChrome: function SEC__resetChrome()
{
this._editors.forEach(function (aEditor) {
aEditor.removeActionListener(this);
}.bind(this));
this._editors = [];
this._view.removeAll();
// (re)enable UI
let matches = this._root.querySelectorAll("toolbarbutton,input,select");
for (let i = 0; i < matches.length; i++) {
matches[i].removeAttribute("disabled");
}
},
/**
* 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.
*
* @see StyleEditor._setupShadowStyleSheet
*/
_populateChrome: function SEC__populateChrome()
{
this.resetChrome();
let document = this.contentDocument;
this._document.title = _("chromeWindowTitle",
document.title || document.location.href);
for (let i = 0; i < document.styleSheets.length; i++) {
let styleSheet = document.styleSheets[i];
this._createStyleEditor(document, styleSheet);
this._showImportedStyleSheets(styleSheet);
}
// Queue editors loading so that ContentAttach is consistently triggered
// right after all editor instances are available (this.editors) but are
// NOT loaded/ready yet. This also helps responsivity during loading when
// there are many heavy stylesheets.
this._editors.forEach(function (aEditor) {
this._window.setTimeout(aEditor.load.bind(aEditor), 0);
}, this);
},
/**
* selects a stylesheet and optionally moves the cursor to a selected line
*
* @param {CSSStyleSheet} [aSheet]
* Stylesheet that should be selected. If a stylesheet is not passed
* and the editor is not initialized we focus the first stylesheet. If
* a stylesheet is not passed and the editor is initialized we ignore
* the call.
* @param {Number} [aLine]
* Line to which the caret should be moved (one-indexed).
* @param {Number} [aCol]
* Column to which the caret should be moved (one-indexed).
*/
selectStyleSheet: function SEC_selectSheet(aSheet, aLine, aCol)
{
let alreadyCalled = !!this._styleSheetToSelect;
this._styleSheetToSelect = {
sheet: aSheet,
line: aLine,
col: aCol,
};
if (alreadyCalled) {
return;
}
let select = function DEC_select(aEditor) {
let sheet = this._styleSheetToSelect.sheet;
let line = this._styleSheetToSelect.line || 1;
let col = this._styleSheetToSelect.col || 1;
if (!aEditor.sourceEditor) {
let onAttach = function SEC_selectSheet_onAttach() {
aEditor.removeActionListener(this);
this.selectedStyleSheetIndex = aEditor.styleSheetIndex;
aEditor.sourceEditor.setCaretPosition(line - 1, col - 1);
let newSheet = this._styleSheetToSelect.sheet;
let newLine = this._styleSheetToSelect.line;
let newCol = this._styleSheetToSelect.col;
this._styleSheetToSelect = null;
if (newSheet != sheet) {
this.selectStyleSheet.bind(this, newSheet, newLine, newCol);
}
}.bind(this);
aEditor.addActionListener({
onAttach: onAttach
});
} else {
// If a line or column was specified we move the caret appropriately.
aEditor.sourceEditor.setCaretPosition(line - 1, col - 1);
this._styleSheetToSelect = null;
}
let summary = sheet ? this.getSummaryElementForEditor(aEditor)
: this._view.getSummaryElementByOrdinal(0);
this._view.activeSummary = summary;
this.selectedStyleSheetIndex = aEditor.styleSheetIndex;
}.bind(this);
if (!this.editors.length) {
// We are in the main initialization phase so we wait for the editor
// containing the target stylesheet to be added and select the target
// stylesheet, optionally moving the cursor to a selected line.
let self = this;
this.addChromeListener({
onEditorAdded: function SEC_selectSheet_onEditorAdded(aChrome, aEditor) {
let sheet = self._styleSheetToSelect.sheet;
if ((sheet && aEditor.styleSheet == sheet) ||
(aEditor.styleSheetIndex == 0 && sheet == null)) {
aChrome.removeChromeListener(this);
aEditor.addActionListener(self);
select(aEditor);
}
}
});
} else if (aSheet) {
// We are already initialized and a stylesheet has been specified. Here
// we iterate through the editors and select the one containing the target
// stylesheet, optionally moving the cursor to a selected line.
for each (let editor in this.editors) {
if (editor.styleSheet == aSheet) {
select(editor);
break;
}
}
}
},
/**
* Disable all UI, effectively making editors read-only.
* This is automatically called when no content window is attached.
*
* @see contentWindow
*/
_disableChrome: function SEC__disableChrome()
{
let matches = this._root.querySelectorAll("button,toolbarbutton,textbox");
for (let i = 0; i < matches.length; i++) {
matches[i].setAttribute("disabled", "disabled");
}
this.editors.forEach(function onEnterReadOnlyMode(aEditor) {
aEditor.readOnly = true;
});
this._view.rootElement.setAttribute("disabled", "disabled");
this._triggerChromeListeners("ContentDetach");
},
/**
* Retrieve the summary element for a given editor.
*
* @param StyleEditor aEditor
* @return DOMElement
* Item's summary element or null if not found.
* @see SplitView
*/
getSummaryElementForEditor: function SEC_getSummaryElementForEditor(aEditor)
{
return this._view.getSummaryElementByOrdinal(aEditor.styleSheetIndex);
},
/**
* Update split view summary of given StyleEditor instance.
*
* @param StyleEditor aEditor
* @param DOMElement aSummary
* Optional item's summary element to update. If none, item corresponding
* to passed aEditor is used.
*/
_updateSummaryForEditor: function SEC__updateSummaryForEditor(aEditor, aSummary)
{
let summary = aSummary || this.getSummaryElementForEditor(aEditor);
let ruleCount = aEditor.styleSheet.cssRules.length;
this._view.setItemClassName(summary, aEditor.flags);
let label = summary.querySelector(".stylesheet-name > label");
label.setAttribute("value", aEditor.getFriendlyName());
text(summary, ".stylesheet-title", aEditor.styleSheet.title || "");
text(summary, ".stylesheet-rule-count",
PluralForm.get(ruleCount, _("ruleCount.label")).replace("#1", ruleCount));
text(summary, ".stylesheet-error-message", aEditor.errorMessage);
},
/**
* IStyleEditorActionListener implementation
* @See StyleEditor.addActionListener.
*/
/**
* Called when source has been loaded and editor is ready for some action.
*
* @param StyleEditor aEditor
*/
onLoad: function SEAL_onLoad(aEditor)
{
let item = this._view.appendTemplatedItem(STYLE_EDITOR_TEMPLATE, {
data: {
editor: aEditor
},
disableAnimations: this._alwaysDisableAnimations,
ordinal: aEditor.styleSheetIndex,
onCreate: function ASV_onItemCreate(aSummary, aDetails, aData) {
let editor = aData.editor;
wire(aSummary, ".stylesheet-enabled", function onToggleEnabled(aEvent) {
aEvent.stopPropagation();
aEvent.target.blur();
editor.enableStyleSheet(editor.styleSheet.disabled);
});
wire(aSummary, ".stylesheet-name", {
events: {
"keypress": function onStylesheetNameActivate(aEvent) {
if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
this._view.activeSummary = aSummary;
}
}.bind(this)
}
});
wire(aSummary, ".stylesheet-saveButton", function onSaveButton(aEvent) {
aEvent.stopPropagation();
aEvent.target.blur();
editor.saveToFile(editor.savedFile);
});
this._updateSummaryForEditor(editor, aSummary);
aSummary.addEventListener("focus", function onSummaryFocus(aEvent) {
if (aEvent.target == aSummary) {
// autofocus the stylesheet name
aSummary.querySelector(".stylesheet-name").focus();
}
}, false);
// autofocus new stylesheets
if (editor.hasFlag(StyleEditorFlags.NEW)) {
this._view.activeSummary = aSummary;
}
this._triggerChromeListeners("EditorAdded", [editor]);
}.bind(this),
onHide: function ASV_onItemShow(aSummary, aDetails, aData) {
aData.editor.onHide();
},
onShow: function ASV_onItemShow(aSummary, aDetails, aData) {
let editor = aData.editor;
if (!editor.inputElement) {
// attach editor to input element the first time it is shown
editor.inputElement = aDetails.querySelector(".stylesheet-editor-input");
}
editor.onShow();
}
});
},
/**
* Called when an editor flag changed.
*
* @param StyleEditor aEditor
* @param string aFlagName
* @see StyleEditor.flags
*/
onFlagChange: function SEAL_onFlagChange(aEditor, aFlagName)
{
this._updateSummaryForEditor(aEditor);
},
/**
* Called when when changes have been committed/applied to the live DOM
* stylesheet.
*
* @param StyleEditor aEditor
*/
onCommit: function SEAL_onCommit(aEditor)
{
this._updateSummaryForEditor(aEditor);
},
};

View File

@ -0,0 +1,332 @@
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["StyleEditorDebuggee", "StyleSheet"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/EventEmitter.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/commonjs/sdk/core/promise.js");
/**
* A StyleEditorDebuggee represents the document the style editor is debugging.
* It maintains a list of StyleSheet objects that represent the stylesheets in
* the target's document. It wraps remote debugging protocol comunications.
*
* It emits these events:
* 'stylesheet-added': A stylesheet has been added to the debuggee's document
* 'stylesheets-cleared': The debuggee's stylesheets have been reset (e.g. the
* page navigated)
*
* @param {Target} target
* The target the debuggee is listening to
*/
let StyleEditorDebuggee = function(target) {
EventEmitter.decorate(this);
this.styleSheets = [];
this.clear = this.clear.bind(this);
this._onNewDocument = this._onNewDocument.bind(this);
this._onStyleSheetsAdded = this._onStyleSheetsAdded.bind(this);
this._target = target;
this._actor = this.target.form.styleEditorActor;
this.client.addListener("styleSheetsAdded", this._onStyleSheetsAdded);
this._target.on("navigate", this._onNewDocument);
this._onNewDocument();
}
StyleEditorDebuggee.prototype = {
/**
* list of StyleSheet objects for this target
*/
styleSheets: null,
/**
* baseURIObject for the current document
*/
baseURI: null,
/**
* The target we're debugging
*/
get target() {
return this._target;
},
/**
* Client for communicating with server with remote debug protocol.
*/
get client() {
return this._target.client;
},
/**
* Get the StyleSheet object with the given href.
*
* @param {string} href
* Url of the stylesheet to find
* @return {StyleSheet}
* StyleSheet with the matching href
*/
styleSheetFromHref: function(href) {
for (let sheet of this.styleSheets) {
if (sheet.href == href) {
return sheet;
}
}
return null;
},
/**
* Clear stylesheets and state.
*/
clear: function() {
this.baseURI = null;
for (let stylesheet of this.styleSheets) {
stylesheet.destroy();
}
this.styleSheets = [];
this.emit("stylesheets-cleared");
},
/**
* Called when target is created or has navigated.
* Clear previous sheets and request new document's
*/
_onNewDocument: function() {
this.clear();
this._getBaseURI();
let message = { type: "newDocument" };
this._sendRequest(message);
},
/**
* request baseURIObject information from the document
*/
_getBaseURI: function() {
let message = { type: "getBaseURI" };
this._sendRequest(message, (response) => {
this.baseURI = response.baseURI;
});
},
/**
* Handle stylesheet-added event from the target
*
* @param {string} type
* Type of event
* @param {object} request
* Event details
*/
_onStyleSheetsAdded: function(type, request) {
for (let form of request.styleSheets) {
let sheet = this._addStyleSheet(form);
this.emit("stylesheet-added", sheet);
}
},
/**
* Create a new StyleSheet object from the form
* and add to our stylesheet list.
*
* @param {object} form
* Initial properties of the stylesheet
*/
_addStyleSheet: function(form) {
let sheet = new StyleSheet(form, this);
this.styleSheets.push(sheet);
return sheet;
},
/**
* Create a new stylesheet with the given text
* and attach it to the document.
*
* @param {string} text
* Initial text of the stylesheet
* @param {function} callback
* Function to call when the stylesheet has been added to the document
*/
createStyleSheet: function(text, callback) {
let message = { type: "newStyleSheet", text: text };
this._sendRequest(message, (response) => {
let sheet = this._addStyleSheet(response.styleSheet);
callback(sheet);
});
},
/**
* Send a request to our actor on the server
*
* @param {object} message
* Message to send to the actor
* @param {function} callback
* Function to call with reponse from actor
*/
_sendRequest: function(message, callback) {
message.to = this._actor;
this.client.request(message, callback);
},
/**
* Clean up and remove listeners
*/
destroy: function() {
this.clear();
this._target.off("will-navigate", this.clear);
this._target.off("navigate", this._onNewDocument);
}
}
/**
* A StyleSheet object represents a stylesheet on the debuggee. It wraps
* communication with a complimentary StyleSheetActor on the server.
*
* It emits these events:
* 'source-load' - The full text source of the stylesheet has been fetched
* 'property-change' - Any property (e.g 'disabled') has changed
* 'style-applied' - A change has been applied to the live stylesheet on the server
* 'error' - An error occured when loading or saving stylesheet
*
* @param {object} form
* Initial properties of the stylesheet
* @param {StyleEditorDebuggee} debuggee
* Owner of the stylesheet
*/
let StyleSheet = function(form, debuggee) {
EventEmitter.decorate(this);
this.debuggee = debuggee;
this._client = debuggee.client;
this._actor = form.actor;
this._onSourceLoad = this._onSourceLoad.bind(this);
this._onPropertyChange = this._onPropertyChange.bind(this);
this._onError = this._onError.bind(this);
this._onStyleApplied = this._onStyleApplied.bind(this);
this._client.addListener("sourceLoad-" + this._actor, this._onSourceLoad);
this._client.addListener("propertyChange-" + this._actor, this._onPropertyChange);
this._client.addListener("error-" + this._actor, this._onError);
this._client.addListener("styleApplied-" + this._actor, this._onStyleApplied);
// set initial property values
for (let attr in form) {
this[attr] = form[attr];
}
}
StyleSheet.prototype = {
/**
* Toggle the disabled attribute of the stylesheet
*/
toggleDisabled: function() {
let message = { type: "toggleDisabled" };
this._sendRequest(message);
},
/**
* Request that the source of the stylesheet be fetched.
* 'source-load' event will be fired when it's been fetched.
*/
fetchSource: function() {
let message = { type: "fetchSource" };
this._sendRequest(message);
},
/**
* Update the stylesheet in place with the given full source.
*
* @param {string} sheetText
* Full text to update the stylesheet with
*/
update: function(sheetText) {
let message = { type: "update", text: sheetText, transition: true };
this._sendRequest(message);
},
/**
* Handle source load event from the client.
*
* @param {string} type
* Event type
* @param {object} request
* Event details
*/
_onSourceLoad: function(type, request) {
this.emit("source-load", request.source);
},
/**
* Handle a property change on the stylesheet
*
* @param {string} type
* Event type
* @param {object} request
* Event details
*/
_onPropertyChange: function(type, request) {
this[request.property] = request.value;
this.emit("property-change", request.property);
},
/**
* Propogate errors from the server that relate to this stylesheet.
*
* @param {string} type
* Event type
* @param {object} request
* Event details
*/
_onError: function(type, request) {
this.emit("error", request.errorMessage);
},
/**
* Handle event when update has been successfully applied and propogate it.
*/
_onStyleApplied: function() {
this.emit("style-applied");
},
/**
* Send a request to our actor on the server
*
* @param {object} message
* Message to send to the actor
* @param {function} callback
* Function to call with reponse from actor
*/
_sendRequest: function(message, callback) {
message.to = this._actor;
this._client.request(message, callback);
},
/**
* Clean up and remove event listeners
*/
destroy: function() {
this._client.removeListener("sourceLoad-" + this._actor, this._onSourceLoad);
this._client.removeListener("propertyChange-" + this._actor, this._onPropertyChange);
this._client.removeListener("error-" + this._actor, this._onError);
this._client.removeListener("styleApplied-" + this._actor, this._onStyleApplied);
}
}

View File

@ -12,6 +12,10 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
Cu.import("resource:///modules/devtools/EventEmitter.jsm");
Cu.import("resource:///modules/devtools/StyleEditorDebuggee.jsm");
Cu.import("resource:///modules/devtools/StyleEditorUI.jsm");
Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "StyleEditorChrome",
"resource:///modules/devtools/StyleEditorChrome.jsm");
@ -21,183 +25,99 @@ this.StyleEditorPanel = function StyleEditorPanel(panelWin, toolbox) {
this._toolbox = toolbox;
this._target = toolbox.target;
this.newPage = this.newPage.bind(this);
this.destroy = this.destroy.bind(this);
this.beforeNavigate = this.beforeNavigate.bind(this);
this._target.on("will-navigate", this.beforeNavigate);
this._target.on("navigate", this.newPage);
this._target.on("close", this.destroy);
this._panelWin = panelWin;
this._panelDoc = panelWin.document;
this.destroy = this.destroy.bind(this);
this._showError = this._showError.bind(this);
}
StyleEditorPanel.prototype = {
get target() this._toolbox.target,
get panelWindow() this._panelWin,
/**
* open is effectively an asynchronous constructor
*/
open: function StyleEditor_open() {
let contentWin = this._toolbox.target.window;
open: function() {
let deferred = Promise.defer();
this.setPage(contentWin).then(function() {
let promise;
// We always interact with the target as if it were remote
if (!this.target.isRemote) {
promise = this.target.makeRemote();
} else {
promise = Promise.resolve(this.target);
}
promise.then(() => {
this.target.on("close", this.destroy);
this._debuggee = new StyleEditorDebuggee(this.target);
this.UI = new StyleEditorUI(this._debuggee, this._panelDoc);
this.UI.on("error", this._showError);
this.isReady = true;
deferred.resolve(this);
}.bind(this));
})
return deferred.promise;
},
/**
* Target getter.
* Show an error message from the style editor in the toolbox
* notification box.
*
* @param {string} event
* Type of event
* @param {string} errorCode
* Error code of error to report
*/
get target() this._target,
/**
* Panel window getter.
*/
get panelWindow() this._panelWin,
/**
* StyleEditorChrome instance getter.
*/
get styleEditorChrome() this._panelWin.styleEditorChrome,
/**
* Set the page to target.
*/
setPage: function StyleEditor_setPage(contentWindow) {
if (this._panelWin.styleEditorChrome) {
this._panelWin.styleEditorChrome.contentWindow = contentWindow;
this.selectStyleSheet(null, null, null);
} else {
let chromeRoot = this._panelDoc.getElementById("style-editor-chrome");
let chrome = new StyleEditorChrome(chromeRoot, contentWindow);
let promise = chrome.open();
this._panelWin.styleEditorChrome = chrome;
this.selectStyleSheet(null, null, null);
return promise;
_showError: function(event, errorCode) {
let message = _(errorCode);
let notificationBox = this._toolbox.getNotificationBox();
let notification = notificationBox.getNotificationWithValue("styleeditor-error");
if (!notification) {
notificationBox.appendNotification(message,
"styleeditor-error", "", notificationBox.PRIORITY_CRITICAL_LOW);
}
},
/**
* Navigated to a new page.
*/
newPage: function StyleEditor_newPage(event, payload) {
let window = payload._navPayload || payload;
this.reset();
this.setPage(window);
},
/**
* Before navigating to a new page or reloading the page.
*/
beforeNavigate: function StyleEditor_beforeNavigate(event, payload) {
let request = payload._navPayload || payload;
if (this.styleEditorChrome.isDirty) {
this.preventNavigate(request);
}
},
/**
* Show a notificiation about losing unsaved changes.
*/
preventNavigate: function StyleEditor_preventNavigate(request) {
request.suspend();
let notificationBox = null;
if (this.target.isLocalTab) {
let gBrowser = this.target.tab.ownerDocument.defaultView.gBrowser;
notificationBox = gBrowser.getNotificationBox();
}
else {
notificationBox = this._toolbox.getNotificationBox();
}
let notification = notificationBox.
getNotificationWithValue("styleeditor-page-navigation");
if (notification) {
notificationBox.removeNotification(notification, true);
}
let cancelRequest = function onCancelRequest() {
if (request) {
request.cancel(Cr.NS_BINDING_ABORTED);
request.resume(); // needed to allow the connection to be cancelled.
request = null;
}
};
let eventCallback = function onNotificationCallback(event) {
if (event == "removed") {
cancelRequest();
}
};
let buttons = [
{
id: "styleeditor.confirmNavigationAway.buttonLeave",
label: this.strings.GetStringFromName("confirmNavigationAway.buttonLeave"),
accessKey: this.strings.GetStringFromName("confirmNavigationAway.buttonLeaveAccesskey"),
callback: function onButtonLeave() {
if (request) {
request.resume();
request = null;
}
}.bind(this),
},
{
id: "styleeditor.confirmNavigationAway.buttonStay",
label: this.strings.GetStringFromName("confirmNavigationAway.buttonStay"),
accessKey: this.strings.GetStringFromName("confirmNavigationAway.buttonStayAccesskey"),
callback: cancelRequest
},
];
let message = this.strings.GetStringFromName("confirmNavigationAway.message");
notification = notificationBox.appendNotification(message,
"styleeditor-page-navigation", "chrome://browser/skin/Info.png",
notificationBox.PRIORITY_WARNING_HIGH, buttons, eventCallback);
// Make sure this not a transient notification, to avoid the automatic
// transient notification removal.
notification.persistence = -1;
},
/**
* No window available anymore.
*/
reset: function StyleEditor_reset() {
this._panelWin.styleEditorChrome.resetChrome();
},
/**
* Select a stylesheet.
*
* @param {string} href
* Url of stylesheet to find and select in editor
* @param {number} line
* Line number to jump to after selecting
* @param {number} col
* Column number to jump to after selecting
*/
selectStyleSheet: function StyleEditor_selectStyleSheet(stylesheet, line, col) {
this._panelWin.styleEditorChrome.selectStyleSheet(stylesheet, line, col);
selectStyleSheet: function(href, line, col) {
if (!this._debuggee || !this.UI) {
return;
}
let stylesheet = this._debuggee.styleSheetFromHref(href);
this.UI.selectStyleSheet(href, line, col);
},
/**
* Destroy StyleEditor
* Destroy the style editor.
*/
destroy: function StyleEditor_destroy() {
destroy: function() {
if (!this._destroyed) {
this._destroyed = true;
this._target.off("will-navigate", this.beforeNavigate);
this._target.off("navigate", this.newPage);
this._target.off("close", this.destroy);
this._target = null;
this._toolbox = null;
this._panelWin = null;
this._panelDoc = null;
this._debuggee.destroy();
this.UI.destroy();
}
return Promise.resolve(null);

View File

@ -0,0 +1,427 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["StyleEditorUI"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
Cu.import("resource:///modules/devtools/EventEmitter.jsm");
Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
Cu.import("resource:///modules/devtools/SplitView.jsm");
Cu.import("resource:///modules/devtools/StyleSheetEditor.jsm");
const LOAD_ERROR = "error-load";
const STYLE_EDITOR_TEMPLATE = "stylesheet";
/**
* StyleEditorUI is controls and builds the UI of the Style Editor, including
* maintaining a list of editors for each stylesheet on a debuggee.
*
* Emits events:
* 'editor-added': A new editor was added to the UI
* 'error': An error occured
*
* @param {StyleEditorDebuggee} debuggee
* Debuggee of whose stylesheets should be shown in the UI
* @param {Document} panelDoc
* Document of the toolbox panel to populate UI in.
*/
function StyleEditorUI(debuggee, panelDoc) {
EventEmitter.decorate(this);
this._debuggee = debuggee;
this._panelDoc = panelDoc;
this._window = this._panelDoc.defaultView;
this._root = this._panelDoc.getElementById("style-editor-chrome");
this.editors = [];
this.selectedStyleSheetIndex = -1;
this._onStyleSheetAdded = this._onStyleSheetAdded.bind(this);
this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this);
this._onStyleSheetsCleared = this._onStyleSheetsCleared.bind(this);
this._onError = this._onError.bind(this);
debuggee.on("stylesheet-added", this._onStyleSheetAdded);
debuggee.on("stylesheets-cleared", this._onStyleSheetsCleared);
this.createUI();
}
StyleEditorUI.prototype = {
/**
* Get whether any of the editors have unsaved changes.
*
* @return boolean
*/
get isDirty()
{
if (this._markedDirty === true) {
return true;
}
return this.editors.some((editor) => {
return editor.sourceEditor && editor.sourceEditor.dirty;
});
},
/*
* Mark the style editor as having or not having unsaved changes.
*/
set isDirty(value) {
this._markedDirty = value;
},
/**
* Build the initial UI and wire buttons with event handlers.
*/
createUI: function() {
let viewRoot = this._root.parentNode.querySelector(".splitview-root");
this._view = new SplitView(viewRoot);
wire(this._view.rootElement, ".style-editor-newButton", function onNew() {
this._debuggee.createStyleSheet(null, this._onStyleSheetCreated);
}.bind(this));
wire(this._view.rootElement, ".style-editor-importButton", function onImport() {
this._importFromFile(this._mockImportFile || null, this._window);
}.bind(this));
},
/**
* Import a style sheet from file and asynchronously create a
* new stylesheet on the debuggee for it.
*
* @param {mixed} file
* Optional nsIFile or filename string.
* If not set a file picker will be shown.
* @param {nsIWindow} parentWindow
* Optional parent window for the file picker.
*/
_importFromFile: function(file, parentWindow)
{
let onFileSelected = function(file) {
if (!file) {
this.emit("error", LOAD_ERROR);
return;
}
NetUtil.asyncFetch(file, (stream, status) => {
if (!Components.isSuccessCode(status)) {
this.emit("error", LOAD_ERROR);
return;
}
let source = NetUtil.readInputStreamToString(stream, stream.available());
stream.close();
this._debuggee.createStyleSheet(source, (styleSheet) => {
this._onStyleSheetCreated(styleSheet, file);
});
});
}.bind(this);
showFilePicker(file, false, parentWindow, onFileSelected);
},
/**
* Handler for debuggee's 'stylesheets-cleared' event. Remove all editors.
*/
_onStyleSheetsCleared: function() {
this._clearStyleSheetEditors();
this._view.removeAll();
this.selectedStyleSheetIndex = -1;
this._root.classList.add("loading");
},
/**
* When a new or imported stylesheet has been added to the document.
* Add an editor for it.
*/
_onStyleSheetCreated: function(styleSheet, file) {
this._addStyleSheetEditor(styleSheet, file, true);
},
/**
* Handler for debuggee's 'stylesheet-added' event. Add an editor.
*
* @param {string} event
* Event name
* @param {StyleSheet} styleSheet
* StyleSheet object for new sheet
*/
_onStyleSheetAdded: function(event, styleSheet) {
// this might be the first stylesheet, so remove loading indicator
this._root.classList.remove("loading");
this._addStyleSheetEditor(styleSheet);
},
/**
* Forward any error from a stylesheet.
*
* @param {string} event
* Event name
* @param {string} errorCode
* Code represeting type of error
*/
_onError: function(event, errorCode) {
this.emit("error", errorCode);
},
/**
* Add a new editor to the UI for a stylesheet.
*
* @param {StyleSheet} styleSheet
* Object representing stylesheet
* @param {nsIfile} file
* Optional file object that sheet was imported from
* @param {Boolean} isNew
* Optional if stylesheet is a new sheet created by user
*/
_addStyleSheetEditor: function(styleSheet, file, isNew) {
let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew);
editor.once("source-load", this._sourceLoaded.bind(this, editor));
editor.on("property-change", this._summaryChange.bind(this, editor));
editor.on("style-applied", this._summaryChange.bind(this, editor));
editor.on("error", this._onError);
this.editors.push(editor);
// Queue editor loading. This helps responsivity during loading when
// there are many heavy stylesheets.
this._window.setTimeout(editor.fetchSource.bind(editor), 0);
},
/**
* Clear all the editors from the UI.
*/
_clearStyleSheetEditors: function() {
for (let editor of this.editors) {
editor.destroy();
}
this.editors = [];
},
/**
* Handler for an StyleSheetEditor's 'source-load' event.
* Create a summary UI for the editor.
*
* @param {StyleSheetEditor} editor
* Editor to create UI for.
*/
_sourceLoaded: function(editor) {
// add new sidebar item and editor to the UI
this._view.appendTemplatedItem(STYLE_EDITOR_TEMPLATE, {
data: {
editor: editor
},
disableAnimations: this._alwaysDisableAnimations,
ordinal: editor.styleSheet.styleSheetIndex,
onCreate: function(summary, details, data) {
let editor = data.editor;
editor.summary = summary;
wire(summary, ".stylesheet-enabled", function onToggleDisabled(event) {
event.stopPropagation();
event.target.blur();
editor.toggleDisabled();
});
wire(summary, ".stylesheet-name", {
events: {
"keypress": function onStylesheetNameActivate(aEvent) {
if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
this._view.activeSummary = summary;
}
}.bind(this)
}
});
wire(summary, ".stylesheet-saveButton", function onSaveButton(event) {
event.stopPropagation();
event.target.blur();
editor.saveToFile(editor.savedFile);
});
this._updateSummaryForEditor(editor, summary);
summary.addEventListener("focus", function onSummaryFocus(event) {
if (event.target == summary) {
// autofocus the stylesheet name
summary.querySelector(".stylesheet-name").focus();
}
}, false);
// autofocus if it's a new user-created stylesheet
if (editor.isNew) {
this._selectEditor(editor);
}
if (this._styleSheetToSelect
&& this._styleSheetToSelect.href == editor.styleSheet.href) {
this.switchToSelectedSheet();
}
// If this is the first stylesheet, select it
if (this.selectedStyleSheetIndex == -1
&& !this._styleSheetToSelect
&& editor.styleSheet.styleSheetIndex == 0) {
this._selectEditor(editor);
}
this.emit("editor-added", editor);
}.bind(this),
onShow: function(summary, details, data) {
let editor = data.editor;
if (!editor.sourceEditor) {
// only initialize source editor when we switch to this view
let inputElement = details.querySelector(".stylesheet-editor-input");
editor.load(inputElement);
}
editor.onShow();
}
});
},
/**
* Switch to the editor that has been marked to be selected.
*/
switchToSelectedSheet: function() {
let sheet = this._styleSheetToSelect;
for each (let editor in this.editors) {
if (editor.styleSheet.href == sheet.href) {
this._selectEditor(editor, sheet.line, sheet.col);
this._styleSheetToSelect = null;
break;
}
}
},
/**
* Select an editor in the UI.
*
* @param {StyleSheetEditor} editor
* Editor to switch to.
* @param {number} line
* Line number to jump to
* @param {number} col
* Column number to jump to
*/
_selectEditor: function(editor, line, col) {
line = line || 1;
col = col || 1;
this.selectedStyleSheetIndex = editor.styleSheet.styleSheetIndex;
editor.getSourceEditor().then(() => {
editor.sourceEditor.setCaretPosition(line - 1, col - 1);
});
this._view.activeSummary = editor.summary;
},
/**
* selects a stylesheet and optionally moves the cursor to a selected line
*
* @param {string} [href]
* Href of stylesheet that should be selected. If a stylesheet is not passed
* and the editor is not initialized we focus the first stylesheet. If
* a stylesheet is not passed and the editor is initialized we ignore
* the call.
* @param {Number} [line]
* Line to which the caret should be moved (one-indexed).
* @param {Number} [col]
* Column to which the caret should be moved (one-indexed).
*/
selectStyleSheet: function(href, line, col)
{
let alreadyCalled = !!this._styleSheetToSelect;
this._styleSheetToSelect = {
href: href,
line: line,
col: col,
};
if (alreadyCalled) {
return;
}
/* Switch to the editor for this sheet, if it exists yet.
Otherwise each editor will be checked when it's created. */
this.switchToSelectedSheet();
},
/**
* Handler for an editor's 'property-changed' event.
* Update the summary in the UI.
*
* @param {StyleSheetEditor} editor
* Editor for which a property has changed
*/
_summaryChange: function(editor) {
this._updateSummaryForEditor(editor);
},
/**
* Update split view summary of given StyleEditor instance.
*
* @param {StyleSheetEditor} editor
* @param {DOMElement} summary
* Optional item's summary element to update. If none, item corresponding
* to passed editor is used.
*/
_updateSummaryForEditor: function(editor, summary) {
summary = summary || editor.summary;
if (!summary) {
return;
}
let ruleCount = "-";
if (editor.styleSheet.ruleCount !== undefined) {
ruleCount = editor.styleSheet.ruleCount;
}
var flags = [];
if (editor.styleSheet.disabled) {
flags.push("disabled");
}
if (editor.unsaved) {
flags.push("unsaved");
}
this._view.setItemClassName(summary, flags.join(" "));
let label = summary.querySelector(".stylesheet-name > label");
label.setAttribute("value", editor.friendlyName);
text(summary, ".stylesheet-title", editor.styleSheet.title || "");
text(summary, ".stylesheet-rule-count",
PluralForm.get(ruleCount, _("ruleCount.label")).replace("#1", ruleCount));
text(summary, ".stylesheet-error-message", editor.errorMessage);
},
destroy: function() {
this._clearStyleSheetEditors();
this._debuggee.off("stylesheet-added", this._onStyleSheetAdded);
this._debuggee.off("stylesheets-cleared", this._onStyleSheetsCleared);
}
}

View File

@ -8,11 +8,10 @@
this.EXPORTED_SYMBOLS = [
"_",
"assert",
"attr", // XXXkhuey unused?
"getCurrentBrowserTabContentWindow", // XXXkhuey unused?
"log",
"text",
"wire"
"wire",
"showFilePicker"
];
const Cc = Components.classes;
@ -108,7 +107,7 @@ function forEach(aObject, aCallback)
/**
* Log a message to the console.
*
*
* @param ...rest
* One or multiple arguments to log.
* If multiple arguments are given, they will be joined by " " in the log.
@ -162,3 +161,63 @@ this.wire = function wire(aRoot, aSelectorOrElement, aDescriptor)
}
}
/**
* Show file picker and return the file user selected.
*
* @param mixed file
* Optional nsIFile or string representing the filename to auto-select.
* @param boolean toSave
* If true, the user is selecting a filename to save.
* @param nsIWindow parentWindow
* Optional parent window. If null the parent window of the file picker
* will be the window of the attached input element.
* @param callback
* The callback method, which will be called passing in the selected
* file or null if the user did not pick one.
*/
this.showFilePicker = function showFilePicker(path, toSave, parentWindow, callback)
{
if (typeof(path) == "string") {
try {
if (Services.io.extractScheme(path) == "file") {
let uri = Services.io.newURI(path, null, null);
let file = uri.QueryInterface(Ci.nsIFileURL).file;
callback(file);
return;
}
} catch (ex) {
callback(null);
return;
}
try {
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
file.initWithPath(path);
callback(file);
return;
} catch (ex) {
callback(null);
return;
}
}
if (path) { // "path" is an nsIFile
callback(path);
return;
}
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
let mode = toSave ? fp.modeSave : fp.modeOpen;
let key = toSave ? "saveStyleSheet" : "importStyleSheet";
let fpCallback = function(result) {
if (result == Ci.nsIFilePicker.returnCancel) {
callback(null);
} else {
callback(fp.file);
}
};
fp.init(parentWindow, _(key + ".title"), mode);
fp.appendFilters(_(key + ".filter"), "*.css");
fp.appendFilters(fp.filterAll);
fp.open(fpCallback);
return;
}

View File

@ -0,0 +1,548 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["StyleSheetEditor"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource:///modules/devtools/EventEmitter.jsm");
Cu.import("resource:///modules/source-editor.jsm");
Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
const SAVE_ERROR = "error-save";
// max update frequency in ms (avoid potential typing lag and/or flicker)
// @see StyleEditor.updateStylesheet
const UPDATE_STYLESHEET_THROTTLE_DELAY = 500;
/**
* StyleSheetEditor controls the editor linked to a particular StyleSheet
* object.
*
* Emits events:
* 'source-load': The source of the stylesheet has been fetched
* 'property-change': A property on the underlying stylesheet has changed
* 'source-editor-load': The source editor for this editor has been loaded
* 'error': An error has occured
*
* @param {StyleSheet} styleSheet
* @param {DOMWindow} win
* panel window for style editor
* @param {nsIFile} file
* Optional file that the sheet was imported from
* @param {boolean} isNew
* Optional whether the sheet was created by the user
*/
function StyleSheetEditor(styleSheet, win, file, isNew) {
EventEmitter.decorate(this);
this.styleSheet = styleSheet;
this._inputElement = null;
this._sourceEditor = null;
this._window = win;
this._isNew = isNew;
this.savedFile = file;
this.errorMessage = null;
this._state = { // state to use when inputElement attaches
text: "",
selection: {start: 0, end: 0},
readOnly: false,
topIndex: 0, // the first visible line
};
this._styleSheetFilePath = null;
if (styleSheet.href &&
Services.io.extractScheme(this.styleSheet.href) == "file") {
this._styleSheetFilePath = this.styleSheet.href;
}
this._onSourceLoad = this._onSourceLoad.bind(this);
this._onPropertyChange = this._onPropertyChange.bind(this);
this._onError = this._onError.bind(this);
this._focusOnSourceEditorReady = false;
this.styleSheet.once("source-load", this._onSourceLoad);
this.styleSheet.on("property-change", this._onPropertyChange);
this.styleSheet.on("error", this._onError);
}
StyleSheetEditor.prototype = {
/**
* This editor's source editor
*/
get sourceEditor() {
return this._sourceEditor;
},
/**
* Whether there are unsaved changes in the editor
*/
get unsaved() {
return this._sourceEditor && this._sourceEditor.dirty;
},
/**
* Whether the editor is for a stylesheet created by the user
* through the style editor UI.
*/
get isNew() {
return this._isNew;
},
/**
* Get a user-friendly name for the style sheet.
*
* @return string
*/
get friendlyName() {
if (this.savedFile) { // reuse the saved filename if any
return this.savedFile.leafName;
}
if (this._isNew) {
let index = this.styleSheet.styleSheetIndex + 1; // 0-indexing only works for devs
return _("newStyleSheet", index);
}
if (!this.styleSheet.href) {
let index = this.styleSheet.styleSheetIndex + 1; // 0-indexing only works for devs
return _("inlineStyleSheet", index);
}
if (!this._friendlyName) {
let sheetURI = this.styleSheet.href;
let contentURI = this.styleSheet.debuggee.baseURI;
let contentURIScheme = contentURI.scheme;
let contentURILeafIndex = contentURI.specIgnoringRef.lastIndexOf("/");
contentURI = contentURI.specIgnoringRef;
// get content base URI without leaf name (if any)
if (contentURILeafIndex > contentURIScheme.length) {
contentURI = contentURI.substring(0, contentURILeafIndex + 1);
}
// avoid verbose repetition of absolute URI when the style sheet URI
// is relative to the content URI
this._friendlyName = (sheetURI.indexOf(contentURI) == 0)
? sheetURI.substring(contentURI.length)
: sheetURI;
try {
this._friendlyName = decodeURI(this._friendlyName);
} catch (ex) {
}
}
return this._friendlyName;
},
/**
* Start fetching the full text source for this editor's sheet.
*/
fetchSource: function() {
this.styleSheet.fetchSource();
},
/**
* Handle source fetched event. Forward source-load event.
*
* @param {string} event
* Event type
* @param {string} source
* Full-text source of the stylesheet
*/
_onSourceLoad: function(event, source) {
this._state.text = prettifyCSS(source);
this.sourceLoaded = true;
this.emit("source-load");
},
/**
* Forward property-change event from stylesheet.
*
* @param {string} event
* Event type
* @param {string} property
* Property that has changed on sheet
*/
_onPropertyChange: function(event, property) {
this.emit("property-change", property);
},
/**
* Forward error event from stylesheet.
*
* @param {string} event
* Event type
* @param {string} errorCode
*/
_onError: function(event, errorCode) {
this.emit("error", errorCode);
},
/**
* Create source editor and load state into it.
* @param {DOMElement} inputElement
* Element to load source editor in
*/
load: function(inputElement) {
this._inputElement = inputElement;
let sourceEditor = new SourceEditor();
let config = {
initialText: this._state.text,
showLineNumbers: true,
mode: SourceEditor.MODES.CSS,
readOnly: this._state.readOnly,
keys: this._getKeyBindings()
};
sourceEditor.init(inputElement, config, function onSourceEditorReady() {
setupBracketCompletion(sourceEditor);
sourceEditor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
function onTextChanged(event) {
this.updateStyleSheet();
}.bind(this));
this._sourceEditor = sourceEditor;
if (this._focusOnSourceEditorReady) {
this._focusOnSourceEditorReady = false;
sourceEditor.focus();
}
sourceEditor.setTopIndex(this._state.topIndex);
sourceEditor.setSelection(this._state.selection.start,
this._state.selection.end);
this.emit("source-editor-load");
}.bind(this));
sourceEditor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
this._onPropertyChange);
},
/**
* Get the source editor for this editor.
*
* @return {Promise}
* Promise that will resolve with the editor.
*/
getSourceEditor: function() {
let deferred = Promise.defer();
if (this.sourceEditor) {
return Promise.resolve(this);
}
this.on("source-editor-load", (event) => {
deferred.resolve(this);
});
return deferred.promise;
},
/**
* Focus the Style Editor input.
*/
focus: function() {
if (this._sourceEditor) {
this._sourceEditor.focus();
} else {
this._focusOnSourceEditorReady = true;
}
},
/**
* Event handler for when the editor is shown.
*/
onShow: function() {
if (this._sourceEditor) {
this._sourceEditor.setTopIndex(this._state.topIndex);
}
this.focus();
},
/**
* Toggled the disabled state of the underlying stylesheet.
*/
toggleDisabled: function() {
this.styleSheet.toggleDisabled();
},
/**
* Queue a throttled task to update the live style sheet.
*
* @param boolean immediate
* Optional. If true the update is performed immediately.
*/
updateStyleSheet: function(immediate) {
if (this._updateTask) {
// cancel previous queued task not executed within throttle delay
this._window.clearTimeout(this._updateTask);
}
if (immediate) {
this._updateStyleSheet();
} else {
this._updateTask = this._window.setTimeout(this._updateStyleSheet.bind(this),
UPDATE_STYLESHEET_THROTTLE_DELAY);
}
},
/**
* Update live style sheet according to modifications.
*/
_updateStyleSheet: function() {
if (this.styleSheet.disabled) {
return; // TODO: do we want to do this?
}
this._updateTask = null; // reset only if we actually perform an update
// (stylesheet is enabled) so that 'missed' updates
// while the stylesheet is disabled can be performed
// when it is enabled back. @see enableStylesheet
if (this.sourceEditor) {
this._state.text = this.sourceEditor.getText();
}
this.styleSheet.update(this._state.text);
},
/**
* Save the editor contents into a file and set savedFile property.
* A file picker UI will open if file is not set and editor is not headless.
*
* @param mixed file
* Optional nsIFile or string representing the filename to save in the
* background, no UI will be displayed.
* If not specified, the original style sheet URI is used.
* To implement 'Save' instead of 'Save as', you can pass savedFile here.
* @param function(nsIFile aFile) callback
* Optional callback called when the operation has finished.
* aFile has the nsIFile object for saved file or null if the operation
* has failed or has been canceled by the user.
* @see savedFile
*/
saveToFile: function(file, callback) {
let onFile = (returnFile) => {
if (!returnFile) {
if (callback) {
callback(null);
}
return;
}
if (this._sourceEditor) {
this._state.text = this._sourceEditor.getText();
}
let ostream = FileUtils.openSafeFileOutputStream(returnFile);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let istream = converter.convertToInputStream(this._state.text);
NetUtil.asyncCopy(istream, ostream, function onStreamCopied(status) {
if (!Components.isSuccessCode(status)) {
if (callback) {
callback(null);
}
this.emit("error", SAVE_ERROR);
return;
}
FileUtils.closeSafeFileOutputStream(ostream);
// remember filename for next save if any
this._friendlyName = null;
this.savedFile = returnFile;
if (callback) {
callback(returnFile);
}
this.sourceEditor.dirty = false;
}.bind(this));
};
showFilePicker(file || this._styleSheetFilePath, true, this._window, onFile);
},
/**
* Retrieve custom key bindings objects as expected by SourceEditor.
* SourceEditor action names are not displayed to the user.
*
* @return {array} key binding objects for the source editor
*/
_getKeyBindings: function() {
let bindings = [];
bindings.push({
action: "StyleEditor.save",
code: _("saveStyleSheet.commandkey"),
accel: true,
callback: function save() {
this.saveToFile(this.savedFile);
return true;
}.bind(this)
});
bindings.push({
action: "StyleEditor.saveAs",
code: _("saveStyleSheet.commandkey"),
accel: true,
shift: true,
callback: function saveAs() {
this.saveToFile();
return true;
}.bind(this)
});
return bindings;
},
/**
* Clean up for this editor.
*/
destroy: function() {
this.styleSheet.off("source-load", this._onSourceLoad);
this.styleSheet.off("property-change", this._onPropertyChange);
this.styleSheet.off("error", this._onError);
}
}
const TAB_CHARS = "\t";
const OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
const LINE_SEPARATOR = OS === "WINNT" ? "\r\n" : "\n";
/**
* Return string that repeats text for aCount times.
*
* @param string text
* @param number aCount
* @return string
*/
function repeat(text, aCount)
{
return (new Array(aCount + 1)).join(text);
}
/**
* Prettify minified CSS text.
* This prettifies CSS code where there is no indentation in usual places while
* keeping original indentation as-is elsewhere.
*
* @param string text
* The CSS source to prettify.
* @return string
* Prettified CSS source
*/
function prettifyCSS(text)
{
// remove initial and terminating HTML comments and surrounding whitespace
text = text.replace(/(?:^\s*<!--[\r\n]*)|(?:\s*-->\s*$)/g, "");
let parts = []; // indented parts
let partStart = 0; // start offset of currently parsed part
let indent = "";
let indentLevel = 0;
for (let i = 0; i < text.length; i++) {
let c = text[i];
let shouldIndent = false;
switch (c) {
case "}":
if (i - partStart > 1) {
// there's more than just } on the line, add line
parts.push(indent + text.substring(partStart, i));
partStart = i;
}
indent = repeat(TAB_CHARS, --indentLevel);
/* fallthrough */
case ";":
case "{":
shouldIndent = true;
break;
}
if (shouldIndent) {
let la = text[i+1]; // one-character lookahead
if (!/\s/.test(la)) {
// following character should be a new line (or whitespace) but it isn't
// force indentation then
parts.push(indent + text.substring(partStart, i + 1));
if (c == "}") {
parts.push(""); // for extra line separator
}
partStart = i + 1;
} else {
return text; // assume it is not minified, early exit
}
}
if (c == "{") {
indent = repeat(TAB_CHARS, ++indentLevel);
}
}
return parts.join(LINE_SEPARATOR);
}
/**
* Set up bracket completion on a given SourceEditor.
* This automatically closes the following CSS brackets: "{", "(", "["
*
* @param SourceEditor sourceEditor
*/
function setupBracketCompletion(sourceEditor)
{
let editorElement = sourceEditor.editorElement;
let pairs = {
123: { // {
closeString: "}",
closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET
},
40: { // (
closeString: ")",
closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_0
},
91: { // [
closeString: "]",
closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET
},
};
editorElement.addEventListener("keypress", function onKeyPress(event) {
let pair = pairs[event.charCode];
if (!pair || event.ctrlKey || event.metaKey ||
event.accelKey || event.altKey) {
return true;
}
// We detected an open bracket, sending closing character
let keyCode = pair.closeKeyCode;
let charCode = pair.closeString.charCodeAt(0);
let modifiers = 0;
let utils = editorElement.ownerDocument.defaultView.
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils);
let handled = utils.sendKeyEvent("keydown", keyCode, 0, modifiers);
utils.sendKeyEvent("keypress", 0, charCode, modifiers, !handled);
utils.sendKeyEvent("keyup", keyCode, 0, modifiers);
// and rewind caret
sourceEditor.setCaretOffset(sourceEditor.getCaretOffset() - 1);
}, false);
}

View File

@ -51,13 +51,11 @@
<xul:toolbarbutton class="style-editor-newButton devtools-toolbarbutton"
accesskey="&newButton.accesskey;"
tooltiptext="&newButton.tooltip;"
label="&newButton.label;"
disabled="true"/>
label="&newButton.label;"/>
<xul:toolbarbutton class="style-editor-importButton devtools-toolbarbutton"
accesskey="&importButton.accesskey;"
tooltiptext="&importButton.tooltip;"
label="&importButton.label;"
disabled="true"/>
label="&importButton.label;"/>
</xul:toolbar>
</xul:box>
<xul:box id="splitview-resizer-target" class="splitview-nav-container"

View File

@ -21,14 +21,10 @@ _BROWSER_TEST_FILES = \
browser_styleeditor_init.js \
browser_styleeditor_loading.js \
browser_styleeditor_new.js \
browser_styleeditor_passedinsheet.js \
browser_styleeditor_pretty.js \
browser_styleeditor_private_perwindowpb.js \
browser_styleeditor_readonly.js \
browser_styleeditor_reopen.js \
browser_styleeditor_sv_keynav.js \
browser_styleeditor_sv_resize.js \
browser_styleeditor_bug_826982_location_changed.js \
browser_styleeditor_bug_851132_middle_click.js \
head.js \
helpers.js \

View File

@ -1,123 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let tempScope = {};
Cu.import("resource:///modules/devtools/Target.jsm", tempScope);
let TargetFactory = tempScope.TargetFactory;
function test() {
let notificationBox, styleEditor;
let alertActive1_called = false;
let alertActive2_called = false;
function startLocationTests() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
runTests(toolbox.getCurrentPanel(), toolbox);
}).then(null, console.error);
}
function runTests(aStyleEditor) {
styleEditor = aStyleEditor;
let para = content.document.querySelector("p");
ok(para, "found the paragraph element");
is(para.textContent, "init", "paragraph content is correct");
styleEditor.styleEditorChrome.markDirty();
notificationBox = gBrowser.getNotificationBox();
notificationBox.addEventListener("AlertActive", alertActive1, false);
gBrowser.selectedBrowser.addEventListener("load", onPageLoad, true);
content.location = "data:text/html,<div>location change test 1 for " +
"styleeditor</div><p>test1</p>";
}
function alertActive1() {
alertActive1_called = true;
notificationBox.removeEventListener("AlertActive", alertActive1, false);
let notification = notificationBox.
getNotificationWithValue("styleeditor-page-navigation");
ok(notification, "found the styleeditor-page-navigation notification");
// By closing the notification it is expected that page navigation is
// canceled.
executeSoon(function() {
notification.close();
locationTest2();
});
}
function locationTest2() {
// Location did not change.
let para = content.document.querySelector("p");
ok(para, "found the paragraph element, second time");
is(para.textContent, "init", "paragraph content is correct");
notificationBox.addEventListener("AlertActive", alertActive2, false);
content.location = "data:text/html,<div>location change test 2 for " +
"styleeditor</div><p>test2</p>";
}
function alertActive2() {
alertActive2_called = true;
notificationBox.removeEventListener("AlertActive", alertActive2, false);
let notification = notificationBox.
getNotificationWithValue("styleeditor-page-navigation");
ok(notification, "found the styleeditor-page-navigation notification");
let buttons = notification.querySelectorAll("button");
let buttonLeave = null;
for (let i = 0; i < buttons.length; i++) {
if (buttons[i].buttonInfo.id == "styleeditor.confirmNavigationAway.buttonLeave") {
buttonLeave = buttons[i];
break;
}
}
ok(buttonLeave, "the Leave page button was found");
// Accept page navigation.
executeSoon(function(){
buttonLeave.doCommand();
});
}
function onPageLoad() {
gBrowser.selectedBrowser.removeEventListener("load", onPageLoad, true);
isnot(content.location.href.indexOf("test2"), -1,
"page navigated to the correct location");
let para = content.document.querySelector("p");
ok(para, "found the paragraph element, third time");
is(para.textContent, "test2", "paragraph content is correct");
ok(alertActive1_called, "first notification box has been shown");
ok(alertActive2_called, "second notification box has been shown");
testEnd();
}
function testEnd() {
notificationBox = null;
gBrowser.removeCurrentTab();
executeSoon(finish);
}
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onBrowserLoad() {
gBrowser.selectedBrowser.removeEventListener("load", onBrowserLoad, true);
waitForFocus(startLocationTests, content);
}, true);
content.location = "data:text/html,<div>location change tests for " +
"styleeditor.</div><p>init</p>";
}

View File

@ -3,37 +3,45 @@
const TESTCASE_URI = TEST_BASE + "four.html";
let gUI;
function test() {
waitForExplicitFinish();
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
run(aChrome);
let count = 0;
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", function(event, editor) {
count++;
if (count == 2) {
runTests();
}
})
});
content.location = TESTCASE_URI;
}
let gSEChrome, timeoutID;
let timeoutID;
function run(aChrome) {
gSEChrome = aChrome;
function runTests() {
gBrowser.tabContainer.addEventListener("TabOpen", onTabAdded, false);
aChrome.editors[0].addActionListener({onAttach: onEditor0Attach});
aChrome.editors[1].addActionListener({onAttach: onEditor1Attach});
gUI.editors[0].getSourceEditor().then(onEditor0Attach);
gUI.editors[1].getSourceEditor().then(onEditor1Attach);
}
function getStylesheetNameLinkFor(aEditor) {
return gSEChrome.getSummaryElementForEditor(aEditor).querySelector(".stylesheet-name");
return aEditor.summary.querySelector(".stylesheet-name");
}
function onEditor0Attach(aEditor) {
waitForFocus(function () {
// left mouse click should focus editor 1
EventUtils.synthesizeMouseAtCenter(
getStylesheetNameLinkFor(gSEChrome.editors[1]),
getStylesheetNameLinkFor(gUI.editors[1]),
{button: 0},
gChromeWindow);
}, gChromeWindow);
gPanelWindow);
}, gPanelWindow);
}
function onEditor1Attach(aEditor) {
@ -42,9 +50,9 @@ function onEditor1Attach(aEditor) {
// right mouse click should not open a new tab
EventUtils.synthesizeMouseAtCenter(
getStylesheetNameLinkFor(gSEChrome.editors[2]),
getStylesheetNameLinkFor(gUI.editors[2]),
{button: 1},
gChromeWindow);
gPanelWindow);
setTimeout(finish, 0);
}
@ -56,5 +64,5 @@ function onTabAdded() {
registerCleanupFunction(function () {
gBrowser.tabContainer.removeEventListener("TabOpen", onTabAdded, false);
gSEChrome = null;
gUI = null;
});

View File

@ -5,97 +5,71 @@
// https rather than chrome to improve coverage
const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
function test()
{
waitForExplicitFinish();
let count = 0;
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
aChrome.addChromeListener({
onEditorAdded: function (aChrome, aEditor) {
count++;
if (count == 2) {
// we test against first stylesheet after all are ready
let editor = aChrome.editors[0];
if (!editor.sourceEditor) {
editor.addActionListener({
onAttach: function (aEditor) {
run(aChrome, aEditor);
}
});
} else {
run(aChrome, editor);
}
}
addTabAndOpenStyleEditor(function(panel) {
let UI = panel.UI;
UI.on("editor-added", function(event, editor) {
count++;
if (count == 2) {
// we test against first stylesheet after all are ready
let editor = UI.editors[0];
editor.getSourceEditor().then(runTests.bind(this, UI, editor));
}
});
})
});
content.location = TESTCASE_URI;
}
function run(aChrome, aEditor)
function runTests(UI, editor)
{
testEnabledToggle(aChrome, aEditor);
testEnabledToggle(UI, editor);
}
function testEnabledToggle(aChrome, aEditor)
function testEnabledToggle(UI, editor)
{
is(aEditor, aChrome.editors[0],
"stylesheet with index 0 is the first stylesheet listed in the UI");
let summary = editor.summary;
let enabledToggle = summary.querySelector(".stylesheet-enabled");
ok(enabledToggle, "enabled toggle button exists");
let firstStyleSheetEditor = aEditor;
let firstStyleSheetUI = aChrome.getSummaryElementForEditor(aEditor);
let enabledToggle = firstStyleSheetUI.querySelector(".stylesheet-enabled");
is(firstStyleSheetEditor.contentDocument.styleSheets[0].disabled, false,
is(editor.styleSheet.disabled, false,
"first stylesheet is initially enabled");
is(firstStyleSheetEditor.hasFlag("disabled"), false,
"first stylesheet is initially enabled, it does not have DISABLED flag");
is(firstStyleSheetUI.classList.contains("disabled"), false,
is(summary.classList.contains("disabled"), false,
"first stylesheet is initially enabled, UI does not have DISABLED class");
let disabledToggleCount = 0;
firstStyleSheetEditor.addActionListener({
onFlagChange: function (aEditor, aFlagName) {
if (aFlagName != "disabled") {
return;
}
disabledToggleCount++;
if (disabledToggleCount == 1) {
is(firstStyleSheetEditor, aEditor,
"FlagChange handler triggered for DISABLED flag on the first editor");
is(firstStyleSheetEditor.styleSheet.disabled, true,
"first stylesheet is now disabled");
is(firstStyleSheetEditor.hasFlag("disabled"), true,
"first stylesheet is now disabled, it has DISABLED flag");
is(firstStyleSheetUI.classList.contains("disabled"), true,
"first stylesheet is now disabled, UI has DISABLED class");
// now toggle it back to enabled
waitForFocus(function () {
EventUtils.synthesizeMouseAtCenter(enabledToggle, {}, gChromeWindow);
}, gChromeWindow);
return;
}
// disabledToggleCount == 2
is(firstStyleSheetEditor, aEditor,
"FlagChange handler triggered for DISABLED flag on the first editor (2)");
is(firstStyleSheetEditor.styleSheet.disabled, false,
"first stylesheet is now enabled again");
is(firstStyleSheetEditor.hasFlag("disabled"), false,
"first stylesheet is now enabled again, it does not have DISABLED flag");
is(firstStyleSheetUI.classList.contains("disabled"), false,
"first stylesheet is now enabled again, UI does not have DISABLED class");
finish();
editor.on("property-change", function(event, property) {
if (property != "disabled") {
return;
}
disabledToggleCount++;
if (disabledToggleCount == 1) {
is(editor.styleSheet.disabled, true, "first stylesheet is now disabled");
is(summary.classList.contains("disabled"), true,
"first stylesheet is now disabled, UI has DISABLED class");
// now toggle it back to enabled
waitForFocus(function () {
EventUtils.synthesizeMouseAtCenter(enabledToggle, {}, gPanelWindow);
}, gPanelWindow);
return;
}
// disabledToggleCount == 2
is(editor.styleSheet.disabled, false, "first stylesheet is now enabled again");
is(summary.classList.contains("disabled"), false,
"first stylesheet is now enabled again, UI does not have DISABLED class");
finish();
});
waitForFocus(function () {
EventUtils.synthesizeMouseAtCenter(enabledToggle, {}, gChromeWindow);
}, gChromeWindow);
EventUtils.synthesizeMouseAtCenter(enabledToggle, {}, gPanelWindow);
}, gPanelWindow);
}

View File

@ -21,39 +21,28 @@ function test()
copy(TESTCASE_URI_HTML, "simple.html", function(htmlFile) {
copy(TESTCASE_URI_CSS, "simple.css", function(cssFile) {
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
aChrome.addChromeListener({
onEditorAdded: function (aChrome, aEditor) {
if (aEditor.styleSheetIndex != 0) {
return; // we want to test against the first stylesheet
}
if (aEditor.sourceEditor) {
run(aEditor); // already attached to input element
} else {
aEditor.addActionListener({
onAttach: run
});
}
addTabAndOpenStyleEditor(function(panel) {
let UI = panel.UI;
UI.on("editor-added", function(event, editor) {
if (editor.styleSheet.styleSheetIndex != 0) {
return; // we want to test against the first stylesheet
}
});
let editor = UI.editors[0];
editor.getSourceEditor().then(runTests.bind(this, editor));
})
});
let uri = Services.io.newFileURI(htmlFile);
let filePath = uri.resolve("");
content.location = filePath;
});
});
}
function run(aEditor)
function runTests(editor)
{
aEditor.saveToFile(null, function (aFile) {
ok(aFile, "file should get saved directly when using a file:// URI");
gChromeWindow.close();
editor.saveToFile(null, function (file) {
ok(file, "file should get saved directly when using a file:// URI");
finish();
});
}

View File

@ -13,27 +13,21 @@ const FILENAME = "styleeditor-import-test.css";
const SOURCE = "body{background:red;}";
let gUI;
function test()
{
waitForExplicitFinish();
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
aChrome.addChromeListener({
onEditorAdded: testEditorAdded
});
run(aChrome);
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", testEditorAdded);
});
content.location = TESTCASE_URI;
}
function run(aChrome)
{
is(aChrome.editors.length, 2,
"there is 2 stylesheets initially");
}
function testImport(aChrome, aEditor)
function testImport()
{
// create file to import first
let file = FileUtils.getFile("ProfD", [FILENAME]);
@ -46,42 +40,37 @@ function testImport(aChrome, aEditor)
FileUtils.closeSafeFileOutputStream(ostream);
// click the import button now that the file to import is ready
aChrome._mockImportFile = file;
gUI._mockImportFile = file;
waitForFocus(function () {
let document = gChromeWindow.document
let document = gPanelWindow.document
let importButton = document.querySelector(".style-editor-importButton");
EventUtils.synthesizeMouseAtCenter(importButton, {}, gChromeWindow);
}, gChromeWindow);
ok(importButton, "import button exists");
EventUtils.synthesizeMouseAtCenter(importButton, {}, gPanelWindow);
}, gPanelWindow);
});
}
let gAddedCount = 0;
function testEditorAdded(aChrome, aEditor)
function testEditorAdded(aEvent, aEditor)
{
if (++gAddedCount == 2) {
// test import after the 2 initial stylesheets have been loaded
if (!aChrome.editors[0].sourceEditor) {
aChrome.editors[0].addActionListener({
onAttach: function () {
testImport(aChrome);
}
});
} else {
testImport(aChrome);
}
gUI.editors[0].getSourceEditor().then(function() {
testImport();
});
}
if (!aEditor.hasFlag("imported")) {
if (!aEditor.savedFile) {
return;
}
ok(!aEditor.hasFlag("inline"),
"imported stylesheet does not have INLINE flag");
ok(aEditor.savedFile,
is(aEditor.savedFile.leafName, FILENAME,
"imported stylesheet will be saved directly into the same file");
is(aEditor.getFriendlyName(), FILENAME,
is(aEditor.friendlyName, FILENAME,
"imported stylesheet has the same name as the filename");
gUI = null;
finish();
}

View File

@ -5,30 +5,39 @@
// http rather than chrome to improve coverage
const TESTCASE_URI = TEST_BASE_HTTP + "import.html";
let gUI;
function test()
{
waitForExplicitFinish();
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
run(aChrome);
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", onEditorAdded);
});
content.location = TESTCASE_URI;
}
function run(aChrome)
let gAddedCount = 0;
function onEditorAdded()
{
is(aChrome.editors.length, 3,
if (++gAddedCount != 3) {
return;
}
is(gUI.editors.length, 3,
"there are 3 stylesheets after loading @imports");
is(aChrome.editors[0]._styleSheet.href, TEST_BASE_HTTP + "simple.css",
is(gUI.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",
is(gUI.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",
is(gUI.editors[2].styleSheet.href, TEST_BASE_HTTP + "import2.css",
"stylesheet 3 is import2.css");
gUI = null;
finish();
}

View File

@ -4,71 +4,52 @@
const TESTCASE_URI = TEST_BASE + "simple.html";
let gUI;
function test()
{
waitForExplicitFinish();
launchStyleEditorChrome(function(aChrome) {
aChrome.addChromeListener({
onEditorAdded: testEditorAdded
});
run(aChrome);
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", testEditorAdded);
});
content.location = TESTCASE_URI;
}
function run(aChrome)
{
is(aChrome.contentWindow.document.readyState, "complete",
"content document is complete");
let SEC = gChromeWindow.styleEditorChrome;
is(SEC, aChrome, "StyleEditorChrome object exists as new window property");
// check editors are instantiated
is(SEC.editors.length, 2,
"there is two StyleEditor instances managed");
ok(SEC.editors[0].styleSheetIndex < SEC.editors[1].styleSheetIndex,
"editors are ordered by styleSheetIndex");
}
let gEditorAddedCount = 0;
function testEditorAdded(aChrome, aEditor)
function testEditorAdded(aEvent, aEditor)
{
if (aEditor.styleSheetIndex == 0) {
if (aEditor.styleSheet.styleSheetIndex == 0) {
gEditorAddedCount++;
testFirstStyleSheetEditor(aChrome, aEditor);
testFirstStyleSheetEditor(aEditor);
}
if (aEditor.styleSheetIndex == 1) {
if (aEditor.styleSheet.styleSheetIndex == 1) {
gEditorAddedCount++;
testSecondStyleSheetEditor(aChrome, aEditor);
testSecondStyleSheetEditor(aEditor);
}
if (gEditorAddedCount == 2) {
gUI = null;
finish();
}
}
function testFirstStyleSheetEditor(aChrome, aEditor)
function testFirstStyleSheetEditor(aEditor)
{
// Note: the html <link> contains charset="UTF-8".
ok(aEditor._state.text.indexOf("\u263a") >= 0,
"stylesheet is unicode-aware.");
//testing TESTCASE's simple.css stylesheet
is(aEditor.styleSheetIndex, 0,
is(aEditor.styleSheet.styleSheetIndex, 0,
"first stylesheet is at index 0");
is(aEditor, aChrome.editors[0],
is(aEditor, gUI.editors[0],
"first stylesheet corresponds to StyleEditorChrome.editors[0]");
ok(!aEditor.hasFlag("inline"),
"first stylesheet does not have INLINE flag");
let summary = aChrome.getSummaryElementForEditor(aEditor);
ok(!summary.classList.contains("inline"),
"first stylesheet UI does not have INLINE class");
let summary = aEditor.summary;
let name = summary.querySelector(".stylesheet-name > label").getAttribute("value");
is(name, "simple.css",
@ -82,21 +63,16 @@ function testFirstStyleSheetEditor(aChrome, aEditor)
"first stylesheet UI is focused/active");
}
function testSecondStyleSheetEditor(aChrome, aEditor)
function testSecondStyleSheetEditor(aEditor)
{
//testing TESTCASE's inline stylesheet
is(aEditor.styleSheetIndex, 1,
is(aEditor.styleSheet.styleSheetIndex, 1,
"second stylesheet is at index 1");
is(aEditor, aChrome.editors[1],
is(aEditor, gUI.editors[1],
"second stylesheet corresponds to StyleEditorChrome.editors[1]");
ok(aEditor.hasFlag("inline"),
"second stylesheet has INLINE flag");
let summary = aChrome.getSummaryElementForEditor(aEditor);
ok(summary.classList.contains("inline"),
"second stylesheet UI has INLINE class");
let summary = aEditor.summary;
let name = summary.querySelector(".stylesheet-name > label").getAttribute("value");
ok(/^<.*>$/.test(name),

View File

@ -9,29 +9,31 @@ function test()
{
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
// launch Style Editor right when the tab is created (before load)
// this checks that the Style Editor still launches correctly when it is opened
// *while* the page is still loading. The Style Editor should not signal that
// it is loaded until the accompanying content page is loaded.
launchStyleEditorChrome(function (aChrome) {
addTabAndOpenStyleEditor(function(panel) {
panel.UI.on("editor-added", testEditorAdded);
content.location = TESTCASE_URI;
is(aChrome.contentWindow.document.readyState, "complete",
"content document is complete");
let root = gChromeWindow.document.querySelector(".splitview-root");
ok(!root.classList.contains("loading"),
"style editor root element does not have 'loading' class name anymore");
let button = gChromeWindow.document.querySelector(".style-editor-newButton");
ok(!button.hasAttribute("disabled"),
"new style sheet button is enabled");
button = gChromeWindow.document.querySelector(".style-editor-importButton");
ok(!button.hasAttribute("disabled"),
"import button is enabled");
finish();
});
}
function testEditorAdded(event, editor)
{
let root = gPanelWindow.document.querySelector(".splitview-root");
ok(!root.classList.contains("loading"),
"style editor root element does not have 'loading' class name anymore");
let button = gPanelWindow.document.querySelector(".style-editor-newButton");
ok(!button.hasAttribute("disabled"),
"new style sheet button is enabled");
button = gPanelWindow.document.querySelector(".style-editor-importButton");
ok(!button.hasAttribute("disabled"),
"import button is enabled");
finish();
}

View File

@ -4,166 +4,121 @@
const TESTCASE_URI = TEST_BASE + "simple.html";
const TRANSITION_CLASS = "moz-styleeditor-transitioning";
const TESTCASE_CSS_SOURCE = "body{background-color:red;";
let TRANSITION_CLASS = "moz-styleeditor-transitioning";
let TESTCASE_CSS_SOURCE = "body{background-color:red;";
let gUI;
function test()
{
waitForExplicitFinish();
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
aChrome.addChromeListener({
onEditorAdded: testEditorAdded
});
run(aChrome);
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", testEditorAdded);
});
content.location = TESTCASE_URI;
}
function run(aChrome)
{
is(aChrome.editors.length, 2,
"there is 2 stylesheets initially");
}
let gAddedCount = 0; // to add new stylesheet after the 2 initial stylesheets
let gNewEditor; // to make sure only one new stylesheet got created
let gUpdateCount = 0; // to make sure only one Update event is triggered
let gCommitCount = 0; // to make sure only one Commit event is triggered
let gTransitionEndCount = 0;
let gOriginalStyleSheet;
let gOriginalOwnerNode;
let gOriginalHref;
function finishOnTransitionEndAndCommit() {
if (gCommitCount && gTransitionEndCount) {
is(gUpdateCount, 1, "received one Update event");
is(gCommitCount, 1, "received one Commit event");
is(gTransitionEndCount, 1, "received one transitionend event");
if (gNewEditor) {
is(gNewEditor.styleSheet, gOriginalStyleSheet,
"style sheet object did not change");
is(gNewEditor.styleSheet.ownerNode, gOriginalOwnerNode,
"style sheet owner node did not change");
is(gNewEditor.styleSheet.href, gOriginalHref,
"style sheet href did not change");
gNewEditor = null;
finish();
}
}
}
function testEditorAdded(aChrome, aEditor)
function testEditorAdded(aEvent, aEditor)
{
gAddedCount++;
if (gAddedCount == 2) {
waitForFocus(function () { // create a new style sheet
let newButton = gChromeWindow.document.querySelector(".style-editor-newButton");
EventUtils.synthesizeMouseAtCenter(newButton, {}, gChromeWindow);
}, gChromeWindow);
waitForFocus(function () {// create a new style sheet
let newButton = gPanelWindow.document.querySelector(".style-editor-newButton");
ok(newButton, "'new' button exists");
EventUtils.synthesizeMouseAtCenter(newButton, {}, gPanelWindow);
}, gPanelWindow);
}
if (gAddedCount != 3) {
if (gAddedCount < 3) {
return;
}
ok(!gNewEditor, "creating a new stylesheet triggers one EditorAdded event");
gNewEditor = aEditor; // above test will fail if we get a duplicate event
is(aChrome.editors.length, 3,
is(gUI.editors.length, 3,
"creating a new stylesheet added a new StyleEditor instance");
let listener = {
onAttach: function (aEditor) {
waitForFocus(function () {
gOriginalStyleSheet = aEditor.styleSheet;
gOriginalOwnerNode = aEditor.styleSheet.ownerNode;
gOriginalHref = aEditor.styleSheet.href;
aEditor.getSourceEditor().then(testEditor);
ok(aEditor.isLoaded,
"new editor is loaded when attached");
ok(aEditor.hasFlag("new"),
"new editor has NEW flag");
ok(aEditor.hasFlag("unsaved"),
"new editor has UNSAVED flag");
aEditor.styleSheet.once("style-applied", function() {
// when changes have been completely applied to live stylesheet after transisiton
let summary = aEditor.summary;
let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
is(parseInt(ruleCount), 1,
"new editor shows 1 rule after modification");
ok(aEditor.inputElement,
"new editor has an input element attached");
ok(!content.document.documentElement.classList.contains(TRANSITION_CLASS),
"StyleEditor's transition class has been removed from content");
});
}
ok(aEditor.sourceEditor.hasFocus(),
"new editor has focus");
function testEditor(aEditor) {
waitForFocus(function () {
gOriginalHref = aEditor.styleSheet.href;
let summary = aChrome.getSummaryElementForEditor(aEditor);
let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
is(parseInt(ruleCount), 0,
"new editor initially shows 0 rules");
let summary = aEditor.summary;
let computedStyle = content.getComputedStyle(content.document.body, null);
is(computedStyle.backgroundColor, "rgb(255, 255, 255)",
"content's background color is initially white");
ok(aEditor.sourceLoaded,
"new editor is loaded when attached");
ok(aEditor.isNew,
"new editor has isNew flag");
EventUtils.synthesizeKey("[", {accelKey: true}, gChromeWindow);
is(aEditor.sourceEditor.getText(), "",
"Nothing happened as it is a known shortcut in source editor");
ok(aEditor.sourceEditor.hasFocus(),
"new editor has focus");
EventUtils.synthesizeKey("]", {accelKey: true}, gChromeWindow);
is(aEditor.sourceEditor.getText(), "",
"Nothing happened as it is a known shortcut in source editor");
let summary = aEditor.summary;
let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
is(parseInt(ruleCount), 0,
"new editor initially shows 0 rules");
for each (let c in TESTCASE_CSS_SOURCE) {
EventUtils.synthesizeKey(c, {}, gChromeWindow);
}
let computedStyle = content.getComputedStyle(content.document.body, null);
is(computedStyle.backgroundColor, "rgb(255, 255, 255)",
"content's background color is initially white");
is(aEditor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
"rule bracket has been auto-closed");
EventUtils.synthesizeKey("[", {accelKey: true}, gPanelWindow);
is(aEditor.sourceEditor.getText(), "",
"Nothing happened as it is a known shortcut in source editor");
// we know that the testcase above will start a CSS transition
content.addEventListener("transitionend", function () {
gTransitionEndCount++;
EventUtils.synthesizeKey("]", {accelKey: true}, gPanelWindow);
is(aEditor.sourceEditor.getText(), "",
"Nothing happened as it is a known shortcut in source editor");
let computedStyle = content.getComputedStyle(content.document.body, null);
is(computedStyle.backgroundColor, "rgb(255, 0, 0)",
"content's background color has been updated to red");
for each (let c in TESTCASE_CSS_SOURCE) {
EventUtils.synthesizeKey(c, {}, gPanelWindow);
}
executeSoon(finishOnTransitionEndAndCommit);
}, false);
}, gChromeWindow) ;
},
is(aEditor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
"rule bracket has been auto-closed");
onUpdate: function (aEditor) {
gUpdateCount++;
ok(aEditor.unsaved,
"new editor has unsaved flag");
ok(content.document.documentElement.classList.contains(TRANSITION_CLASS),
"StyleEditor's transition class has been added to content");
},
// we know that the testcase above will start a CSS transition
content.addEventListener("transitionend", onTransitionEnd, false);
}, gPanelWindow) ;
}
onCommit: function (aEditor) {
gCommitCount++;
function onTransitionEnd() {
content.removeEventListener("transitionend", onTransitionEnd, false);
ok(aEditor.hasFlag("new"),
"new editor still has NEW flag");
ok(aEditor.hasFlag("unsaved"),
"new editor has UNSAVED flag after modification");
let computedStyle = content.getComputedStyle(content.document.body, null);
is(computedStyle.backgroundColor, "rgb(255, 0, 0)",
"content's background color has been updated to red");
let summary = aChrome.getSummaryElementForEditor(aEditor);
let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
is(parseInt(ruleCount), 1,
"new editor shows 1 rule after modification");
if (gNewEditor) {
is(gNewEditor.styleSheet.href, gOriginalHref,
"style sheet href did not change");
ok(!content.document.documentElement.classList.contains(TRANSITION_CLASS),
"StyleEditor's transition class has been removed from content");
aEditor.removeActionListener(listener);
executeSoon(finishOnTransitionEndAndCommit);
}
};
aEditor.addActionListener(listener);
if (aEditor.sourceEditor) {
listener.onAttach(aEditor);
gNewEditor = null;
gUI = null;
finish();
}
}

View File

@ -1,58 +0,0 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TESTCASE_URI = TEST_BASE + "simple.html";
const LINE = 6;
const COL = 2;
function test()
{
let editor = null;
let sheet = null;
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
run();
}, true);
content.location = TESTCASE_URI;
function run()
{
sheet = content.document.styleSheets[1];
launchStyleEditorChrome(function attachListeners(aChrome) {
aChrome.addChromeListener({
onEditorAdded: checkSourceEditor
});
}, sheet, LINE, COL);
}
function checkSourceEditor(aChrome, aEditor)
{
aChrome.removeChromeListener(this);
if (!aEditor.sourceEditor) {
aEditor.addActionListener({
onAttach: function (aEditor) {
aEditor.removeActionListener(this);
validate(aEditor);
}
});
} else {
validate(aEditor);
}
}
function validate(aEditor)
{
info("validating style editor");
let sourceEditor = aEditor.sourceEditor;
let caretPosition = sourceEditor.getCaretPosition();
is(caretPosition.line, LINE - 1, "caret row is correct"); // index based
is(caretPosition.col, COL - 1, "caret column is correct");
is(aEditor.styleSheet, sheet, "loaded stylesheet matches document stylesheet");
finish();
}
}

View File

@ -4,22 +4,18 @@
const TESTCASE_URI = TEST_BASE + "minified.html";
let gUI;
function test()
{
waitForExplicitFinish();
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
aChrome.addChromeListener({
onEditorAdded: function (aChrome, aEditor) {
if (aEditor.sourceEditor) {
run(aEditor); // already attached to input element
} else {
aEditor.addActionListener({
onAttach: run
});
}
}
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", function(event, editor) {
editor.getSourceEditor().then(function() {
testEditor(editor);
});
});
});
@ -27,21 +23,20 @@ function test()
}
let editorTestedCount = 0;
function run(aEditor)
function testEditor(aEditor)
{
if (aEditor.styleSheetIndex == 0) {
if (aEditor.styleSheet.styleSheetIndex == 0) {
let prettifiedSource = "body\{\r?\n\tbackground\:white;\r?\n\}\r?\n\r?\ndiv\{\r?\n\tfont\-size\:4em;\r?\n\tcolor\:red\r?\n\}\r?\n";
let prettifiedSourceRE = new RegExp(prettifiedSource);
ok(prettifiedSourceRE.test(aEditor.sourceEditor.getText()),
"minified source has been prettified automatically");
editorTestedCount++;
let chrome = gChromeWindow.styleEditorChrome;
let summary = chrome.getSummaryElementForEditor(chrome.editors[1]);
EventUtils.synthesizeMouseAtCenter(summary, {}, gChromeWindow);
let summary = gUI.editors[1].summary;
EventUtils.synthesizeMouseAtCenter(summary, {}, gPanelWindow);
}
if (aEditor.styleSheetIndex == 1) {
if (aEditor.styleSheet.styleSheetIndex == 1) {
let originalSource = "body \{ background\: red; \}\r?\ndiv \{\r?\nfont\-size\: 5em;\r?\ncolor\: red\r?\n\}";
let originalSourceRE = new RegExp(originalSource);
@ -51,6 +46,7 @@ function run(aEditor)
}
if (editorTestedCount == 2) {
gUI = null;
finish();
}
}

View File

@ -4,6 +4,9 @@
// This test makes sure that the style editor does not store any
// content CSS files in the permanent cache when opened from PB mode.
let gUI;
function test() {
waitForExplicitFinish();
let windowsToClose = [];
@ -11,6 +14,8 @@ function test() {
function checkCache() {
checkDiskCacheFor(TEST_HOST);
gUI = null;
finish();
}
@ -18,23 +23,21 @@ function test() {
aWindow.gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
aWindow.gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
cache.evictEntries(Ci.nsICache.STORE_ANYWHERE);
launchStyleEditorChromeFromWindow(aWindow, function(aChrome) {
onEditorAdded(aChrome, aChrome.editors[0]);
openStyleEditorInWindow(aWindow, function(panel) {
gUI = panel.UI;
gUI.on("editor-added", onEditorAdded);
});
}, true);
aWindow.gBrowser.selectedBrowser.loadURI(testURI);
}
function onEditorAdded(aChrome, aEditor) {
aChrome.removeChromeListener(this);
if (aEditor.isLoaded) {
function onEditorAdded(aEvent, aEditor) {
if (aEditor.sourceLoaded) {
checkCache();
} else {
aEditor.addActionListener({
onLoad: checkCache
});
}
else {
aEditor.on("source-load", checkCache);
}
}

View File

@ -1,74 +0,0 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TESTCASE_URI = TEST_BASE + "simple.html";
let gEditorAddedCount = 0;
let gEditorReadOnlyCount = 0;
let gChromeListener = {
onEditorAdded: function (aChrome, aEditor) {
gEditorAddedCount++;
if (aEditor.readOnly) {
gEditorReadOnlyCount++;
}
if (gEditorAddedCount == aChrome.editors.length) {
// continue testing after all editors are ready
is(gEditorReadOnlyCount, 0,
"all editors are NOT read-only initially");
// all editors have been loaded, queue closing the content tab
executeSoon(function () {
gBrowser.removeCurrentTab();
});
}
},
onContentDetach: function (aChrome) {
// check that the UI has switched to read-only
run(aChrome);
}
};
function test()
{
waitForExplicitFinish();
gBrowser.addTab(); // because we'll close the next one
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
aChrome.addChromeListener(gChromeListener);
});
content.location = TESTCASE_URI;
}
function run(aChrome)
{
let document = gChromeWindow.document;
let disabledCount;
let elements;
disabledCount = 0;
elements = document.querySelectorAll("button,toolbarbutton,textbox");
for (let i = 0; i < elements.length; ++i) {
if (elements[i].hasAttribute("disabled")) {
disabledCount++;
}
}
ok(elements.length && disabledCount == elements.length,
"all buttons, input and select elements are disabled");
disabledCount = 0;
aChrome.editors.forEach(function (aEditor) {
if (aEditor.readOnly) {
disabledCount++;
}
});
ok(aChrome.editors.length && disabledCount == aChrome.editors.length,
"all editors are read-only");
aChrome.removeChromeListener(gChromeListener);
finish();
}

View File

@ -1,165 +0,0 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let tempScope = {};
Cu.import("resource:///modules/devtools/Target.jsm", tempScope);
let TargetFactory = tempScope.TargetFactory;
function test() {
// http rather than chrome to improve coverage
const TESTCASE_URI = TEST_BASE_HTTP + "simple.gz.html";
const Cc = Components.classes;
const Ci = Components.interfaces;
let toolbox;
let tempScope = {};
Components.utils.import("resource://gre/modules/FileUtils.jsm", tempScope);
let FileUtils = tempScope.FileUtils;
waitForExplicitFinish();
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
let target = TargetFactory.forTab(gBrowser.selectedTab);
toolbox = gDevTools.getToolbox(target);
aChrome.addChromeListener({
onEditorAdded: function (aChrome, aEditor) {
if (aEditor.styleSheetIndex != 0) {
return; // we want to test against the first stylesheet
}
if (aEditor.sourceEditor) {
run(aEditor); // already attached to input element
} else {
aEditor.addActionListener({
onAttach: run
});
}
}
});
toolbox.once("destroyed", function onClose() {
gChromeWindow = null;
executeSoon(function () {
waitForFocus(function () {
// wait that browser has focus again
// open StyleEditorChrome again (a new one since we closed the previous one)
launchStyleEditorChrome(function (aChrome) {
is(gChromeWindow.document.documentElement.hasAttribute("data-marker"),
false,
"opened a completely new StyleEditorChrome window");
aChrome.addChromeListener({
onEditorAdded: function (aChrome, aEditor) {
if (aEditor.styleSheetIndex != 0) {
return; // we want to test against the first stylesheet
}
if (aEditor.sourceEditor) {
testNewChrome(aEditor); // already attached to input element
} else {
aEditor.addActionListener({
onAttach: testNewChrome
});
}
}
});
});
});
});
}, true);
});
content.location = TESTCASE_URI;
let gFilename;
function run(aEditor)
{
gFilename = FileUtils.getFile("ProfD", ["styleeditor-test.css"])
aEditor.saveToFile(gFilename, function (aFile) {
ok(aFile, "file got saved successfully");
aEditor.addActionListener({
onFlagChange: function (aEditor, aFlag) {
if (aFlag != "unsaved") {
return;
}
ok(aEditor.hasFlag("unsaved"),
"first stylesheet has UNSAVED flag after making a change");
// marker used to check it does not exist when we reopen
// ie. the window we opened is indeed a new one
gChromeWindow.document.documentElement.setAttribute("data-marker", "true");
toolbox.destroy();
}
});
waitForFocus(function () {
// insert char so that this stylesheet has the UNSAVED flag
EventUtils.synthesizeKey("x", {}, gChromeWindow);
}, gChromeWindow);
});
}
function testNewChrome(aEditor)
{
ok(aEditor.savedFile,
"first stylesheet editor will save directly into the same file");
is(aEditor.getFriendlyName(), gFilename.leafName,
"first stylesheet still has the filename as it was saved");
gFilename = null;
ok(aEditor.hasFlag("unsaved"),
"first stylesheet still has UNSAVED flag at reopening");
ok(!aEditor.hasFlag("inline"),
"first stylesheet does not have INLINE flag");
ok(!aEditor.hasFlag("error"),
"editor does not have error flag initially");
let hadError = false;
let onSaveCallback = function (aFile) {
aEditor.addActionListener({
onFlagChange: function (aEditor, aFlag) {
if (!hadError && aFlag == "error") {
ok(aEditor.hasFlag("error"),
"editor has ERROR flag after attempting to save with invalid path");
hadError = true;
// save using source editor key binding (previous successful path)
waitForFocus(function () {
EventUtils.synthesizeKey("S", {accelKey: true}, gChromeWindow);
}, gChromeWindow);
return;
}
if (hadError && aFlag == "unsaved") {
executeSoon(function () {
ok(!aEditor.hasFlag("unsaved"),
"first stylesheet has no UNSAVED flag after successful save");
ok(!aEditor.hasFlag("error"),
"ERROR flag has been removed since last operation succeeded");
finish();
});
}
}
});
}
let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
if (os == "WINNT") {
aEditor.saveToFile("C:\\I_DO_NOT_EXIST_42\\bogus.css", onSaveCallback);
} else {
aEditor.saveToFile("/I_DO_NOT_EXIST_42/bogos.css", onSaveCallback);
}
}
}

View File

@ -4,65 +4,68 @@
const TESTCASE_URI = TEST_BASE + "four.html";
let gUI;
function test()
{
waitForExplicitFinish();
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
run(aChrome);
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", function(event, editor) {
if (editor == gUI.editors[3]) {
runTests();
}
});
});
content.location = TESTCASE_URI;
}
let gChrome;
function run(aChrome)
function runTests()
{
gChrome = aChrome;
aChrome.editors[0].addActionListener({onAttach: onEditor0Attach});
aChrome.editors[2].addActionListener({onAttach: onEditor2Attach});
gUI.editors[0].getSourceEditor().then(onEditor0Attach);
gUI.editors[2].getSourceEditor().then(onEditor2Attach);
}
function getStylesheetNameLinkFor(aEditor)
{
return gChrome.getSummaryElementForEditor(aEditor).querySelector(".stylesheet-name");
return aEditor.summary.querySelector(".stylesheet-name");
}
function onEditor0Attach(aEditor)
{
waitForFocus(function () {
let summary = gChrome.getSummaryElementForEditor(aEditor);
EventUtils.synthesizeMouseAtCenter(summary, {}, gChromeWindow);
let summary = aEditor.summary;
EventUtils.synthesizeMouseAtCenter(summary, {}, gPanelWindow);
let item = getStylesheetNameLinkFor(gChrome.editors[0]);
is(gChromeWindow.document.activeElement, item,
let item = getStylesheetNameLinkFor(gUI.editors[0]);
is(gPanelWindow.document.activeElement, item,
"editor 0 item is the active element");
EventUtils.synthesizeKey("VK_DOWN", {}, gChromeWindow);
item = getStylesheetNameLinkFor(gChrome.editors[1]);
is(gChromeWindow.document.activeElement, item,
EventUtils.synthesizeKey("VK_DOWN", {}, gPanelWindow);
item = getStylesheetNameLinkFor(gUI.editors[1]);
is(gPanelWindow.document.activeElement, item,
"editor 1 item is the active element");
EventUtils.synthesizeKey("VK_HOME", {}, gChromeWindow);
item = getStylesheetNameLinkFor(gChrome.editors[0]);
is(gChromeWindow.document.activeElement, item,
EventUtils.synthesizeKey("VK_HOME", {}, gPanelWindow);
item = getStylesheetNameLinkFor(gUI.editors[0]);
is(gPanelWindow.document.activeElement, item,
"fist editor item is the active element");
EventUtils.synthesizeKey("VK_END", {}, gChromeWindow);
item = getStylesheetNameLinkFor(gChrome.editors[3]);
is(gChromeWindow.document.activeElement, item,
EventUtils.synthesizeKey("VK_END", {}, gPanelWindow);
item = getStylesheetNameLinkFor(gUI.editors[3]);
is(gPanelWindow.document.activeElement, item,
"last editor item is the active element");
EventUtils.synthesizeKey("VK_UP", {}, gChromeWindow);
item = getStylesheetNameLinkFor(gChrome.editors[2]);
is(gChromeWindow.document.activeElement, item,
EventUtils.synthesizeKey("VK_UP", {}, gPanelWindow);
item = getStylesheetNameLinkFor(gUI.editors[2]);
is(gPanelWindow.document.activeElement, item,
"editor 2 item is the active element");
EventUtils.synthesizeKey("VK_RETURN", {}, gChromeWindow);
EventUtils.synthesizeKey("VK_RETURN", {}, gPanelWindow);
// this will attach and give focus editor 2
}, gChromeWindow);
}, gPanelWindow);
}
function onEditor2Attach(aEditor)
@ -70,6 +73,6 @@ function onEditor2Attach(aEditor)
ok(aEditor.sourceEditor.hasFocus(),
"editor 2 has focus");
gChrome = null;
gUI = null;
finish();
}

View File

@ -4,53 +4,58 @@
const TESTCASE_URI = TEST_BASE + "simple.html";
let gOriginalWidth; // these are set by run() when gChromeWindow is ready
let gOriginalWidth; // these are set by runTests()
let gOriginalHeight;
function test()
{
waitForExplicitFinish();
addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
run(aChrome);
addTabAndOpenStyleEditor(function(panel) {
let UI = panel.UI;
UI.on("editor-added", function(event, editor) {
if (editor == UI.editors[1]) {
// wait until both editors are added
runTests(UI);
}
});
});
content.location = TESTCASE_URI;
}
function run(aChrome)
function runTests(aUI)
{
is(aChrome.editors.length, 2,
is(aUI.editors.length, 2,
"there is 2 stylesheets initially");
aChrome.editors[0].addActionListener({
onAttach: function onEditorAttached(aEditor) {
executeSoon(function () {
waitForFocus(function () {
// queue a resize to inverse aspect ratio
// this will trigger a detach and reattach (to workaround bug 254144)
let originalSourceEditor = aEditor.sourceEditor;
aEditor.sourceEditor.setCaretOffset(4); // to check the caret is preserved
aUI.editors[0].getSourceEditor().then(function onEditorAttached(aEditor) {
executeSoon(function () {
waitForFocus(function () {
// queue a resize to inverse aspect ratio
// this will trigger a detach and reattach (to workaround bug 254144)
let originalSourceEditor = aEditor.sourceEditor;
aEditor.sourceEditor.setCaretOffset(4); // to check the caret is preserved
gOriginalWidth = gChromeWindow.outerWidth;
gOriginalHeight = gChromeWindow.outerHeight;
gChromeWindow.resizeTo(120, 480);
gOriginalWidth = gPanelWindow.outerWidth;
gOriginalHeight = gPanelWindow.outerHeight;
gPanelWindow.resizeTo(120, 480);
executeSoon(function () {
is(aEditor.sourceEditor, originalSourceEditor,
"the editor still references the same SourceEditor instance");
is(aEditor.sourceEditor.getCaretOffset(), 4,
"the caret position has been preserved");
executeSoon(function () {
is(aEditor.sourceEditor, originalSourceEditor,
"the editor still references the same SourceEditor instance");
is(aEditor.sourceEditor.getCaretOffset(), 4,
"the caret position has been preserved");
// queue a resize to original aspect ratio
waitForFocus(function () {
gChromeWindow.resizeTo(gOriginalWidth, gOriginalHeight);
executeSoon(function () {
finish();
});
}, gChromeWindow);
});
}, gChromeWindow);
});
}
// queue a resize to original aspect ratio
waitForFocus(function () {
gPanelWindow.resizeTo(gOriginalWidth, gOriginalHeight);
executeSoon(function () {
finish();
});
}, gPanelWindow);
});
}, gPanelWindow);
});
});
}

View File

@ -12,7 +12,7 @@ let TargetFactory = tempScope.TargetFactory;
Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
let console = tempScope.console;
let gChromeWindow; //StyleEditorChrome window
let gPanelWindow;
let cache = Cc["@mozilla.org/network/cache-service;1"]
.getService(Ci.nsICacheService);
@ -23,12 +23,38 @@ Services.scriptloader.loadSubScript(testDir + "/helpers.js", this);
function cleanup()
{
gChromeWindow = null;
gPanelWindow = null;
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
}
function addTabAndOpenStyleEditor(callback) {
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
openStyleEditorInWindow(window, callback);
}, true);
}
function openStyleEditorInWindow(win, callback) {
let target = TargetFactory.forTab(win.gBrowser.selectedTab);
win.gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
let panel = toolbox.getCurrentPanel();
gPanelWindow = panel._panelWin;
panel.UI._alwaysDisableAnimations = true;
/*
if (aSheet) {
panel.selectStyleSheet(aSheet, aLine, aCol);
} */
callback(panel);
});
}
/*
function launchStyleEditorChrome(aCallback, aSheet, aLine, aCol)
{
launchStyleEditorChromeFromWindow(window, aCallback, aSheet, aLine, aCol);
@ -39,12 +65,12 @@ function launchStyleEditorChromeFromWindow(aWindow, aCallback, aSheet, aLine, aC
let target = TargetFactory.forTab(aWindow.gBrowser.selectedTab);
gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
let panel = toolbox.getCurrentPanel();
gChromeWindow = panel._panelWin;
gChromeWindow.styleEditorChrome._alwaysDisableAnimations = true;
gPanelWindow = panel._panelWin;
gPanelWindow.styleEditorChrome._alwaysDisableAnimations = true;
if (aSheet) {
panel.selectStyleSheet(aSheet, aLine, aCol);
}
aCallback(gChromeWindow.styleEditorChrome);
aCallback(gPanelWindow.styleEditorChrome);
});
}
@ -56,6 +82,7 @@ function addTabAndLaunchStyleEditorChromeWhenLoaded(aCallback, aSheet, aLine, aC
launchStyleEditorChrome(aCallback, aSheet, aLine, aCol);
}, true);
}
*/
function checkDiskCacheFor(host)
{

View File

@ -20,7 +20,7 @@
<body>
Time passes:
<script>
for (i = 0; i < 30000; i++) {
for (i = 0; i < 5000; i++) {
document.write("<br>...");
}
</script>

View File

@ -84,22 +84,10 @@ function testInlineStyleSheet()
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
let panel = toolbox.getCurrentPanel();
let win = panel._panelWin;
win.styleEditorChrome.addChromeListener({
onEditorAdded: function checkEditor(aChrome, aEditor) {
if (!aEditor.sourceEditor) {
aEditor.addActionListener({
onAttach: function (aEditor) {
aEditor.removeActionListener(this);
validateStyleEditorSheet(aEditor);
}
});
} else {
validateStyleEditorSheet(aEditor);
}
}
});
panel.UI.on("editor-added", (event, editor) => {
validateStyleEditorSheet(editor);
})
});
let link = getLinkByIndex(1);
@ -111,7 +99,7 @@ function validateStyleEditorSheet(aEditor)
info("validating style editor stylesheet");
let sheet = doc.styleSheets[0];
is(aEditor.styleSheet, sheet, "loaded stylesheet matches document stylesheet");
is(aEditor.styleSheet.href, sheet.href, "loaded stylesheet matches document stylesheet");
finishUp();
}

View File

@ -88,8 +88,10 @@ function testInlineStyleSheet()
info("clicking an inline stylesheet");
toolbox.once("styleeditor-ready", function(id, aToolbox) {
aToolbox.panelWindow.styleEditorChrome.addChromeListener({
onEditorAdded: validateStyleEditorSheet
let panel = toolbox.getCurrentPanel();
panel.UI.on("editor-added", (event, editor) => {
validateStyleEditorSheet(editor);
});
});
@ -98,13 +100,13 @@ function testInlineStyleSheet()
link.click();
}
function validateStyleEditorSheet(aChrome, aEditor)
function validateStyleEditorSheet(aEditor)
{
info("validating style editor stylesheet");
let sheet = doc.styleSheets[0];
is(aEditor.styleSheet, sheet, "loaded stylesheet matches document stylesheet");
is(aEditor.styleSheet.href, sheet.href, "loaded stylesheet matches document stylesheet");
win.close();
finishup();

View File

@ -340,20 +340,14 @@ WebConsole.prototype = {
viewSourceInStyleEditor:
function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine)
{
let styleSheets = {};
if (this.target.isLocalTab) {
styleSheets = this.target.window.document.styleSheets;
}
for each (let style in styleSheets) {
if (style.href == aSourceURL) {
gDevTools.showToolbox(this.target, "styleeditor").then(function(toolbox) {
toolbox.getCurrentPanel().selectStyleSheet(style, aSourceLine);
});
return;
gDevTools.showToolbox(this.target, "styleeditor").then(function(toolbox) {
try {
toolbox.getCurrentPanel().selectStyleSheet(aSourceURL, aSourceLine);
} catch(e) {
// Open view source if style editor fails.
this.viewSource(aSourceURL, aSourceLine);
}
}
// Open view source if style editor fails.
this.viewSource(aSourceURL, aSourceLine);
});
},
/**

View File

@ -7,7 +7,7 @@
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test" +
"/test-bug-782653-css-errors.html";
let nodes, hud, SEC;
let nodes, hud, StyleEditorUI;
function test()
{
@ -23,7 +23,7 @@ function testViewSource(aHud)
hud = aHud;
registerCleanupFunction(function() {
nodes = hud = SEC = null;
nodes = hud = StyleEditorUI = null;
});
let selector = ".webconsole-msg-cssparser .webconsole-location";
@ -41,7 +41,16 @@ function testViewSource(aHud)
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = gDevTools.getToolbox(target);
toolbox.once("styleeditor-selected", onStyleEditorReady);
toolbox.once("styleeditor-selected", (event, panel) => {
StyleEditorUI = panel.UI;
let count = 0;
StyleEditorUI.on("editor-added", function() {
if (++count == 2) {
onStyleEditorReady(panel);
}
});
});
EventUtils.sendMouseEvent({ type: "click" }, nodes[0]);
},
@ -49,45 +58,30 @@ function testViewSource(aHud)
});
}
function onStyleEditorReady(aEvent, aPanel)
function onStyleEditorReady(aPanel)
{
info(aEvent + " event fired");
SEC = aPanel.styleEditorChrome;
let win = aPanel.panelWindow;
ok(win, "Style Editor Window is defined");
ok(SEC, "Style Editor Chrome is defined");
function sheetForNode(aNode)
{
let href = aNode.getAttribute("title");
let sheet, i = 0;
while((sheet = content.document.styleSheets[i++])) {
if (sheet.href == href) {
return sheet;
}
}
return null;
}
ok(StyleEditorUI, "Style Editor UI is defined");
waitForFocus(function() {
info("style editor window focused");
let sheet = sheetForNode(nodes[0]);
ok(sheet, "sheet found");
let href = nodes[0].getAttribute("title");
ok(href.contains("test-bug-782653-css-errors-1.css"), "got first stylesheet href")
let line = nodes[0].sourceLine;
ok(line, "found source line");
is(line, 8, "found source line");
checkStyleEditorForSheetAndLine(sheet, line - 1, function() {
checkStyleEditorForSheetAndLine(href, line - 1, function() {
info("first check done");
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = gDevTools.getToolbox(target);
let sheet = sheetForNode(nodes[1]);
ok(sheet, "sheet found");
let href = nodes[1].getAttribute("title");
ok(href.contains("test-bug-782653-css-errors-2.css"), "got second stylesheet href")
let line = nodes[1].sourceLine;
ok(line, "found source line");
is(line, 7, "found source line");
toolbox.selectTool("webconsole").then(function() {
info("webconsole selected");
@ -95,7 +89,7 @@ function onStyleEditorReady(aEvent, aPanel)
toolbox.once("styleeditor-selected", function(aEvent) {
info(aEvent + " event fired");
checkStyleEditorForSheetAndLine(sheet, line - 1, function() {
checkStyleEditorForSheetAndLine(href, line - 1, function() {
info("second check done");
finishTest();
});
@ -107,15 +101,15 @@ function onStyleEditorReady(aEvent, aPanel)
}, win);
}
function checkStyleEditorForSheetAndLine(aStyleSheet, aLine, aCallback)
function checkStyleEditorForSheetAndLine(aHref, aLine, aCallback)
{
let foundEditor = null;
waitForSuccess({
name: "style editor for stylesheet",
validatorFn: function()
{
for (let editor of SEC.editors) {
if (editor.styleSheet == aStyleSheet) {
for (let editor of StyleEditorUI.editors) {
if (editor.styleSheet.href == aHref) {
foundEditor = editor;
return true;
}
@ -136,7 +130,7 @@ function performLineCheck(aEditor, aLine, aCallback)
{
is(aEditor.sourceEditor.getCaretPosition().line, aLine,
"correct line is selected");
is(SEC.selectedStyleSheetIndex, aEditor.styleSheetIndex,
is(StyleEditorUI.selectedStyleSheetIndex, aEditor.styleSheet.styleSheetIndex,
"correct stylesheet is selected in the editor");
aCallback && executeSoon(aCallback);
@ -150,7 +144,8 @@ function performLineCheck(aEditor, aLine, aCallback)
},
successFn: checkForCorrectState,
failureFn: function() {
info("selectedStyleSheetIndex " + SEC.selectedStyleSheetIndex + " expected " + aEditor.styleSheetIndex);
info("selectedStyleSheetIndex " + StyleEditorUI.selectedStyleSheetIndex
+ " expected " + aEditor.styleSheet.styleSheetIndex);
finishTest();
},
});

View File

@ -182,7 +182,8 @@ const UnsolicitedNotifications = {
"tabDetached": "tabDetached",
"tabNavigated": "tabNavigated",
"pageError": "pageError",
"webappsEvent": "webappsEvent"
"webappsEvent": "webappsEvent",
"styleSheetsAdded": "styleSheetsAdded"
};
/**

View File

@ -195,6 +195,9 @@ var DebuggerServer = {
#endif
if ("nsIProfiler" in Ci)
this.addActors("chrome://global/content/devtools/dbg-profiler-actors.js");
this.addActors("chrome://global/content/devtools/dbg-styleeditor-actors.js");
this.addTabActor(this.StyleEditorActor, "styleEditorActor");
},
/**

View File

@ -9,3 +9,4 @@ toolkit.jar:
content/global/devtools/dbg-browser-actors.js (debugger/server/dbg-browser-actors.js)
content/global/devtools/dbg-webconsole-actors.js (webconsole/dbg-webconsole-actors.js)
content/global/devtools/dbg-profiler-actors.js (debugger/server/dbg-profiler-actors.js)
content/global/devtools/dbg-styleeditor-actors.js (styleeditor/dbg-styleeditor-actors.js)

View File

@ -8,4 +8,5 @@ PARALLEL_DIRS += [
'debugger',
'sourcemap',
'webconsole',
'styleeditor'
]

View File

@ -0,0 +1,19 @@
# 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/.
DEPTH = ../../..
topsrcdir = @top_srcdir@
srcdir = @srcdir@
VPATH = @srcdir@
include $(DEPTH)/config/autoconf.mk
#ifneq (Android,$(OS_TARGET))
# TEST_DIRS += test
#endif
include $(topsrcdir)/config/rules.mk
#libs::
# $(INSTALL) $(IFLAGS1) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools

View File

@ -0,0 +1,745 @@
/* 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/. */
"use strict";
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
let TRANSITION_CLASS = "moz-styleeditor-transitioning";
let TRANSITION_DURATION_MS = 500;
let TRANSITION_RULE = "\
:root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\
transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \
transition-delay: 0ms !important;\
transition-timing-function: ease-out !important;\
transition-property: all !important;\
}";
let LOAD_ERROR = "error-load";
/**
* Creates a StyleEditorActor. StyleEditorActor provides remote access to the
* built-in style editor module.
*/
function StyleEditorActor(aConnection, aParentActor)
{
this.conn = aConnection;
this._onDocumentLoaded = this._onDocumentLoaded.bind(this);
this._onSheetLoaded = this._onSheetLoaded.bind(this);
if (aParentActor instanceof BrowserTabActor &&
aParentActor.browser instanceof Ci.nsIDOMWindow) {
this._window = aParentActor.browser;
}
else if (aParentActor instanceof BrowserTabActor &&
aParentActor.browser instanceof Ci.nsIDOMElement) {
this._window = aParentActor.browser.contentWindow;
}
else {
this._window = Services.wm.getMostRecentWindow("navigator:browser");
}
// keep a map of sheets-to-actors so we don't create two actors for one sheet
this._sheets = new Map();
this._actorPool = new ActorPool(this.conn);
this.conn.addActorPool(this._actorPool);
}
StyleEditorActor.prototype = {
/**
* Actor pool for all of the actors we send to the client.
*/
_actorPool: null,
/**
* The debugger server connection instance.
*/
conn: null,
/**
* The content window we work with.
*/
get win() this._window,
/**
* The current content document of the window we work with.
*/
get doc() this._window.document,
/**
* A window object, usually the browser window
*/
_window: null,
actorPrefix: "styleEditor",
form: function()
{
return { actor: this.actorID };
},
/**
* Destroy the current StyleEditorActor instance.
*/
disconnect: function()
{
if (this._observer) {
this._observer.disconnect();
delete this._observer;
}
this._sheets.clear();
this.conn.removeActorPool(this._actorPool);
this._actorPool = null;
this.conn = this._window = null;
},
/**
* Release an actor from our actor pool.
*/
releaseActor: function(actor)
{
if (this._actorPool) {
this._actorPool.removeActor(actor.actorID);
}
},
/**
* Get the BaseURI for the document.
*
* @return {object} JSON message to with BaseURI
*/
onGetBaseURI: function() {
return { baseURI: this.doc.baseURIObject };
},
/**
* Called when target navigates to a new document.
* Adds load listeners to document.
*/
onNewDocument: function() {
// delete previous document's actors
this._clearStyleSheetActors();
// Note: listening for load won't be necessary once
// https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed
if (this.doc.readyState == "complete") {
this._onDocumentLoaded();
}
else {
this.win.addEventListener("load", this._onDocumentLoaded, false);
}
return {};
},
/**
* Event handler for document loaded event.
*/
_onDocumentLoaded: function(event) {
if (event) {
this.win.removeEventListener("load", this._onDocumentLoaded, false);
}
let styleSheets = [];
if (this.doc.styleSheets.length) {
this._addStyleSheets(this.doc.styleSheets);
}
},
/**
* Clear all the current stylesheet actors in map.
*/
_clearStyleSheetActors: function() {
for (let actor in this._sheets) {
this.releaseActor(this._sheets[actor]);
}
this._sheets.clear();
},
/**
* Get the actors of all the stylesheets in the current document.
*
* @return {object} JSON message with the stylesheet actors' forms
*/
onGetStyleSheets: function() {
let styleSheets = [];
for (let i = 0; i < this.doc.styleSheets.length; ++i) {
let styleSheet = this.doc.styleSheets[i];
let actor = this._createStyleSheetActor(styleSheet);
styleSheets.push(actor.form());
}
return { "styleSheets": styleSheets };
},
/**
* Add all the stylesheets to the map and create an actor
* for each one if not already created. Send event that there
* are new stylesheets.
*
* @param {[DOMStyleSheet]} styleSheets
* Stylesheets to add
*/
_addStyleSheets: function(styleSheets)
{
let sheets = [];
for (let i = 0; i < styleSheets.length; i++) {
let styleSheet = styleSheets[i];
sheets.push(styleSheet);
// Get all sheets, including imported ones
let imports = this._getImported(styleSheet);
sheets = sheets.concat(imports);
}
let actors = sheets.map((sheet) => {
let actor = this._createStyleSheetActor(sheet);
return actor.form();
});
this._notifyStyleSheetsAdded(actors);
},
/**
* Send an event notifying that there are new style sheets
*
* @param {[object]} actors
* Forms of the new style sheet actors
*/
_notifyStyleSheetsAdded: function(actors)
{
this.conn.send({
from: this.actorID,
type: "styleSheetsAdded",
styleSheets: actors
});
},
/**
* Get all the stylesheets @imported from a stylesheet.
*
* @param {DOMStyleSheet} styleSheet
* Style sheet to search
* @return {array}
* All the imported stylesheets
*/
_getImported: function(styleSheet) {
let imported = [];
for (let i = 0; i < styleSheet.cssRules.length; i++) {
let rule = styleSheet.cssRules[i];
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;
}
imported.push(rule.styleSheet);
// recurse imports in this stylesheet as well
imported = imported.concat(this._getImported(rule.styleSheet));
}
else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
// @import rules must precede all others except @charset
break;
}
}
return imported;
},
/**
* Create a new actor for a style sheet, if it hasn't
* already been created, and return it.
*
* @param {DOMStyleSheet} aStyleSheet
* The style sheet to create an actor for.
* @return {StyleSheetActor}
* The actor for this style sheet
*/
_createStyleSheetActor: function(aStyleSheet)
{
if (this._sheets.has(aStyleSheet)) {
return this._sheets.get(aStyleSheet);
}
let actor = new StyleSheetActor(aStyleSheet, this);
this._actorPool.addActor(actor);
this._sheets.set(aStyleSheet, actor);
return actor;
},
/**
* Handler for style sheet loading event. Add
* a new actor for the sheet and notify.
*
* @param {Event} event
*/
_onSheetLoaded: function(event) {
let style = event.target;
style.removeEventListener("load", this._onSheetLoaded, false);
let actor = this._createStyleSheetActor(style.sheet);
this._notifyStyleSheetsAdded([actor.form()]);
},
/**
* Create a new style sheet in the document with the given text.
* Return an actor for it.
*
* @param {object} request
* Debugging protocol request object, with 'text property'
* @return {object}
* Object with 'styelSheet' property for form on new actor.
*/
onNewStyleSheet: function(request) {
let parent = this.doc.documentElement;
let style = this.doc.createElementNS("http://www.w3.org/1999/xhtml", "style");
style.setAttribute("type", "text/css");
if (request.text) {
style.appendChild(this.doc.createTextNode(request.text));
}
parent.appendChild(style);
let actor = this._createStyleSheetActor(style.sheet);
return { styleSheet: actor.form() };
}
};
/**
* The request types this actor can handle.
*/
StyleEditorActor.prototype.requestTypes = {
"getStyleSheets": StyleEditorActor.prototype.onGetStyleSheets,
"newStyleSheet": StyleEditorActor.prototype.onNewStyleSheet,
"getBaseURI": StyleEditorActor.prototype.onGetBaseURI,
"newDocument": StyleEditorActor.prototype.onNewDocument
};
function StyleSheetActor(aStyleSheet, aParentActor) {
this.styleSheet = aStyleSheet;
this.parentActor = aParentActor;
// text and index are unknown until source load
this.text = null;
this._styleSheetIndex = -1;
this._transitionRefCount = 0;
this._onSourceLoad = this._onSourceLoad.bind(this);
this._notifyError = this._notifyError.bind(this);
// if this sheet has an @import, then it's rules are loaded async
let ownerNode = this.styleSheet.ownerNode;
if (ownerNode) {
let onSheetLoaded = function(event) {
ownerNode.removeEventListener("load", onSheetLoaded, false);
this._notifyPropertyChanged("ruleCount");
}.bind(this);
ownerNode.addEventListener("load", onSheetLoaded, false);
}
}
StyleSheetActor.prototype = {
actorPrefix: "stylesheet",
toString: function() {
return "[StyleSheetActor " + this.actorID + "]";
},
disconnect: function() {
this.parentActor.releaseActor(this);
},
/**
* Window of target
*/
get win() {
return this.parentActor._window;
},
/**
* Document of target.
*/
get doc() {
return this.win.document;
},
/**
* Retrieve the index (order) of stylesheet in the document.
*
* @return number
*/
get styleSheetIndex()
{
if (this._styleSheetIndex == -1) {
for (let i = 0; i < this.doc.styleSheets.length; i++) {
if (this.doc.styleSheets[i] == this.styleSheet) {
this._styleSheetIndex = i;
break;
}
}
}
return this._styleSheetIndex;
},
/**
* Get the current state of the actor
*
* @return {object}
* With properties of the underlying stylesheet, plus 'text',
* 'styleSheetIndex' and 'parentActor' if it's @imported
*/
form: function() {
let form = {
actor: this.actorID, // actorID is set when this actor is added to a pool
href: this.styleSheet.href,
disabled: this.styleSheet.disabled,
title: this.styleSheet.title,
styleSheetIndex: this.styleSheetIndex,
text: this.text
}
// get parent actor if this sheet was @imported
let parent = this.styleSheet.parentStyleSheet;
if (parent) {
form.parentActor = this.parentActor._sheets.get(parent);
}
try {
form.ruleCount = this.styleSheet.cssRules.length;
}
catch(e) {
// stylesheet had an @import rule that wasn't loaded yet
}
return form;
},
/**
* Toggle the disabled property of the style sheet
*
* @return {object}
* 'disabled' - the disabled state after toggling.
*/
onToggleDisabled: function() {
this.styleSheet.disabled = !this.styleSheet.disabled;
this._notifyPropertyChanged("disabled");
return { disabled: this.styleSheet.disabled };
},
/**
* Send an event notifying that a property of the stylesheet
* has changed.
*
* @param {string} property
* Name of the changed property
*/
_notifyPropertyChanged: function(property) {
this.conn.send({
from: this.actorID,
type: "propertyChange-" + this.actorID,
property: property,
value: this.form()[property]
})
},
/**
* Send an event notifying that an error has occured
*
* @param {string} message
* Error message
*/
_notifyError: function(message) {
this.conn.send({
from: this.actorID,
type: "error-" + this.actorID,
errorMessage: message
});
},
/**
* Handler for event when the style sheet's full text has been
* loaded from its source.
*
* @param {string} source
* Text of the style sheet
* @param {[type]} charset
* Optional charset of the source
*/
_onSourceLoad: function(source, charset) {
this.text = this._decodeCSSCharset(source, charset || "");
this.conn.send({
from: this.actorID,
type: "sourceLoad-" + this.actorID,
source: this.text
});
},
/**
* Fetch the source of the style sheet from its URL
*/
onFetchSource: function() {
if (!this.styleSheet.href) {
// this is an inline <style> sheet
let source = this.styleSheet.ownerNode.textContent;
this._onSourceLoad(source);
return {};
}
let scheme = Services.io.extractScheme(this.styleSheet.href);
switch (scheme) {
case "file":
this._styleSheetFilePath = this.styleSheet.href;
case "chrome":
case "resource":
this._loadSourceFromFile(this.styleSheet.href);
break;
default:
this._loadSourceFromCache(this.styleSheet.href);
break;
}
return {};
},
/**
* Decode a CSS source string to unicode according to the character set rules
* defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>.
*
* @param string string
* Source of a CSS stylesheet, loaded from file or cache.
* @param string channelCharset
* Charset of the source string if set by the HTTP channel.
* @return string
* The CSS string, in unicode.
*/
_decodeCSSCharset: function(string, channelCharset)
{
// StyleSheet's charset can be specified from multiple sources
if (channelCharset.length > 0) {
// step 1 of syndata.html: charset given in HTTP header.
return this._convertToUnicode(string, channelCharset);
}
let sheet = this.styleSheet;
if (sheet) {
// Do we have a @charset rule in the stylesheet?
// step 2 of syndata.html (without the BOM check).
if (sheet.cssRules) {
let rules = sheet.cssRules;
if (rules.length
&& rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
return this._convertToUnicode(string, rules.item(0).encoding);
}
}
// step 3: charset attribute of <link> or <style> element, if it exists
if (sheet.ownerNode && sheet.ownerNode.getAttribute) {
let linkCharset = sheet.ownerNode.getAttribute("charset");
if (linkCharset != null) {
return this._convertToUnicode(string, linkCharset);
}
}
// step 4 (1 of 2): charset of referring stylesheet.
let parentSheet = sheet.parentStyleSheet;
if (parentSheet && parentSheet.cssRules &&
parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
return this._convertToUnicode(string,
parentSheet.cssRules[0].encoding);
}
// step 4 (2 of 2): charset of referring document.
if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) {
return this._convertToUnicode(string,
sheet.ownerNode.ownerDocument.characterSet);
}
}
// step 5: default to utf-8.
return this._convertToUnicode(string, "UTF-8");
},
/**
* Convert a given string, encoded in a given character set, to unicode.
*
* @param string string
* A string.
* @param string charset
* A character set.
* @return string
* A unicode string.
*/
_convertToUnicode: function(string, charset) {
// Decoding primitives.
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
try {
converter.charset = charset;
return converter.ConvertToUnicode(string);
} catch(e) {
return string;
}
},
/**
* Load source from a file or file-like resource.
*
* @param string href
* URL for the stylesheet.
*/
_loadSourceFromFile: function(href)
{
try {
NetUtil.asyncFetch(href, (stream, status) => {
if (!Components.isSuccessCode(status)) {
this._notifyError(LOAD_ERROR);
return;
}
let source = NetUtil.readInputStreamToString(stream, stream.available());
stream.close();
this._onSourceLoad(source);
});
} catch (ex) {
this._notifyError(LOAD_ERROR);
}
},
/**
* Load source from the HTTP cache.
*
* @param string href
* URL for the stylesheet.
*/
_loadSourceFromCache: function(href)
{
let channel = Services.io.newChannel(href, null, null);
let chunks = [];
let channelCharset = "";
let streamListener = { // nsIStreamListener inherits nsIRequestObserver
onStartRequest: (aRequest, aContext, aStatusCode) => {
if (!Components.isSuccessCode(aStatusCode)) {
this._notifyError(LOAD_ERROR);
}
},
onDataAvailable: (aRequest, aContext, aStream, aOffset, aCount) => {
let channel = aRequest.QueryInterface(Ci.nsIChannel);
if (!channelCharset) {
channelCharset = channel.contentCharset;
}
chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
},
onStopRequest: (aRequest, aContext, aStatusCode) => {
if (!Components.isSuccessCode(aStatusCode)) {
this._notifyError(LOAD_ERROR);
return;
}
let source = chunks.join("");
this._onSourceLoad(source, channelCharset);
}
};
if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
let loadContext = this.win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsILoadContext);
channel.setPrivate(loadContext.usePrivateBrowsing);
}
channel.loadFlags = channel.LOAD_FROM_CACHE;
channel.asyncOpen(streamListener, null);
},
/**
* Update the style sheet in place with new text
*
* @param {object} request
* 'text' - new text
* 'transition' - whether to do CSS transition for change.
*/
onUpdate: function(request) {
DOMUtils.parseStyleSheet(this.styleSheet, request.text);
if (request.transition) {
this._insertTransistionRule();
}
else {
this._notifyStyleApplied();
}
this._notifyPropertyChanged("ruleCount");
return {};
},
/**
* Insert a catch-all transition rule into the document. Set a timeout
* to remove the rule after a certain time.
*/
_insertTransistionRule: function() {
// Insert the global transition rule
// Use a ref count to make sure we do not add it multiple times.. and remove
// it only when all pending StyleEditor-generated transitions ended.
if (this._transitionRefCount == 0) {
this.styleSheet.insertRule(TRANSITION_RULE, this.styleSheet.cssRules.length);
this.doc.documentElement.classList.add(TRANSITION_CLASS);
}
this._transitionRefCount++;
// Set up clean up and commit after transition duration (+10% buffer)
// @see _onTransitionEnd
this.win.setTimeout(this._onTransitionEnd.bind(this),
Math.floor(TRANSITION_DURATION_MS * 1.1));
},
/**
* This cleans up class and rule added for transition effect and then
* notifies that the style has been applied.
*/
_onTransitionEnd: function()
{
if (--this._transitionRefCount == 0) {
this.doc.documentElement.classList.remove(TRANSITION_CLASS);
this.styleSheet.deleteRule(this.styleSheet.cssRules.length - 1);
}
this._notifyStyleApplied();
},
/**
* Send and event notifying that the new style has been applied fully.
*/
_notifyStyleApplied: function()
{
this.conn.send({
from: this.actorID,
type: "styleApplied-" + this.actorID
})
}
}
StyleSheetActor.prototype.requestTypes = {
"toggleDisabled": StyleSheetActor.prototype.onToggleDisabled,
"fetchSource": StyleSheetActor.prototype.onFetchSource,
"update": StyleSheetActor.prototype.onUpdate
};
XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});

View File

@ -0,0 +1,6 @@
# vim: set filetype=python:
# 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/.
#TEST_DIRS += ['tests']