Merge fx-team to m-c a=merge

This commit is contained in:
Wes Kocher 2015-05-14 15:34:39 -07:00
commit 51fad76ff2
138 changed files with 3642 additions and 1849 deletions

View File

@ -1632,6 +1632,9 @@ pref("browser.newtab.preload", true);
// Remembers if the about:newtab intro has been shown
pref("browser.newtabpage.introShown", false);
// Remembers if the about:newtab update intro has been shown
pref("browser.newtabpage.updateIntroShown", false);
// Toggles the content of 'about:newtab'. Shows the grid when enabled.
pref("browser.newtabpage.enabled", true);
@ -1912,4 +1915,4 @@ pref("browser.pocket.api", "api.getpocket.com");
pref("browser.pocket.site", "getpocket.com");
pref("browser.pocket.oAuthConsumerKey", "40249-e88c401e1b1f2242d9e441c4");
pref("browser.pocket.useLocaleList", true);
pref("browser.pocket.enabledLocales", "en-US de es-ES ja ru");
pref("browser.pocket.enabledLocales", "en-US de es-ES ja ja-JP-mac ru");

View File

@ -114,6 +114,5 @@
<stringbundleset id="stringbundleset">
<stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
<stringbundle id="bundle_browser_region" src="chrome://browser-region/locale/region.properties"/>
<stringbundle id="bundle_pocket" src="chrome://browser/content/browser-pocket.properties"/>
</stringbundleset>
</overlay>

View File

@ -504,6 +504,49 @@ let LoopUI;
this._maybeShowBrowserSharingInfoBar();
}
},
/**
* Fetch the favicon of the currently selected tab in the format of a data-uri.
*
* @param {Function} callback Function to be invoked with an error object as
* its first argument when an error occurred or
* a string as second argument when the favicon
* has been fetched.
*/
getFavicon: function(callback) {
let favicon = gBrowser.getIcon(gBrowser.selectedTab);
// If the tab image's url starts with http(s), fetch icon from favicon
// service via the moz-anno protocol.
if (/^https?:/.test(favicon)) {
let faviconURI = makeURI(favicon);
favicon = this.favIconService.getFaviconLinkForIcon(faviconURI).spec;
}
if (!favicon) {
callback(new Error("No favicon found"));
return;
}
favicon = this.PlacesUtils.getImageURLForResolution(window, favicon);
// We XHR the favicon to get a File object, which we can pass to the FileReader
// object. The FileReader turns the File object into a data-uri.
let xhr = new XMLHttpRequest();
xhr.open("get", favicon, true);
xhr.responseType = "blob";
xhr.overrideMimeType("image/x-icon");
xhr.onload = () => {
if (xhr.status != 200) {
callback(new Error("Invalid status code received for favicon XHR: " + xhr.status));
return;
}
let reader = new FileReader();
reader.onload = () => callback(null, reader.result);
reader.onerror = callback;
reader.readAsDataURL(xhr.response);
};
xhr.onerror = callback;
xhr.send();
}
};
})();
@ -511,3 +554,6 @@ XPCOMUtils.defineLazyModuleGetter(LoopUI, "injectLoopAPI", "resource:///modules/
XPCOMUtils.defineLazyModuleGetter(LoopUI, "LoopRooms", "resource:///modules/loop/LoopRooms.jsm");
XPCOMUtils.defineLazyModuleGetter(LoopUI, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(LoopUI, "PanelFrame", "resource:///modules/PanelFrame.jsm");
XPCOMUtils.defineLazyModuleGetter(LoopUI, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(LoopUI, "favIconService",
"@mozilla.org/browser/favicon-service;1", "nsIFaviconService");

View File

@ -397,6 +397,9 @@
key="manBookmarkKb"/>
<menuseparator id="organizeBookmarksSeparator"/>
<menuitem id="menu_pocket" label="&pocketMenuitem.label;"
#ifndef XP_MACOSX
class="menuitem-iconic"
#endif
oncommand="openUILink(Pocket.listURL, event);"/>
<menuseparator id="menu_pocketSeparator"/>
<menuitem id="menu_bookmarkThisPage"

View File

@ -1571,15 +1571,19 @@ let BookmarkingUI = {
let locale = Cc["@mozilla.org/chrome/chrome-registry;1"].
getService(Ci.nsIXULChromeRegistry).
getSelectedLocale("browser");
let url = "chrome://browser/content/browser-pocket-" + locale + ".properties";
let bundle = Services.strings.createBundle(url);
let item = document.getElementById(prefix + "pocket");
try {
item.setAttribute("label", bundle.GetStringFromName("pocketMenuitem.label"));
} catch (err) {
// GetStringFromName throws when the bundle doesn't exist. In that
// case, the item will retain the browser-pocket.dtd en-US string that
// it has in the markup.
if (locale != "en-US") {
if (locale == "ja-JP-mac")
locale = "ja";
let url = "chrome://browser/content/browser-pocket-" + locale + ".properties";
let bundle = Services.strings.createBundle(url);
let item = document.getElementById(prefix + "pocket");
try {
item.setAttribute("label", bundle.GetStringFromName("pocketMenuitem.label"));
} catch (err) {
// GetStringFromName throws when the bundle doesn't exist. In that
// case, the item will retain the browser-pocket.dtd en-US string that
// it has in the markup.
}
}
}
document.getElementById(prefix + "pocket").hidden = hidden;

View File

@ -773,8 +773,8 @@ window[chromehidden~="toolbar"] toolbar:not(#nav-bar):not(#TabsToolbar):not(#pri
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#click-to-play-plugins-notification");
}
#password-fill-notification {
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#password-fill-notification");
#login-fill-notification {
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#login-fill-notification");
}
.login-fill-item {

View File

@ -783,6 +783,7 @@
<image id="push-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="addons-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="indexedDB-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="login-fill-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="password-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="webapps-notification-icon" class="notification-anchor-icon" role="button"/>
<image id="plugins-notification-icon" class="notification-anchor-icon" role="button"/>

View File

@ -49,9 +49,15 @@ addMessageListener("ContextMenu:DoCustomCommand", function(message) {
PageMenuChild.executeMenu(message.data);
});
addMessageListener("RemoteLogins:fillForm", function(message) {
LoginManagerContent.receiveMessage(message, content);
});
addEventListener("DOMFormHasPassword", function(event) {
LoginManagerContent.onDOMFormHasPassword(event, content);
InsecurePasswordUtils.checkForInsecurePasswords(event.target);
LoginManagerContent.onFormPassword(event);
});
addEventListener("pageshow", function(event) {
LoginManagerContent.onPageShow(event, content);
});
addEventListener("DOMAutoComplete", function(event) {
LoginManagerContent.onUsernameInput(event);

View File

@ -5,53 +5,232 @@
#endif
const PREF_INTRO_SHOWN = "browser.newtabpage.introShown";
const PREF_UPDATE_INTRO_SHOWN = "browser.newtabpage.updateIntroShown";
// These consts indicate the type of intro/onboarding we show.
const WELCOME = "welcome";
const UPDATE = "update";
// The maximum paragraph ID listed for 'newtab.intro.paragraph'
// strings in newTab.properties
const MAX_PARAGRAPH_ID = 9;
const NUM_INTRO_PAGES = 3;
let gIntro = {
_nodeIDSuffixes: [
"panel",
"what",
"mask",
"modal",
"numerical-progress",
"text",
"buttons",
"image",
"header",
"footer"
],
_imageTypes: {
COG : "cog",
PIN_REMOVE : "pin-remove",
SUGGESTED : "suggested"
},
/**
* The paragraphs & buttons to show on each page in the intros.
*
* _introPages.welcome and _introPages.update contain an array of
* indices of paragraphs to be used to lookup text in _paragraphs
* for each page of the intro.
*
* Similarly, _introPages.buttons is used to lookup text for buttons
* on each page of the intro.
*/
_introPages: {
"welcome": [[0,1],[2,3],[4,5]],
"update": [[6,5],[4,3],[0,1]],
"buttons": [["skip", "continue"],["back", "next"],["back", "gotit"]],
"welcome-images": ["cog", "pin-remove", "suggested"],
"update-images": ["suggested", "pin-remove", "cog"]
},
_paragraphs: [],
_nodes: {},
_images: {},
init: function() {
for (let idSuffix of this._nodeIDSuffixes) {
this._nodes[idSuffix] = document.getElementById("newtab-intro-" + idSuffix);
}
this._nodes.panel.addEventListener("popupshowing", e => this._setUpPanel());
this._nodes.panel.addEventListener("popuphidden", e => this._hidePanel());
this._nodes.what.addEventListener("click", e => this.showPanel());
let brand = Services.strings.createBundle("chrome://branding/locale/brand.properties");
this._brandShortName = brand.GetStringFromName("brandShortName");
},
_setImage: function(imageType) {
// Remove previously existing images, if any.
let currImageHolder = this._nodes.image;
while (currImageHolder.firstChild) {
currImageHolder.removeChild(currImageHolder.firstChild);
}
this._nodes.image.appendChild(this._images[imageType]);
},
_goToPage: function(pageNum) {
this._currPage = pageNum;
this._nodes["numerical-progress"].innerHTML = `${this._bold(pageNum + 1)} / ${NUM_INTRO_PAGES}`;
this._nodes["numerical-progress"].setAttribute("page", pageNum);
// Set the page's image
let imageType = this._introPages[this._onboardingType + "-images"][pageNum];
this._setImage(imageType);
// Set the paragraphs
let paragraphNodes = this._nodes.text.getElementsByTagName("p");
let paragraphIDs = this._introPages[this._onboardingType][pageNum];
paragraphIDs.forEach((arg, index) => {
paragraphNodes[index].innerHTML = this._paragraphs[arg];
});
// Set the buttons
let buttonNodes = this._nodes.buttons.getElementsByTagName("input");
let buttonIDs = this._introPages.buttons[pageNum];
buttonIDs.forEach((arg, index) => {
buttonNodes[index].setAttribute("value", newTabString("intro." + arg));
});
},
_bold: function(str) {
return `<strong>${str}</strong>`
},
_link: function(url, text) {
return `<a href="${url}" target="_blank">${text}</a>`;
},
_span: function(text, className) {
return `<span class="${className}">${text}</span>`;
},
_exitIntro: function() {
this._nodes.mask.style.opacity = 0;
this._nodes.mask.addEventListener("transitionend", () => {
this._nodes.mask.style.display = "none";
});
},
_back: function() {
if (this._currPage == 0) {
// We're on the first page so 'back' means exit.
this._exitIntro();
return;
}
this._goToPage(this._currPage - 1);
},
_next: function() {
if (this._currPage == (NUM_INTRO_PAGES - 1)) {
// We're on the last page so 'next' means exit.
this._exitIntro();
return;
}
this._goToPage(this._currPage + 1);
},
_generateImages: function() {
Object.keys(this._imageTypes).forEach(type => {
let image = "";
let imageClass = "";
switch (this._imageTypes[type]) {
case this._imageTypes.COG:
image = document.getElementById("newtab-customize-panel").cloneNode(true);
image.removeAttribute("hidden");
image.removeAttribute("type");
image.classList.add("newtab-intro-image-customize");
break;
case this._imageTypes.PIN_REMOVE:
imageClass = "-hover";
// fall-through
case this._imageTypes.SUGGESTED:
image = document.createElementNS(HTML_NAMESPACE, "div");
image.classList.add("newtab-intro-cell-wrapper");
// Create the cell's inner HTML code.
image.innerHTML =
'<div class="newtab-intro-cell' + imageClass + '">' +
' <div class="newtab-site newtab-intro-image-tile" type="sponsored">' +
' <a class="newtab-link">' +
' <span class="newtab-thumbnail"/>' +
' <span class="newtab-title">Example Title</span>' +
' </a>' +
' <input type="button" class="newtab-control newtab-control-pin"/>' +
' <input type="button" class="newtab-control newtab-control-block"/>' + (imageClass ? "" :
' <span class="newtab-sponsored">' + newTabString("suggested.tag") + '</span>') +
' </div>' +
'</div>';
break;
}
this._images[this._imageTypes[type]] = image;
});
},
_generateParagraphs: function() {
let customizeIcon = '<input type="button" class="newtab-control newtab-customize"/>';
let substringMappings = {
"2": [this._link(TILES_PRIVACY_LINK, newTabString("privacy.link"))],
"4": [customizeIcon, this._bold(newTabString("intro.controls"))],
"6": [this._bold(newTabString("intro.paragraph6.remove")), this._bold(newTabString("intro.paragraph6.pin"))],
"7": [this._link(TILES_INTRO_LINK, newTabString("learn.link"))],
"8": [this._brandShortName, this._link(TILES_INTRO_LINK, newTabString("learn.link"))]
}
for (let i = 1; i <= MAX_PARAGRAPH_ID; i++) {
try {
this._paragraphs.push(newTabString("intro.paragraph" + i, substringMappings[i]));
} catch (ex) {
// Paragraph with this ID doesn't exist so continue
}
}
},
showIfNecessary: function() {
if (!Services.prefs.getBoolPref(PREF_INTRO_SHOWN)) {
Services.prefs.setBoolPref(PREF_INTRO_SHOWN, true);
this._onboardingType = WELCOME;
this.showPanel();
} else if (!Services.prefs.getBoolPref(PREF_UPDATE_INTRO_SHOWN)) {
this._onboardingType = UPDATE;
this.showPanel();
}
Services.prefs.setBoolPref(PREF_INTRO_SHOWN, true);
Services.prefs.setBoolPref(PREF_UPDATE_INTRO_SHOWN, true);
},
showPanel: function() {
// Point the panel at the 'what' link
this._nodes.panel.hidden = false;
this._nodes.panel.openPopup(this._nodes.what);
},
this._nodes.mask.style.display = "block";
this._nodes.mask.style.opacity = 1;
_setUpPanel: function() {
// Build the panel if necessary
if (this._nodes.panel.childNodes.length == 1) {
['<a href="' + TILES_INTRO_LINK + '">' + newTabString("learn.link") + "</a>",
'<a href="' + TILES_PRIVACY_LINK + '">' + newTabString("privacy.link") + "</a>",
'<input type="button" class="newtab-customize"/>',
].forEach((arg, index) => {
let paragraph = document.createElementNS(HTML_NAMESPACE, "p");
this._nodes.panel.appendChild(paragraph);
paragraph.innerHTML = newTabString("intro.paragraph" + (index + 1), [arg]);
});
if (!this._paragraphs.length) {
// It's our first time showing the panel. Do some initial setup
this._generateParagraphs();
this._generateImages();
}
},
this._goToPage(0);
_hidePanel: function() {
this._nodes.panel.hidden = true;
}
// Header text
let boldSubstr = this._onboardingType == WELCOME ? this._span(this._brandShortName, "bold") : "";
this._nodes.header.innerHTML = newTabString("intro.header." + this._onboardingType, [boldSubstr]);
// Footer links
let footerLinkNodes = this._nodes.footer.getElementsByTagName("li");
[this._link(TILES_INTRO_LINK, newTabString("learn.link2")),
this._link(TILES_PRIVACY_LINK, newTabString("privacy.link2")),
].forEach((arg, index) => {
footerLinkNodes[index].innerHTML = arg;
});
},
};

View File

@ -55,6 +55,7 @@ input[type=button] {
position: absolute;
right: 70px;
top: 20px;
display: none;
}
#newtab-intro-what:-moz-locale-dir(rtl) {
@ -157,13 +158,24 @@ input[type=button] {
}
/* CELLS */
.newtab-cell {
.newtab-cell,
.newtab-intro-cell,
.newtab-intro-cell-hover {
display: -moz-box;
height: 210px;
margin: 20px 10px 35px;
width: 290px;
}
.newtab-intro-cell-wrapper {
margin-top: -12px;
}
.newtab-intro-cell,
.newtab-intro-cell-hover {
margin: 0;
}
/* SITES */
.newtab-site {
position: relative;
@ -306,7 +318,8 @@ input[type=button] {
}
.newtab-control:-moz-focusring,
.newtab-cell:not([ignorehover]) > .newtab-site:hover > .newtab-control {
.newtab-cell:not([ignorehover]) > .newtab-site:hover > .newtab-control,
.newtab-intro-cell-hover .newtab-control {
opacity: 1;
}
@ -558,6 +571,11 @@ input[type=button] {
cursor: pointer;
}
.newtab-customize-panel-item,
.newtab-customize-complex-option {
width: 100%;
}
.newtab-customize-panel-item:not([selected]),
.newtab-customize-panel-subitem:not([selected]){
color: #7A7A7A;
@ -572,7 +590,7 @@ input[type=button] {
.selectable:not([selected]):hover {
background: url("chrome://global/skin/menu/shared-menu-check-hover.svg") no-repeat #FFFFFF;
background-size: 16px 16px;
background-position: 15px 20px;
background-position: 15px 15px;
color: #171F26;
}
@ -584,7 +602,7 @@ input[type=button] {
.newtab-search-panel-engine[selected] {
background: url("chrome://global/skin/menu/shared-menu-check-active.svg") no-repeat transparent;
background-size: 16px 16px;
background-position: 15px 20px;
background-position: 15px 15px;
color: black;
font-weight: 600;
}
@ -606,7 +624,7 @@ input[type=button] {
.newtab-customize-panel-subitem {
font-size: 12px;
padding-left: 40px;
padding-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid threedshadow;
}
@ -617,10 +635,244 @@ input[type=button] {
.newtab-customize-panel-superitem {
line-height: 14px;
border-bottom: medium none !important;
padding: 20px 0px 10px 40px;
padding: 15px 0px 10px 40px;
}
.searchSuggestionTable {
font: message-box;
font-size: 16px;
}
/**
* Onboarding styling
*/
#newtab-intro-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #424F5A;
z-index:102;
background-color: rgba(66,79,90,0.95);
transition: opacity .5s linear;
overflow: auto;
display: none;
}
#newtab-intro-modal {
font-family: "Helvetica";
width: 700px;
height: 500px;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
background: linear-gradient(#FFFFFF, #F9F9F9);
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.7);
border-radius: 8px 8px 0px 0px;
}
#newtab-intro-header {
font-size: 28px;
color: #737980;
text-align: center;
top: 50px;
position: relative;
border-bottom: 2px solid #E0DFE0;
padding-bottom: 10px;
width: 600px;
display: block;
margin: 0px auto;
font-weight: 100;
}
#newtab-intro-header .bold {
font-weight: 500;
color: #343F48;
}
#newtab-intro-footer {
width: 100%;
height: 55px;
margin: 0px auto;
display: block;
position: absolute;
bottom: 0px;
background-color: white;
box-shadow: 0 -1px 4px -1px #EBEBEB;
text-align: center;
vertical-align: middle;
line-height: 55px;
}
#newtab-intro-footer > ul {
list-style-type: none;
margin: 0px;
padding: 0px;
}
#newtab-intro-footer > ul > li {
display: inline;
padding-left: 10px;
padding-right: 10px;
}
#newtab-intro-footer > ul > li > a {
text-decoration: none;
color: #4A90E2;
}
#newtab-intro-footer > ul > li > a:visited {
color: #171F26;
}
#newtab-intro-footer > ul > :first-child {
border-right: solid 1px #C1C1C1;
}
#newtab-intro-body {
height: 300px;
position: relative;
display: block;
top: 50px;
margin: 25px 50px 30px;
}
#newtab-intro-content > * {
display: inline-block;
}
#newtab-intro-content {
height: 210px;
position: relative;
}
#newtab-intro-buttons {
height: 90px;
text-align: center;
vertical-align: middle;
line-height: 90px;
}
#newtab-intro-tile {
width: 290px;
height: 100%;
}
#newtab-intro-text,
#newtab-intro-image {
height: 100%;
width: 270px;
right: 0px;
position: absolute;
font-size: 14px;
line-height: 20px;
margin-top: -12px;
}
#newtab-intro-image {
left: 0px;
right: auto;
}
.newtab-intro-image-tile {
}
.newtab-intro-image-customize {
display: block;
box-shadow: 3px 3px 5px #888;
margin: 0 !important;
}
.newtab-intro-image-customize .newtab-customize-panel-item:not([selected]):hover {
background-color: inherit;
color: #7A7A7A;
background: none;
}
#newtab-intro-text > p {
margin: 0 0 1em 0;
}
#newtab-intro-text .newtab-control {
background-size: 18px auto;
height: 18px;
width: 18px;
vertical-align: middle;
opacity: 1;
position: inherit;
}
#newtab-intro-buttons > input {
width: 150px;
height: 50px;
margin: 0px 5px;
vertical-align: bottom;
border-radius: 2px;
border: solid 1px #2C72E2;
background-color: #FFFFFF;
color: #4A90E2;
-moz-user-focus: normal;
}
#newtab-intro-buttons > input[default] {
background-color: #4A90E2;
color: #FFFFFF;
}
#newtab-intro-buttons > input:hover {
background-color: #2C72E2;
color: #FFFFFF;
}
#newtab-intro-progress {
position: absolute;
width: 100%;
}
#newtab-intro-numerical-progress {
text-align: center;
top: 15px;
position: relative;
font-size: 12px;
color: #424F5A;
}
#newtab-intro-graphical-progress {
text-align: left;
border-radius: 1.5px;
overflow: hidden;
position: relative;
margin: 10px auto 0px;
height: 3px;
top: 8px;
width: 35px;
background-color: #DCDCDC;
}
#indicator {
position: absolute;
top: 0px;
left: 0px;
display: inline-block;
width: 0%;
height: 4px;
background: none repeat scroll 0% 0% #FF9500;
transition: width 0.3s ease-in-out 0s;
}
#newtab-intro-numerical-progress[page="0"] + #newtab-intro-graphical-progress > #indicator {
width: 33%;
}
#newtab-intro-numerical-progress[page="1"] + #newtab-intro-graphical-progress > #indicator {
width: 66%;
}
#newtab-intro-numerical-progress[page="2"] + #newtab-intro-graphical-progress > #indicator {
width: 100%;
}

View File

@ -58,6 +58,35 @@
</xul:hbox>
</xul:panel>
<div id="newtab-intro-mask">
<div id="newtab-intro-modal">
<div id="newtab-intro-progress">
<div id="newtab-intro-numerical-progress"/>
<div id="newtab-intro-graphical-progress">
<span id="indicator"/>
</div>
</div>
<div id="newtab-intro-header"/>
<div id="newtab-intro-body">
<div id="newtab-intro-content">
<div id="newtab-intro-image"/>
<div id="newtab-intro-text">
<p/><p/>
</div>
</div>
<div id="newtab-intro-buttons">
<input type="button" onclick="gIntro._back()"/>
<input type="button" default="true" onclick="gIntro._next()"/>
</div>
</div>
<div id="newtab-intro-footer">
<ul>
<li/><li/>
</ul>
</div>
</div>
</div>
<div id="newtab-scrollbox">
<div id="newtab-vertical-margin">

View File

@ -190,16 +190,20 @@ nsContextMenu.prototype = {
let locale = Cc["@mozilla.org/chrome/chrome-registry;1"].
getService(Ci.nsIXULChromeRegistry).
getSelectedLocale("browser");
let url = "chrome://browser/content/browser-pocket-" + locale + ".properties";
let bundle = Services.strings.createBundle(url);
let item = document.getElementById("context-pocket");
try {
item.setAttribute("label", bundle.GetStringFromName("saveToPocketCmd.label"));
item.setAttribute("accesskey", bundle.GetStringFromName("saveToPocketCmd.accesskey"));
} catch (err) {
// GetStringFromName throws when the bundle doesn't exist. In that
// case, the item will retain the browser-pocket.dtd en-US string that
// it has in the markup.
if (locale != "en-US") {
if (locale == "ja-JP-mac")
locale = "ja";
let url = "chrome://browser/content/browser-pocket-" + locale + ".properties";
let bundle = Services.strings.createBundle(url);
let item = document.getElementById("context-pocket");
try {
item.setAttribute("label", bundle.GetStringFromName("saveToPocketCmd.label"));
item.setAttribute("accesskey", bundle.GetStringFromName("saveToPocketCmd.accesskey"));
} catch (err) {
// GetStringFromName throws when the bundle doesn't exist. In that
// case, the item will retain the browser-pocket.dtd en-US string that
// it has in the markup.
}
}
}
}

View File

@ -5621,7 +5621,13 @@
<field name="mOverCloseButton">false</field>
<field name="mCorrespondingMenuitem">null</field>
<!--
While it would make sense to track this in a field, the field will get nuked
once the node is gone from the DOM, which causes us to think the tab is not
closed, which causes us to make wrong decisions. So we use an expando instead.
<field name="closing">false</field>
-->
<method name="_mouseenter">
<body><![CDATA[

View File

@ -406,6 +406,10 @@ skip-if = e10s && debug # Bug 907326
[browser_tabkeynavigation.js]
skip-if = e10s
[browser_tabopen_reflows.js]
[browser_tabs_close_beforeunload.js]
support-files =
close_beforeunload_opens_second_tab.html
close_beforeunload.html
[browser_tabs_isActive.js]
skip-if = e10s # Bug 1100664 - test relies on linkedBrowser.docShell
[browser_tabs_owner.js]

View File

@ -0,0 +1,43 @@
"use strict";
const FIRST_TAB = getRootDirectory(gTestPath) + "close_beforeunload_opens_second_tab.html";
const SECOND_TAB = getRootDirectory(gTestPath) + "close_beforeunload.html";
add_task(function*() {
let firstTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, FIRST_TAB);
let newTabPromise = waitForNewTabEvent(gBrowser);
ContentTask.spawn(firstTab.linkedBrowser, "", function*() {
content.document.getElementsByTagName("a")[0].click();
});
let tabOpenEvent = yield newTabPromise;
let secondTab = tabOpenEvent.target;
yield ContentTask.spawn(secondTab.linkedBrowser, SECOND_TAB, function*(expectedURL) {
if (content.window.location.href == expectedURL &&
content.document.readyState === "complete") {
return Promise.resolve();
}
return new Promise(function(resolve, reject) {
content.window.addEventListener("load", function() {
if (content.window.location.href == expectedURL) {
resolve();
}
}, false);
});
});
let closeBtn = document.getAnonymousElementByAttribute(secondTab, "anonid", "close-button");
let closePromise = BrowserTestUtils.removeTab(secondTab, {dontRemove: true});
info("closing second tab (which will self-close in beforeunload)");
closeBtn.click();
ok(secondTab.closing, "Second tab should be marked as closing synchronously.");
yield closePromise;
ok(secondTab.closing, "Second tab should still be marked as closing");
ok(!secondTab.linkedBrowser, "Second tab's browser should be dead");
ok(!firstTab.closing, "First tab should not be closing");
ok(firstTab.linkedBrowser, "First tab's browser should be alive");
info("closing first tab");
yield BrowserTestUtils.removeTab(firstTab);
ok(firstTab.closing, "First tab should be marked as closing");
ok(!firstTab.linkedBrowser, "First tab's browser should be dead");
});

View File

@ -0,0 +1,8 @@
<body>
<p>I will close myself if you close me.</p>
<script>
window.onbeforeunload = function() {
window.close();
};
</script>
</body>

View File

@ -0,0 +1,3 @@
<body>
<a href="#" onclick="window.open('close_beforeunload.html', '_blank')">Open second tab</a>
</body>

View File

@ -2,50 +2,118 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
const INTRO_PREF = "browser.newtabpage.introShown";
const UPDATE_INTRO_PREF = "browser.newtabpage.updateIntroShown";
const PRELOAD_PREF = "browser.newtab.preload";
function runTests() {
let origIntro = Services.prefs.getBoolPref(INTRO_PREF);
let origUpdateIntro = Services.prefs.getBoolPref(UPDATE_INTRO_PREF);
let origPreload = Services.prefs.getBoolPref(PRELOAD_PREF);
registerCleanupFunction(_ => {
Services.prefs.setBoolPref(INTRO_PREF, origIntro);
Services.prefs.setBoolPref(INTRO_PREF, origUpdateIntro);
Services.prefs.setBoolPref(PRELOAD_PREF, origPreload);
});
// Test with preload false
Services.prefs.setBoolPref(INTRO_PREF, false);
Services.prefs.setBoolPref(UPDATE_INTRO_PREF, false);
Services.prefs.setBoolPref(PRELOAD_PREF, false);
let panel;
function maybeWaitForPanel() {
// If already open, no need to wait
if (panel.state == "open") {
executeSoon(TestRunner.next);
return;
}
// We're expecting the panel to open, so wait for it
panel.addEventListener("popupshown", TestRunner.next);
isnot(panel.state, "open", "intro panel can be slow to show");
}
let intro;
let brand = Services.strings.createBundle("chrome://branding/locale/brand.properties");
let brandShortName = brand.GetStringFromName("brandShortName");
yield addNewTabPageTab();
panel = getContentDocument().getElementById("newtab-intro-panel");
yield maybeWaitForPanel();
is(panel.state, "open", "intro automatically shown on first opening");
intro = getContentDocument().getElementById("newtab-intro-mask");
is(intro.style.opacity, 1, "intro automatically shown on first opening");
is(getContentDocument().getElementById("newtab-intro-header").innerHTML,
'Welcome to New Tab on <span xmlns="http://www.w3.org/1999/xhtml" class="bold">' + brandShortName + '</span>!', "we show the first-run intro.");
is(Services.prefs.getBoolPref(INTRO_PREF), true, "newtab remembers that the intro was shown");
is(Services.prefs.getBoolPref(UPDATE_INTRO_PREF), true, "newtab avoids showing update if intro was shown");
yield addNewTabPageTab();
panel = getContentDocument().getElementById("newtab-intro-panel");
is(panel.state, "closed", "intro not shown on second opening");
intro = getContentDocument().getElementById("newtab-intro-mask");
is(intro.style.opacity, 0, "intro not shown on second opening");
// Test with preload true
Services.prefs.setBoolPref(INTRO_PREF, false);
Services.prefs.setBoolPref(PRELOAD_PREF, true);
yield addNewTabPageTab();
panel = getContentDocument().getElementById("newtab-intro-panel");
yield maybeWaitForPanel();
is(panel.state, "open", "intro automatically shown on preloaded opening");
intro = getContentDocument().getElementById("newtab-intro-mask");
is(intro.style.opacity, 1, "intro automatically shown on preloaded opening");
is(getContentDocument().getElementById("newtab-intro-header").innerHTML,
'Welcome to New Tab on <span xmlns="http://www.w3.org/1999/xhtml" class="bold">' + brandShortName + '</span>!', "we show the first-run intro.");
is(Services.prefs.getBoolPref(INTRO_PREF), true, "newtab remembers that the intro was shown");
is(Services.prefs.getBoolPref(UPDATE_INTRO_PREF), true, "newtab avoids showing update if intro was shown");
// Test with first run true but update false
Services.prefs.setBoolPref(UPDATE_INTRO_PREF, false);
yield addNewTabPageTab();
intro = getContentDocument().getElementById("newtab-intro-mask");
is(intro.style.opacity, 1, "intro automatically shown on preloaded opening");
is(getContentDocument().getElementById("newtab-intro-header").innerHTML,
"New Tab got an update!", "we show the update intro.");
is(Services.prefs.getBoolPref(INTRO_PREF), true, "INTRO_PREF stays true");
is(Services.prefs.getBoolPref(UPDATE_INTRO_PREF), true, "newtab remembers that the update intro was show");
// Test clicking the 'next' and 'back' buttons.
let buttons = getContentDocument().getElementById("newtab-intro-buttons").getElementsByTagName("input");
let progress = getContentDocument().getElementById("newtab-intro-numerical-progress");
let back = buttons[0];
let next = buttons[1];
is(progress.getAttribute("page"), 0, "we are on the first page");
is(intro.style.opacity, 1, "intro visible");
let createMutationObserver = function(fcn) {
return new Promise(resolve => {
let observer = new MutationObserver(function(mutations) {
fcn();
observer.disconnect();
resolve();
});
let config = { attributes: true, attributeFilter: ["style"], childList: true };
observer.observe(progress, config);
});
}
let p = createMutationObserver(function() {
is(progress.getAttribute("page"), 1, "we get to the 2nd page");
is(intro.style.opacity, 1, "intro visible");
});
next.click();
yield p.then(TestRunner.next);
p = createMutationObserver(function() {
is(progress.getAttribute("page"), 2, "we get to the 3rd page");
is(intro.style.opacity, 1, "intro visible");
});
next.click();
yield p.then(TestRunner.next);
p = createMutationObserver(function() {
is(progress.getAttribute("page"), 1, "go back to 2nd page");
is(intro.style.opacity, 1, "intro visible");
});
back.click();
yield p.then(TestRunner.next);
p = createMutationObserver(function() {
is(progress.getAttribute("page"), 0, "go back to 1st page");
is(intro.style.opacity, 1, "intro visible");
});
back.click();
yield p.then(TestRunner.next);
p = createMutationObserver(function() {
is(progress.getAttribute("page"), 0, "another back will 'skip tutorial'");
is(intro.style.opacity, 0, "intro exited");
});
back.click();
p.then(TestRunner.next);
}

View File

@ -27,7 +27,6 @@ support-files =
unchecked.jpg
[browser_aboutHome_activation.js]
skip-if = e10s # Bug 1053965 "cw.ensureSnippetsMapThen is not a function", also see general/browser.ini about:home comments
[browser_addons.js]
skip-if = e10s && debug # Leaking docshells (bug 1150147)
[browser_blocklist.js]

View File

@ -17,7 +17,7 @@ let snippet =
' "name": "Demo Social Service",' +
' "origin": "https://example.com",' +
' "iconURL": "chrome://branding/content/icon16.png",' +
' "icon32URL": "chrome://branding/content/favicon32.png",' +
' "icon32URL": "chrome://branding/content/icon32.png",' +
' "icon64URL": "chrome://branding/content/icon64.png",' +
' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
' "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
@ -29,7 +29,7 @@ let snippet =
' }' +
' </script>' +
' <div id="activationSnippet" onclick="activateProvider(this)">' +
' <img src="chrome://branding/content/favicon32.png"></img>' +
' <img src="chrome://branding/content/icon32.png"></img>' +
' </div>';
// enable one-click activation
@ -39,7 +39,7 @@ let snippet2 =
' "name": "Demo Social Service",' +
' "origin": "https://example.com",' +
' "iconURL": "chrome://branding/content/icon16.png",' +
' "icon32URL": "chrome://branding/content/favicon32.png",' +
' "icon32URL": "chrome://branding/content/icon32.png",' +
' "icon64URL": "chrome://branding/content/icon64.png",' +
' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
' "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
@ -52,18 +52,14 @@ let snippet2 =
' }' +
' </script>' +
' <div id="activationSnippet" onclick="activateProvider(this)">' +
' <img src="chrome://branding/content/favicon32.png"></img>' +
' <img src="chrome://branding/content/icon32.png"></img>' +
' </div>';
let gTests = [
{
desc: "Test activation with enable panel",
setup: function (aSnippetsMap)
{
// This must be some incorrect xhtml code.
aSnippetsMap.set("snippets", snippet);
},
snippet: snippet,
run: function (aSnippetsMap)
{
let deferred = Promise.defer();
@ -81,7 +77,6 @@ let gTests = [
gBrowser.removeTab(gBrowser.selectedTab);
SocialService.uninstallProvider(SocialSidebar.provider.origin, function () {
info("provider uninstalled");
aSnippetsMap.delete("snippets");
deferred.resolve(true);
});
});
@ -91,11 +86,7 @@ let gTests = [
{
desc: "Test activation bypassing enable panel",
setup: function (aSnippetsMap)
{
// This must be some incorrect xhtml code.
aSnippetsMap.set("snippets", snippet2);
},
snippet: snippet2,
run: function (aSnippetsMap)
{
let deferred = Promise.defer();
@ -113,7 +104,6 @@ let gTests = [
gBrowser.removeTab(gBrowser.selectedTab);
SocialService.uninstallProvider(SocialSidebar.provider.origin, function () {
info("provider uninstalled");
aSnippetsMap.delete("snippets");
deferred.resolve(true);
});
});
@ -136,7 +126,7 @@ function test()
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
// Add an event handler to modify the snippets map once it's ready.
let snippetsPromise = promiseSetupSnippetsMap(tab, test.setup);
let snippetsPromise = promiseSetupSnippetsMap(tab, test.snippet);
// Start loading about:home and wait for it to complete.
yield promiseTabLoadEvent(tab, "about:home", "AboutHomeLoadSnippetsCompleted");
@ -196,32 +186,45 @@ function promiseTabLoadEvent(aTab, aURL, aEventType="load")
* The setup function to be run.
* @return {Promise} resolved when the snippets are ready. Gets the snippets map.
*/
function promiseSetupSnippetsMap(aTab, aSetupFn)
function promiseSetupSnippetsMap(aTab, aSnippet)
{
let deferred = Promise.defer();
info("Waiting for snippets map");
aTab.linkedBrowser.addEventListener("AboutHomeLoadSnippets", function load(event) {
aTab.linkedBrowser.removeEventListener("AboutHomeLoadSnippets", load, true);
let cw = aTab.linkedBrowser.contentWindow.wrappedJSObject;
// The snippets should already be ready by this point. Here we're
// just obtaining a reference to the snippets map.
cw.ensureSnippetsMapThen(function (aSnippetsMap) {
aSnippetsMap = Cu.waiveXrays(aSnippetsMap);
info("Got snippets map: " +
"{ last-update: " + aSnippetsMap.get("snippets-last-update") +
", cached-version: " + aSnippetsMap.get("snippets-cached-version") +
" }");
// Don't try to update.
aSnippetsMap.set("snippets-last-update", Date.now());
aSnippetsMap.set("snippets-cached-version", AboutHomeUtils.snippetsVersion);
// Clear snippets.
aSnippetsMap.delete("snippets");
aSetupFn(aSnippetsMap);
deferred.resolve(aSnippetsMap);
});
}, true, true);
return deferred.promise;
return ContentTask.spawn(aTab.linkedBrowser,
{snippetsVersion: AboutHomeUtils.snippetsVersion,
snippet: aSnippet},
function*(arg) {
let obj = {};
Cu.import("resource://gre/modules/Promise.jsm", obj);
let deferred = obj.Promise.defer();
addEventListener("AboutHomeLoadSnippets", function load(event) {
removeEventListener("AboutHomeLoadSnippets", load, true);
let cw = content.window.wrappedJSObject;
// The snippets should already be ready by this point. Here we're
// just obtaining a reference to the snippets map.
cw.ensureSnippetsMapThen(function (aSnippetsMap) {
aSnippetsMap = Cu.waiveXrays(aSnippetsMap);
console.log("Got snippets map: " +
"{ last-update: " + aSnippetsMap.get("snippets-last-update") +
", cached-version: " + aSnippetsMap.get("snippets-cached-version") +
" }");
// Don't try to update.
aSnippetsMap.set("snippets-last-update", Date.now());
aSnippetsMap.set("snippets-cached-version", arg.snippetsVersion);
// Clear snippets.
aSnippetsMap.delete("snippets");
aSnippetsMap.set("snippets", arg.snippet);
deferred.resolve();
});
}, true, true);
return deferred.promise;
});
}
@ -233,7 +236,7 @@ function sendActivationEvent(tab, callback) {
if (doc.defaultView.frames[0])
doc = doc.defaultView.frames[0].document;
let button = doc.getElementById("activationSnippet");
EventUtils.synthesizeMouseAtCenter(button, {}, doc.defaultView);
BrowserTestUtils.synthesizeMouseAtCenter(button, {}, tab.linkedBrowser);
executeSoon(callback);
}

View File

@ -2781,7 +2781,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
which is empty because the actual panel is not implemented inside an XBL
binding, but made of elements added to the notification panel. This
allows accessing the full structure while the panel is hidden. -->
<binding id="password-fill-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
<binding id="login-fill-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
<content>
<children/>
</content>

View File

@ -75,7 +75,7 @@ browser.jar:
* content/browser/browser.css (content/browser.css)
* content/browser/browser.js (content/browser.js)
* content/browser/browser.xul (content/browser.xul)
content/browser/browser-pocket.properties (content/browser-pocket.properties)
content/browser/browser-pocket-en-US.properties (content/browser-pocket-en-US.properties)
content/browser/browser-pocket.dtd (content/browser-pocket.dtd)
content/browser/browser-pocket-de.properties (content/browser-pocket-de.properties)
content/browser/browser-pocket-es-ES.properties (content/browser-pocket-es-ES.properties)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 193 KiB

View File

@ -26,8 +26,8 @@
# settings.
# The dialog units for the bitmap's dimensions should match exactly with the
# bitmap's width and height in pixels.
!define APPNAME_BMP_WIDTH_DU 123u
!define APPNAME_BMP_HEIGHT_DU 56u
!define APPNAME_BMP_WIDTH_DU 108u
!define APPNAME_BMP_HEIGHT_DU 48u
!define INTRO_BLURB_WIDTH_DU "232u"
!define INTRO_BLURB_EDGE_DU "196u"
!define INTRO_BLURB_LTR_TOP_DU "16u"

View File

@ -376,6 +376,7 @@ let CustomizableUIInternal = {
// Before you ask, yes, deleting things inside a let x of y loop where y is a Set is
// safe, and we won't skip any items.
futurePlacedWidgets.delete(widget.id);
gDirty = true;
break;
}
// Otherwise, if we're somewhere other than the beginning, check if the previous
@ -386,6 +387,7 @@ let CustomizableUIInternal = {
if (previousWidgetIndex != -1) {
savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id);
futurePlacedWidgets.delete(widget.id);
gDirty = true;
break;
}
}
@ -394,6 +396,7 @@ let CustomizableUIInternal = {
// with doing nothing else now; if the item remains in gFuturePlacements, we'll
// add it at the end in restoreStateForArea.
}
this.saveState();
},
wrapWidget: function(aWidgetId) {

View File

@ -1080,6 +1080,8 @@ if (Services.prefs.getBoolPref("browser.pocket.enabled")) {
}
if (isEnabledForLocale) {
if (browserLocale == "ja-JP-mac")
browserLocale = "ja";
let url = "chrome://browser/content/browser-pocket-" + browserLocale + ".properties";
let strings = Services.strings.createBundle(url);
let label;
@ -1090,7 +1092,7 @@ if (Services.prefs.getBoolPref("browser.pocket.enabled")) {
} catch (err) {
// GetStringFromName throws when the bundle doesn't exist. In that case,
// fall back to the en-US browser-pocket.properties.
url = "chrome://browser/content/browser-pocket.properties";
url = "chrome://browser/content/browser-pocket-en-US.properties";
strings = Services.strings.createBundle(url);
label = strings.GetStringFromName("pocket-button.label");
tooltiptext = strings.GetStringFromName("pocket-button.tooltiptext");

View File

@ -515,19 +515,11 @@ const PanelUI = {
* @return the selected locale or "en-US" if none is selected
*/
function getLocale() {
const PREF_SELECTED_LOCALE = "general.useragent.locale";
try {
let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE,
Ci.nsIPrefLocalizedString);
if (locale)
return locale;
let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry);
return chromeRegistry.getSelectedLocale("browser");
} catch (ex) {
return "en-US";
}
catch (e) { }
try {
return Services.prefs.getCharPref(PREF_SELECTED_LOCALE);
}
catch (e) { }
return "en-US";
}

View File

@ -229,30 +229,59 @@ body {
-moz-margin-start: 0;
}
.new-room-view > .context > .context-preview {
float: right;
width: 100px;
max-height: 200px;
-moz-margin-start: 10px;
margin-bottom: 10px;
.new-room-view > .context > .checkbox-wrapper {
margin-bottom: .5em;
}
body[dir=rtl] .new-room-view > .context > .context-preview {
.new-room-view > .context > .checkbox-wrapper > .checkbox {
border-color: #0096dd;
background-color: #fff;
}
.new-room-view > .context > .checkbox-wrapper > .checkbox.checked {
background-image: url("../shared/img/check.svg#check-blue");
}
.new-room-view > .context > .checkbox-wrapper > label {
color: #3c3c3c;
font-weight: 700;
}
.new-room-view > .context > .context-content {
border: 1px solid #0096dd;
border-radius: 3px;
background: #fff;
padding: .8em;
display: flex;
flex-flow: row nowrap;
line-height: 1.1em;
}
.new-room-view > .context > .context-content > .context-preview {
float: left;
width: 16px;
max-height: 16px;
-moz-margin-end: .8em;
flex: 0 1 auto;
}
body[dir=rtl] .new-room-view > .context > .context-content > .context-preview {
float: left;
}
.new-room-view > .context > .context-preview[src=""] {
.new-room-view > .context > .context-content > .context-preview[src=""] {
display: none;
}
.new-room-view > .context > .context-description {
.new-room-view > .context > .context-content > .context-description {
flex: 0 1 auto;
display: block;
color: #707070;
}
.new-room-view > .context > .context-url {
.new-room-view > .context > .context-content > .context-description > .context-url {
display: block;
color: #59A1D7;
font-weight: 700;
clear: both;
}

View File

@ -18,6 +18,7 @@ loop.panel = (function(_, mozL10n) {
var sharedUtils = loop.shared.utils;
var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup;
var Checkbox = sharedViews.Checkbox;
var ContactsList = loop.contacts.ContactsList;
var ContactDetailsForm = loop.contacts.ContactDetailsForm;
@ -717,23 +718,27 @@ loop.panel = (function(_, mozL10n) {
onDocumentVisible: function() {
this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) {
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
var description = metadata.description || metadata.title;
var previewImage = metadata.favicon || "";
var description = metadata.title || metadata.description;
var url = metadata.url;
this.setState({previewImage: previewImage,
description: description,
url: url});
this.setState({
previewImage: previewImage,
description: description,
url: url
});
}.bind(this));
},
onDocumentHidden: function() {
this.setState({previewImage: "",
description: "",
url: ""});
this.setState({
previewImage: "",
description: "",
url: ""
});
},
onCheckboxChange: function(event) {
this.setState({checked: event.target.checked});
onCheckboxChange: function(newState) {
this.setState({checked: newState.checked});
},
handleCreateButtonClick: function() {
@ -770,14 +775,15 @@ loop.panel = (function(_, mozL10n) {
return (
React.createElement("div", {className: "new-room-view"},
React.createElement("div", {className: contextClasses},
React.createElement("label", {className: "context-enabled"},
React.createElement("input", {className: "context-checkbox",
type: "checkbox", onChange: this.onCheckboxChange}),
mozL10n.get("context_offer_label")
),
React.createElement("img", {className: "context-preview", src: this.state.previewImage}),
React.createElement("span", {className: "context-description"}, this.state.description),
React.createElement("span", {className: "context-url"}, hostname)
React.createElement(Checkbox, {label: mozL10n.get("context_inroom_label"),
onChange: this.onCheckboxChange}),
React.createElement("div", {className: "context-content"},
React.createElement("img", {className: "context-preview", src: this.state.previewImage}),
React.createElement("span", {className: "context-description"},
this.state.description,
React.createElement("span", {className: "context-url"}, hostname)
)
)
),
React.createElement("button", {className: "btn btn-info new-room-button",
onClick: this.handleCreateButtonClick,

View File

@ -18,6 +18,7 @@ loop.panel = (function(_, mozL10n) {
var sharedUtils = loop.shared.utils;
var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup;
var Checkbox = sharedViews.Checkbox;
var ContactsList = loop.contacts.ContactsList;
var ContactDetailsForm = loop.contacts.ContactDetailsForm;
@ -717,23 +718,27 @@ loop.panel = (function(_, mozL10n) {
onDocumentVisible: function() {
this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) {
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
var description = metadata.description || metadata.title;
var previewImage = metadata.favicon || "";
var description = metadata.title || metadata.description;
var url = metadata.url;
this.setState({previewImage: previewImage,
description: description,
url: url});
this.setState({
previewImage: previewImage,
description: description,
url: url
});
}.bind(this));
},
onDocumentHidden: function() {
this.setState({previewImage: "",
description: "",
url: ""});
this.setState({
previewImage: "",
description: "",
url: ""
});
},
onCheckboxChange: function(event) {
this.setState({checked: event.target.checked});
onCheckboxChange: function(newState) {
this.setState({checked: newState.checked});
},
handleCreateButtonClick: function() {
@ -770,14 +775,15 @@ loop.panel = (function(_, mozL10n) {
return (
<div className="new-room-view">
<div className={contextClasses}>
<label className="context-enabled">
<input className="context-checkbox"
type="checkbox" onChange={this.onCheckboxChange}/>
{mozL10n.get("context_offer_label")}
</label>
<img className="context-preview" src={this.state.previewImage}/>
<span className="context-description">{this.state.description}</span>
<span className="context-url">{hostname}</span>
<Checkbox label={mozL10n.get("context_inroom_label")}
onChange={this.onCheckboxChange} />
<div className="context-content">
<img className="context-preview" src={this.state.previewImage}/>
<span className="context-description">
{this.state.description}
<span className="context-url">{hostname}</span>
</span>
</div>
</div>
<button className="btn btn-info new-room-button"
onClick={this.handleCreateButtonClick}

View File

@ -312,8 +312,8 @@ loop.roomViews = (function(mozL10n) {
// checkbox will be disabled in that case.
if (nextProps.editMode) {
this.props.mozLoop.getSelectedTabMetadata(function(metadata) {
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
var description = metadata.description || metadata.title;
var previewImage = metadata.favicon || "";
var description = metadata.title || metadata.description;
var url = metadata.url;
this.setState({
availableContext: {
@ -480,8 +480,10 @@ loop.roomViews = (function(mozL10n) {
locationData = checkboxLabel = sharedUtils.formatURL(location);
}
if (!checkboxLabel) {
checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
this.state.availableContext.url : ""));
try {
checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
this.state.availableContext.url : ""));
} catch (ex) {}
}
var cx = React.addons.classSet;

View File

@ -312,8 +312,8 @@ loop.roomViews = (function(mozL10n) {
// checkbox will be disabled in that case.
if (nextProps.editMode) {
this.props.mozLoop.getSelectedTabMetadata(function(metadata) {
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
var description = metadata.description || metadata.title;
var previewImage = metadata.favicon || "";
var description = metadata.title || metadata.description;
var url = metadata.url;
this.setState({
availableContext: {
@ -480,8 +480,10 @@ loop.roomViews = (function(mozL10n) {
locationData = checkboxLabel = sharedUtils.formatURL(location);
}
if (!checkboxLabel) {
checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
this.state.availableContext.url : ""));
try {
checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
this.state.availableContext.url : ""));
} catch (ex) {}
}
var cx = React.addons.classSet;

View File

@ -999,7 +999,7 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
}
.room-context-thumbnail {
width: 100px;
width: 16px;
-moz-margin-end: 1ch;
margin-bottom: 1em;
order: 1;

View File

@ -26,6 +26,9 @@
stroke: rgba(255,255,255,.4);
stroke-width: 0.5;
}
use[id$="-blue"] {
fill: #0096dd;
}
</style>
<defs style="display: none;">
<path id="check-shape" d="M 9.39,16.5 16.28,6 14.77,4.5 9.37,12.7 6.28,9.2 4.7,10.7 z"/>
@ -33,4 +36,5 @@
<use id="check" xlink:href="#check-shape"/>
<use id="check-active" xlink:href="#check-shape"/>
<use id="check-disabled" xlink:href="#check-shape"/>
<use id="check-blue" xlink:href="#check-shape"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -904,7 +904,16 @@ function injectLoopAPI(targetWindow) {
win.messageManager.addMessageListener("PageMetadata:PageDataResult", function onPageDataResult(msg) {
win.messageManager.removeMessageListener("PageMetadata:PageDataResult", onPageDataResult);
let pageData = msg.json;
callback(cloneValueInto(pageData, targetWindow));
win.LoopUI.getFavicon(function(err, favicon) {
if (err) {
MozLoopService.log.error("Error occurred whilst fetching favicon", err);
// We don't return here intentionally to make sure the callback is
// invoked at all times. We just report the error here.
}
pageData.favicon = favicon || null;
callback(cloneValueInto(pageData, targetWindow));
});
});
win.gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData");
}

View File

@ -794,10 +794,12 @@ describe("loop.panel", function() {
it("should dispatch a CreateRoom action with context when clicking on the " +
"Start a conversation button", function() {
fakeMozLoop.userProfile = {email: fakeEmail};
var favicon = "data:image/x-icon;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
fakeMozLoop.getSelectedTabMetadata = function (callback) {
callback({
url: "http://invalid.com",
description: "fakeSite",
favicon: favicon,
previews: ["fakeimage.png"]
});
};
@ -810,8 +812,7 @@ describe("loop.panel", function() {
var node = view.getDOMNode();
// Select the checkbox
TestUtils.Simulate.change(node.querySelector(".context-checkbox"),
{"target": {"checked": true}});
TestUtils.Simulate.click(node.querySelector(".checkbox-wrapper"));
TestUtils.Simulate.click(node.querySelector(".new-room-button"));
@ -821,7 +822,7 @@ describe("loop.panel", function() {
urls: [{
location: "http://invalid.com",
description: "fakeSite",
thumbnail: "fakeimage.png"
thumbnail: favicon
}]
}));
});
@ -848,8 +849,8 @@ describe("loop.panel", function() {
// Simulate being visible
view.onDocumentVisible();
var contextEnabledCheckbox = view.getDOMNode().querySelector(".context-enabled");
expect(contextEnabledCheckbox).to.not.equal(null);
var contextContent = view.getDOMNode().querySelector(".context-content");
expect(contextContent).to.not.equal(null);
});
it("should not show context information when a URL is unavailable", function() {
@ -886,6 +887,26 @@ describe("loop.panel", function() {
var contextHostname = view.getDOMNode().querySelector(".context-url");
expect(contextHostname.textContent).eql("www.example.com");
});
it("should show the favicon when available", function() {
var favicon = "data:image/x-icon;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
fakeMozLoop.getSelectedTabMetadata = function (callback) {
callback({
url: "https://www.example.com:1234",
description: "fake description",
favicon: favicon,
previews: ["foo.gif"]
});
};
var view = createTestComponent();
// Simulate being visible.
view.onDocumentVisible();
var contextPreview = view.getDOMNode().querySelector(".context-preview");
expect(contextPreview.src).eql(favicon);
});
});
describe('loop.panel.ToSView', function() {

View File

@ -10,6 +10,7 @@ describe("loop.roomViews", function () {
var sandbox, dispatcher, roomStore, activeRoomStore, fakeWindow,
fakeMozLoop, fakeContextURL;
var favicon = "data:image/x-icon;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
beforeEach(function() {
sandbox = sinon.sandbox.create();
@ -20,10 +21,22 @@ describe("loop.roomViews", function () {
getAudioBlob: sinon.stub(),
getLoopPref: sinon.stub(),
getSelectedTabMetadata: sinon.stub().callsArgWith(0, {
favicon: favicon,
previews: [],
title: ""
}),
isSocialShareButtonAvailable: sinon.stub()
isSocialShareButtonAvailable: sinon.stub(),
rooms: {
get: sinon.stub().callsArgWith(1, null, {
roomToken: "fakeToken",
roomName: "fakeName",
decryptedContext: {
roomName: "fakeName",
urls: []
}
}),
update: sinon.stub().callsArgWith(2, null)
}
};
fakeWindow = {
@ -49,7 +62,7 @@ describe("loop.roomViews", function () {
sdkDriver: {}
});
roomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: {},
mozLoop: fakeMozLoop,
activeRoomStore: activeRoomStore
});
@ -723,6 +736,32 @@ describe("loop.roomViews", function () {
var checkbox = view.getDOMNode().querySelector(".checkbox");
expect(checkbox.classList.contains("disabled")).to.eql(true);
});
it("should render the editMode view when the edit button is clicked", function(next) {
var roomName = "Hello, is it me you're looking for?";
view = mountTestComponent({
roomData: {
roomToken: "fakeToken",
roomName: roomName,
roomContextUrls: [fakeContextURL]
}
});
// Switch to editMode via setting the prop, since we can control that
// better.
view.setProps({ editMode: true }, function() {
// First check if availableContext is set correctly.
expect(view.state.availableContext).to.not.eql(null);
expect(view.state.availableContext.previewImage).to.eql(favicon);
var node = view.getDOMNode();
expect(node.querySelector(".room-context-name").value).to.eql(roomName);
expect(node.querySelector(".room-context-url").value).to.eql(fakeContextURL.location);
expect(node.querySelector(".room-context-comments").value).to.eql(fakeContextURL.description);
next();
});
});
});
describe("Update Room", function() {

View File

@ -16,6 +16,7 @@ skip-if = e10s
[browser_loop_fxa_server.js]
[browser_LoopContacts.js]
[browser_mozLoop_appVersionInfo.js]
[browser_mozLoop_context.js]
[browser_mozLoop_prefs.js]
[browser_mozLoop_doNotDisturb.js]
skip-if = buildapp == 'mulet'

View File

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* This file contains tests for various context-in-conversations helpers in
* LoopUI and MozLoopAPI.
*/
"use strict";
add_task(loadLoopPanel);
function promiseGetMetadata() {
return new Promise(resolve => gMozLoopAPI.getSelectedTabMetadata(resolve));
}
add_task(function* test_mozLoop_getSelectedTabMetadata() {
Assert.ok(gMozLoopAPI, "mozLoop should exist");
let metadata = yield promiseGetMetadata();
Assert.strictEqual(metadata.url, null, "URL should be empty for about:blank");
Assert.strictEqual(metadata.favicon, null, "Favicon should be empty for about:blank");
Assert.strictEqual(metadata.title, "", "Title should be empty for about:blank");
Assert.deepEqual(metadata.previews, [], "No previews available for about:blank");
let tab = gBrowser.selectedTab = gBrowser.addTab();
yield promiseTabLoadEvent(tab, "about:home");
metadata = yield promiseGetMetadata();
Assert.strictEqual(metadata.url, null, "URL should be empty for about:home");
Assert.ok(metadata.favicon.startsWith("data:image/x-icon;base64,"),
"Favicon should be set for about:home");
Assert.ok(metadata.title, "Title should be set for about:home");
Assert.deepEqual(metadata.previews, [], "No previews available for about:home");
gBrowser.removeTab(tab);
});

View File

@ -17,7 +17,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
let Pocket = {
get site() Services.prefs.getCharPref("browser.pocket.site"),
get listURL() { return "https://" + Pocket.site; },
get listURL() { return "https://" + Pocket.site + "/?src=ff_ext"; },
/**
* Functions related to the Pocket panel UI.
@ -50,7 +50,7 @@ let Pocket = {
let window = document.defaultView;
let iframe = document.getElementById('pocket-panel-iframe');
iframe.removeEventListener("load", Pocket.onPanelLoaded, true);
iframe.removeEventListener("load", Pocket.onFrameLoaded, true);
window.pktUI.pocketPanelDidShow();
},

View File

@ -294,7 +294,7 @@ var pktUI = (function() {
{
startheight = overflowMenuHeight;
}
else if (pktApi.getSignupAB() == 'storyboard')
else if (pktApi.getSignupAB().indexOf('storyboard') > -1)
{
startheight = 460;
if (fxasignedin == '1')

View File

@ -108,6 +108,9 @@
.pkt_ext_containersignup .pkt_ext_learnmore {
font-size: 12px;
}
.pkt_ext_containersignup .pkt_ext_learnmoreinactive {
visibility: hidden;
}
.pkt_ext_signupdetail h4 {
font-size: 12px;
font-weight: normal;
@ -356,6 +359,20 @@
.pkt_ext_signup_ru .signup-btn-firefox .text {
left: 15px;
}
.pkt_ext_signup_de .signup-btn-firefox .logo,
.pkt_ext_signup_es .pkt_ext_signupdetail_hero .signup-btn-firefox .logo,
.pkt_ext_signup_ja .signup-btn-firefox .logo,
.pkt_ext_signup_ru .signup-btn-firefox .logo {
height: 2.4em;
}
@media (min-resolution: 1.1dppx) {
.pkt_ext_signup_de .signup-btn-firefox .logo,
.pkt_ext_signup_es .pkt_ext_signupdetail_hero .signup-btn-firefox .logo,
.pkt_ext_signup_ja .signup-btn-firefox .logo,
.pkt_ext_signup_ru .signup-btn-firefox .logo {
height: 2.5em;
}
}
.pkt_ext_signup_de .signup-btn-email,
.pkt_ext_signup_es .pkt_ext_signupdetail_hero .signup-btn-email,
.pkt_ext_signup_ja .signup-btn-email,

View File

@ -3,6 +3,7 @@ Translations.en =
{
addtags: "Add Tags",
alreadyhaveacct: "Already a Pocket user?",
continueff: "Continue with Firefox",
errorgeneric: "There was an error when trying to save to Pocket.",
learnmore: "Learn More",
loginnow: "Log in",
@ -34,11 +35,11 @@ Translations.de =
addtags: "Tags hinzufügen",
alreadyhaveacct: "Sind Sie bereits Pocket-Nutzer?",
continueff: "Mit Firefox fortfahren",
error1: "Wir konnten keine vorgeschlagenen Tags für dieses Element finden.",
error3: "Bitte melden Sie sich bei Pocket an und versuchen Sie es erneut.",
errorgeneric: "Beim Speichern des Links in Pocket ist ein Problem aufgetreten.",
learnmore: "Mehr erfahren",
loginnow: "Anmelden",
maxtaglength: "Tags dürfen höchsten 25 Zeichen lang sein.",
mustbeconnected: "Bitte versichere dich, dass du mit dem Internet verbunden bist.",
onlylinkssaved: "Es können nur Links gespeichert werden",
pagenotsaved: "Seite nicht gespeichert",
pageremoved: "Seite entfernt",
@ -47,6 +48,7 @@ Translations.de =
processingtags: "Tags werden hinzugefügt…",
removepage: "Seite entfernen",
save: "Speichern",
saving: "Speichern...",
signupemail: "Mit E-Mail registrieren",
signuptosave: "Registrieren Sie sich bei Pocket. Das ist kostenlos.",
suggestedtags: "Vorgeschlagene Tags",
@ -64,11 +66,11 @@ Translations.es =
addtags: "Añadir etiquetas",
alreadyhaveacct: "¿Ya tiene cuenta Pocket?",
continueff: "Continuar con Firefox",
error1: "No se han encontrado etiquetas sugeridas para este elemento.",
error3: "Inicie sesión en Pocket y vuelva a intentarlo.",
errorgeneric: "Se ha producido un error al guardar el enlace en Pocket.",
learnmore: "Saber más",
loginnow: "Iniciar sesión",
maxtaglength: "Las etiquetas están limitadas a 25 caracteres.",
mustbeconnected: "Comprueba que tienes conexión a Internet.",
onlylinkssaved: "Solo se pueden guardar enlaces",
pagenotsaved: "Página no guardada",
pageremoved: "Página eliminada",
@ -77,6 +79,7 @@ Translations.es =
processingtags: "Añadiendo etiquetas…",
removepage: "Eliminar página",
save: "Guardar",
saving: "Guardando…",
signupemail: "Regístrese con su correo.",
signuptosave: "Regístrese en Pocket. Es gratis.",
suggestedtags: "Etiquetas sugeridas",
@ -94,11 +97,11 @@ Translations.ja =
addtags: "タグを追加",
alreadyhaveacct: "アカウントをお持ちですか?",
continueff: "Firefox で続行",
error1: "この項目に合うタグが見つかりません。",
error3: "Pocket にログインしてやり直してください。",
errorgeneric: "Pocket にリンクを保存中に問題が発生しました。",
learnmore: "詳細",
loginnow: "ログイン",
maxtaglength: "タグは 25 文字までです。",
mustbeconnected: "インターネットに接続されていることをご確認ください。",
onlylinkssaved: "リンクのみ保存できます",
pagenotsaved: "ページを保存できませんでした",
pageremoved: "ページを削除しました",
@ -107,6 +110,7 @@ Translations.ja =
processingtags: "タグを追加中...",
removepage: "ページを削除",
save: "保存",
saving: "保存中...",
signupemail: "メールでアカウント登録",
signuptosave: "Pocket にアカウント登録してください。無料です。",
suggestedtags: "タグ候補",
@ -124,11 +128,11 @@ Translations.ru =
addtags: "Добавить теги",
alreadyhaveacct: "Уже используете Pocket?",
continueff: "Продолжить через Firefox",
error1: "Мы не смогли найти для этого элемента рекомендуемые теги.",
error3: "Пожалуйста, войдите в Pocket, и попробуйте ещё раз.",
errorgeneric: "Не удалось сохранить в Pocket.",
learnmore: "Узнайте больше",
loginnow: "Войдите",
maxtaglength: "Длина тега не должна превышать 25 символов.",
mustbeconnected: "Убедитесь, что вы подключены к Интернет.",
onlylinkssaved: "Можно сохранять только ссылки",
pagenotsaved: "Страница не сохранена",
pageremoved: "Страница удалена",
@ -137,6 +141,7 @@ Translations.ru =
processingtags: "Добавление тегов...",
removepage: "Удалить страницу",
save: "Сохранить",
saving: "Сохранение...",
signupemail: "Регистрация по эл. почте",
signuptosave: "Зарегистрируйтесь в Pocket. Это бесплатно.",
suggestedtags: "Рекомендуемые теги",

View File

@ -78,7 +78,7 @@ var PKT_SAVED_OVERLAY = function (options)
}
else if (resp.status == 'error') {
var msg = $('<p class="suggestedtag_msg">');
msg.text(resp.error);
msg.text(resp.error.message);
$('.pkt_ext_suggestedtag_detail').append(msg);
this.suggestedTagsLoaded = true;
if (!myself.mouseInside) {
@ -315,7 +315,7 @@ var PKT_SAVED_OVERLAY = function (options)
thePKT_SAVED.sendMessage("addTags",
{
url: myself.savedUrl,
tags: originaltags
tags: originaltags
}, function(resp)
{
if (resp.status == 'success')
@ -324,7 +324,7 @@ var PKT_SAVED_OVERLAY = function (options)
}
else if (resp.status == 'error')
{
$('.pkt_ext_edit_msg').addClass('pkt_ext_edit_msg_error pkt_ext_edit_msg_active').text(resp.error);
$('.pkt_ext_edit_msg').addClass('pkt_ext_edit_msg_error pkt_ext_edit_msg_active').text(resp.error.message);
}
});
});
@ -348,7 +348,7 @@ var PKT_SAVED_OVERLAY = function (options)
myself.showStateFinalMsg(myself.dictJSON.pageremoved);
}
else if (resp.status == 'error') {
$('.pkt_ext_edit_msg').addClass('pkt_ext_edit_msg_error pkt_ext_edit_msg_active').text(resp.error);
$('.pkt_ext_edit_msg').addClass('pkt_ext_edit_msg_error pkt_ext_edit_msg_active').text(resp.error.message);
}
});
}

View File

@ -187,7 +187,9 @@ PKT_SIGNUP_OVERLAY.prototype = {
this.getTranslations();
this.dictJSON.fxasignedin = this.fxasignedin ? 1 : 0;
this.dictJSON.variant = (this.variant ? this.variant : 'undefined');
this.dictJSON.variant += this.fxasignedin ? '_fxa' : '_nonfxa';
this.dictJSON.pockethost = this.pockethost;
this.dictJSON.showlearnmore = (this.variant.indexOf('_lm') > -1 || this.variant == 'storyboard' || this.variant == 'hero') ? 1 : 0;
// extra modifier class for collapsed state
if (this.inoverflowmenu)
@ -202,7 +204,7 @@ PKT_SIGNUP_OVERLAY.prototype = {
}
// Create actual content
if (this.variant == 'storyboard')
if (this.variant == 'storyboard' || this.variant == 'storyboard_lm' || this.variant == 'storyboard_nlm')
{
$('body').append(Handlebars.templates.signupstoryboard_shell(this.dictJSON));
}

View File

@ -29,74 +29,99 @@ templates['saved_shell'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":fun
},"useData":true});
templates['signup_shell'] = template({"1":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return " <p class=\"btn-container\"><a href=\"https://"
return " <p class=\"pkt_ext_learnmorecontainer\"><a class=\"pkt_ext_learnmore\" href=\"https://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "/ff_signup?s="
+ "?s=ffi&t=learnmore&v="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "&t=wlm\" target=_blank\" class=\"btn signup-btn-firefox\"><span class=\"logo\"></span><span class=\"text\">"
+ escapeExpression(((helper = (helper = helpers.signinfirefox || (depth0 != null ? depth0.signinfirefox : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signinfirefox","hash":{},"data":data}) : helper)))
+ "</span></a></p>\n";
+ "\" target=\"_blank\">"
+ escapeExpression(((helper = (helper = helpers.learnmore || (depth0 != null ? depth0.learnmore : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"learnmore","hash":{},"data":data}) : helper)))
+ "</a></p>\n";
},"3":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return " <p class=\"pkt_ext_learnmorecontainer\"><a class=\"pkt_ext_learnmore pkt_ext_learnmoreinactive\" href=\"#\">"
+ escapeExpression(((helper = (helper = helpers.learnmore || (depth0 != null ? depth0.learnmore : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"learnmore","hash":{},"data":data}) : helper)))
+ "</a></p>\n";
},"5":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return " <p class=\"btn-container\"><a href=\"https://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "/ff_signup?s="
+ "/ff_signup?s=ffi&t=signupff&v="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "&t=wlm\" target=_blank\" class=\"btn signup-btn-firefox\"><span class=\"logo\"></span><span class=\"text\">"
+ escapeExpression(((helper = (helper = helpers.signupfirefox || (depth0 != null ? depth0.signupfirefox : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signupfirefox","hash":{},"data":data}) : helper)))
+ "</span></a></p>\n <p class=\"btn-container\"><a href=\"http://"
+ "\" target=\"_blank\" class=\"btn signup-btn-firefox\"><span class=\"logo\"></span><span class=\"text\">"
+ escapeExpression(((helper = (helper = helpers.signinfirefox || (depth0 != null ? depth0.signinfirefox : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signinfirefox","hash":{},"data":data}) : helper)))
+ "</span></a></p>\n";
},"7":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return " <p class=\"btn-container\"><a href=\"https://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "/signup?force=email&src=extension&s="
+ "/ff_signup?s=ffi&t=signupff&v="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "&t=wlm\" target=\"_blank\" class=\"btn btn-secondary signup-btn-email signup-btn-initstate\">"
+ "\" target=\"_blank\" class=\"btn signup-btn-firefox\"><span class=\"logo\"></span><span class=\"text\">"
+ escapeExpression(((helper = (helper = helpers.signupfirefox || (depth0 != null ? depth0.signupfirefox : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signupfirefox","hash":{},"data":data}) : helper)))
+ "</span></a></p>\n <p class=\"btn-container\"><a href=\"https://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "/signup?force=email&src=extension&s=ffi&t=signupemail&v="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "\" target=\"_blank\" class=\"btn btn-secondary signup-btn-email signup-btn-initstate\">"
+ escapeExpression(((helper = (helper = helpers.signupemail || (depth0 != null ? depth0.signupemail : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signupemail","hash":{},"data":data}) : helper)))
+ "</a></p>\n";
},"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
var stack1, helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, buffer = "<div class=\"pkt_ext_introdetail pkt_ext_introdetailhero\">\n <h2 class=\"pkt_ext_logo\">Pocket</h2>\n <p class=\"pkt_ext_tagline\">"
+ escapeExpression(((helper = (helper = helpers.tagline || (depth0 != null ? depth0.tagline : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"tagline","hash":{},"data":data}) : helper)))
+ "</p>\n <p class=\"pkt_ext_learnmorecontainer\"><a class=\"pkt_ext_learnmore\" href=\"http://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "?s="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "&t=wlm&src=ff_learn_more\" target=\"_blank\">"
+ escapeExpression(((helper = (helper = helpers.learnmore || (depth0 != null ? depth0.learnmore : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"learnmore","hash":{},"data":data}) : helper)))
+ "</a></p>\n <div class=\"pkt_ext_introimg\"></div>\n</div>\n<div class=\"pkt_ext_signupdetail pkt_ext_signupdetail_hero\">\n <h4>"
+ "</p>\n";
stack1 = helpers['if'].call(depth0, (depth0 != null ? depth0.showlearnmore : depth0), {"name":"if","hash":{},"fn":this.program(1, data),"inverse":this.program(3, data),"data":data});
if (stack1 != null) { buffer += stack1; }
buffer += " <div class=\"pkt_ext_introimg\"></div>\n</div>\n<div class=\"pkt_ext_signupdetail pkt_ext_signupdetail_hero\">\n <h4>"
+ escapeExpression(((helper = (helper = helpers.signuptosave || (depth0 != null ? depth0.signuptosave : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signuptosave","hash":{},"data":data}) : helper)))
+ "</h4>\n";
stack1 = helpers['if'].call(depth0, (depth0 != null ? depth0.fxasignedin : depth0), {"name":"if","hash":{},"fn":this.program(1, data),"inverse":this.program(3, data),"data":data});
stack1 = helpers['if'].call(depth0, (depth0 != null ? depth0.fxasignedin : depth0), {"name":"if","hash":{},"fn":this.program(5, data),"inverse":this.program(7, data),"data":data});
if (stack1 != null) { buffer += stack1; }
return buffer + " <p class=\"alreadyhave\">"
+ escapeExpression(((helper = (helper = helpers.alreadyhaveacct || (depth0 != null ? depth0.alreadyhaveacct : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"alreadyhaveacct","hash":{},"data":data}) : helper)))
+ " <a href=\"http://"
+ " <a href=\"https://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "/login?ep=3&src=extension&s="
+ "/login?ep=3&src=extension&s=ffi&t=login&v="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "&t=wlm\" target=\"_blank\">"
+ "\" target=\"_blank\">"
+ escapeExpression(((helper = (helper = helpers.loginnow || (depth0 != null ? depth0.loginnow : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"loginnow","hash":{},"data":data}) : helper)))
+ "</a>.</p>\n</div>";
},"useData":true});
templates['signupstoryboard_shell'] = template({"1":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return " <p class=\"btn-container\"><a href=\"https://"
return " <p><a class=\"pkt_ext_learnmore\" href=\"https://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "/ff_signup?s="
+ "?s=ffi&t=learnmore&v="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "&t=wlm\" target=_blank\" class=\"btn signup-btn-firefox\"><span class=\"logo\"></span><span class=\"text\">"
+ escapeExpression(((helper = (helper = helpers.signinfirefox || (depth0 != null ? depth0.signinfirefox : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signinfirefox","hash":{},"data":data}) : helper)))
+ "</span></a></p>\n";
+ "\" target=\"_blank\">"
+ escapeExpression(((helper = (helper = helpers.learnmore || (depth0 != null ? depth0.learnmore : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"learnmore","hash":{},"data":data}) : helper)))
+ "</a></p>\n";
},"3":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return " <p><a class=\"pkt_ext_learnmore pkt_ext_learnmoreinactive\" href=\"#\">"
+ escapeExpression(((helper = (helper = helpers.learnmore || (depth0 != null ? depth0.learnmore : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"learnmore","hash":{},"data":data}) : helper)))
+ "</a></p>\n";
},"5":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return " <p class=\"btn-container\"><a href=\"https://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "/ff_signup?s="
+ "/ff_signup?s=ffi&t=signupff&v="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "&t=wlm\" target=_blank\" class=\"btn signup-btn-firefox\"><span class=\"logo\"></span><span class=\"text\">"
+ escapeExpression(((helper = (helper = helpers.signupfirefox || (depth0 != null ? depth0.signupfirefox : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signupfirefox","hash":{},"data":data}) : helper)))
+ "</span></a></p>\n <p class=\"btn-container\"><a href=\"http://"
+ "\" target=\"_blank\" class=\"btn signup-btn-firefox\"><span class=\"logo\"></span><span class=\"text\">"
+ escapeExpression(((helper = (helper = helpers.signinfirefox || (depth0 != null ? depth0.signinfirefox : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signinfirefox","hash":{},"data":data}) : helper)))
+ "</span></a></p>\n";
},"7":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return " <p class=\"btn-container\"><a href=\"https://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "/signup?force=email&src=extension&s="
+ "/ff_signup?s=ffi&t=signupff&v="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "&t=wlm\" target=\"_blank\" class=\"btn btn-secondary signup-btn-email signup-btn-initstate\">"
+ "\" target=\"_blank\" class=\"btn signup-btn-firefox\"><span class=\"logo\"></span><span class=\"text\">"
+ escapeExpression(((helper = (helper = helpers.signupfirefox || (depth0 != null ? depth0.signupfirefox : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signupfirefox","hash":{},"data":data}) : helper)))
+ "</span></a></p>\n <p class=\"btn-container\"><a href=\"https://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "/signup?force=email&src=extension&s=ffi&t=signupemail&v="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "\" target=\"_blank\" class=\"btn btn-secondary signup-btn-email signup-btn-initstate\">"
+ escapeExpression(((helper = (helper = helpers.signupemail || (depth0 != null ? depth0.signupemail : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signupemail","hash":{},"data":data}) : helper)))
+ "</a></p>\n";
},"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
@ -104,24 +129,21 @@ templates['signupstoryboard_shell'] = template({"1":function(depth0,helpers,part
+ escapeExpression(((helper = (helper = helpers.taglinestory_one || (depth0 != null ? depth0.taglinestory_one : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"taglinestory_one","hash":{},"data":data}) : helper)))
+ "</p>\n </div>\n <div class=\"pkt_ext_introstoryone_img\"></div>\n </div>\n <div class=\"pkt_ext_introstorydivider\"></div>\n <div class=\"pkt_ext_introstory pkt_ext_introstorytwo\">\n <div class=\"pkt_ext_introstory_text\">\n <p class=\"pkt_ext_tagline\">"
+ escapeExpression(((helper = (helper = helpers.taglinestory_two || (depth0 != null ? depth0.taglinestory_two : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"taglinestory_two","hash":{},"data":data}) : helper)))
+ "</p>\n <p><a class=\"pkt_ext_learnmore\" href=\"http://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "?s="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "&t=wlm&src=ff_learn_more\" target=\"_blank\">"
+ escapeExpression(((helper = (helper = helpers.learnmore || (depth0 != null ? depth0.learnmore : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"learnmore","hash":{},"data":data}) : helper)))
+ "</a></p>\n </div>\n <div class=\"pkt_ext_introstorytwo_img\"></div>\n </div>\n</div>\n<div class=\"pkt_ext_signupdetail\">\n <h4>"
+ "</p>\n";
stack1 = helpers['if'].call(depth0, (depth0 != null ? depth0.showlearnmore : depth0), {"name":"if","hash":{},"fn":this.program(1, data),"inverse":this.program(3, data),"data":data});
if (stack1 != null) { buffer += stack1; }
buffer += " </div>\n <div class=\"pkt_ext_introstorytwo_img\"></div>\n </div>\n</div>\n<div class=\"pkt_ext_signupdetail\">\n <h4>"
+ escapeExpression(((helper = (helper = helpers.signuptosave || (depth0 != null ? depth0.signuptosave : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"signuptosave","hash":{},"data":data}) : helper)))
+ "</h4>\n";
stack1 = helpers['if'].call(depth0, (depth0 != null ? depth0.fxasignedin : depth0), {"name":"if","hash":{},"fn":this.program(1, data),"inverse":this.program(3, data),"data":data});
stack1 = helpers['if'].call(depth0, (depth0 != null ? depth0.fxasignedin : depth0), {"name":"if","hash":{},"fn":this.program(5, data),"inverse":this.program(7, data),"data":data});
if (stack1 != null) { buffer += stack1; }
return buffer + " <p class=\"alreadyhave\">"
+ escapeExpression(((helper = (helper = helpers.alreadyhaveacct || (depth0 != null ? depth0.alreadyhaveacct : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"alreadyhaveacct","hash":{},"data":data}) : helper)))
+ " <a href=\"http://"
+ " <a href=\"https://"
+ escapeExpression(((helper = (helper = helpers.pockethost || (depth0 != null ? depth0.pockethost : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"pockethost","hash":{},"data":data}) : helper)))
+ "/login?ep=3&src=extension&s="
+ "/login?ep=3&src=extension&s=ffi&t=login&v="
+ escapeExpression(((helper = (helper = helpers.variant || (depth0 != null ? depth0.variant : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"variant","hash":{},"data":data}) : helper)))
+ "&t=wlm\" target=\"_blank\">"
+ "\" target=\"_blank\">"
+ escapeExpression(((helper = (helper = helpers.loginnow || (depth0 != null ? depth0.loginnow : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"loginnow","hash":{},"data":data}) : helper)))
+ "</a>.</p>\n</div>";
},"useData":true});

View File

@ -1,16 +1,20 @@
<div class="pkt_ext_introdetail pkt_ext_introdetailhero">
<h2 class="pkt_ext_logo">Pocket</h2>
<p class="pkt_ext_tagline">{{tagline}}</p>
<p class="pkt_ext_learnmorecontainer"><a class="pkt_ext_learnmore" href="http://{{pockethost}}?s={{variant}}&t=wlm&src=ff_learn_more" target="_blank">{{learnmore}}</a></p>
{{#if showlearnmore}}
<p class="pkt_ext_learnmorecontainer"><a class="pkt_ext_learnmore" href="https://{{pockethost}}?s=ffi&t=learnmore&v={{variant}}" target="_blank">{{learnmore}}</a></p>
{{else}}
<p class="pkt_ext_learnmorecontainer"><a class="pkt_ext_learnmore pkt_ext_learnmoreinactive" href="#">{{learnmore}}</a></p>
{{/if}}
<div class="pkt_ext_introimg"></div>
</div>
<div class="pkt_ext_signupdetail pkt_ext_signupdetail_hero">
<h4>{{signuptosave}}</h4>
{{#if fxasignedin}}
<p class="btn-container"><a href="https://{{pockethost}}/ff_signup?s={{variant}}&t=wlm" target=_blank" class="btn signup-btn-firefox"><span class="logo"></span><span class="text">{{signinfirefox}}</span></a></p>
<p class="btn-container"><a href="https://{{pockethost}}/ff_signup?s=ffi&t=signupff&v={{variant}}" target="_blank" class="btn signup-btn-firefox"><span class="logo"></span><span class="text">{{signinfirefox}}</span></a></p>
{{else}}
<p class="btn-container"><a href="https://{{pockethost}}/ff_signup?s={{variant}}&t=wlm" target=_blank" class="btn signup-btn-firefox"><span class="logo"></span><span class="text">{{signupfirefox}}</span></a></p>
<p class="btn-container"><a href="http://{{pockethost}}/signup?force=email&src=extension&s={{variant}}&t=wlm" target="_blank" class="btn btn-secondary signup-btn-email signup-btn-initstate">{{signupemail}}</a></p>
<p class="btn-container"><a href="https://{{pockethost}}/ff_signup?s=ffi&t=signupff&v={{variant}}" target="_blank" class="btn signup-btn-firefox"><span class="logo"></span><span class="text">{{signupfirefox}}</span></a></p>
<p class="btn-container"><a href="https://{{pockethost}}/signup?force=email&src=extension&s=ffi&t=signupemail&v={{variant}}" target="_blank" class="btn btn-secondary signup-btn-email signup-btn-initstate">{{signupemail}}</a></p>
{{/if}}
<p class="alreadyhave">{{alreadyhaveacct}} <a href="http://{{pockethost}}/login?ep=3&src=extension&s={{variant}}&t=wlm" target="_blank">{{loginnow}}</a>.</p>
<p class="alreadyhave">{{alreadyhaveacct}} <a href="https://{{pockethost}}/login?ep=3&src=extension&s=ffi&t=login&v={{variant}}" target="_blank">{{loginnow}}</a>.</p>
</div>

View File

@ -9,7 +9,11 @@
<div class="pkt_ext_introstory pkt_ext_introstorytwo">
<div class="pkt_ext_introstory_text">
<p class="pkt_ext_tagline">{{taglinestory_two}}</p>
<p><a class="pkt_ext_learnmore" href="http://{{pockethost}}?s={{variant}}&t=wlm&src=ff_learn_more" target="_blank">{{learnmore}}</a></p>
{{#if showlearnmore}}
<p><a class="pkt_ext_learnmore" href="https://{{pockethost}}?s=ffi&t=learnmore&v={{variant}}" target="_blank">{{learnmore}}</a></p>
{{else}}
<p><a class="pkt_ext_learnmore pkt_ext_learnmoreinactive" href="#">{{learnmore}}</a></p>
{{/if}}
</div>
<div class="pkt_ext_introstorytwo_img"></div>
</div>
@ -17,10 +21,10 @@
<div class="pkt_ext_signupdetail">
<h4>{{signuptosave}}</h4>
{{#if fxasignedin}}
<p class="btn-container"><a href="https://{{pockethost}}/ff_signup?s={{variant}}&t=wlm" target=_blank" class="btn signup-btn-firefox"><span class="logo"></span><span class="text">{{signinfirefox}}</span></a></p>
<p class="btn-container"><a href="https://{{pockethost}}/ff_signup?s=ffi&t=signupff&v={{variant}}" target="_blank" class="btn signup-btn-firefox"><span class="logo"></span><span class="text">{{signinfirefox}}</span></a></p>
{{else}}
<p class="btn-container"><a href="https://{{pockethost}}/ff_signup?s={{variant}}&t=wlm" target=_blank" class="btn signup-btn-firefox"><span class="logo"></span><span class="text">{{signupfirefox}}</span></a></p>
<p class="btn-container"><a href="http://{{pockethost}}/signup?force=email&src=extension&s={{variant}}&t=wlm" target="_blank" class="btn btn-secondary signup-btn-email signup-btn-initstate">{{signupemail}}</a></p>
<p class="btn-container"><a href="https://{{pockethost}}/ff_signup?s=ffi&t=signupff&v={{variant}}" target="_blank" class="btn signup-btn-firefox"><span class="logo"></span><span class="text">{{signupfirefox}}</span></a></p>
<p class="btn-container"><a href="https://{{pockethost}}/signup?force=email&src=extension&s=ffi&t=signupemail&v={{variant}}" target="_blank" class="btn btn-secondary signup-btn-email signup-btn-initstate">{{signupemail}}</a></p>
{{/if}}
<p class="alreadyhave">{{alreadyhaveacct}} <a href="http://{{pockethost}}/login?ep=3&src=extension&s={{variant}}&t=wlm" target="_blank">{{loginnow}}</a>.</p>
<p class="alreadyhave">{{alreadyhaveacct}} <a href="https://{{pockethost}}/login?ep=3&src=extension&s=ffi&t=login&v={{variant}}" target="_blank">{{loginnow}}</a>.</p>
</div>

View File

@ -49,11 +49,8 @@ var pktApi = (function() {
*/
// Base url for all api calls
// TODO: This is a dev server and will be changed before launch
var pocketAPIhost = Services.prefs.getCharPref("browser.pocket.api");
var pocketSiteHost = Services.prefs.getCharPref("browser.pocket.site");
// Base url for all api calls
var pocketAPIhost = Services.prefs.getCharPref("browser.pocket.api"); // api.getpocket.com
var pocketSiteHost = Services.prefs.getCharPref("browser.pocket.site"); // getpocket.com
var baseAPIUrl = "https://" + pocketAPIhost + "/v3";
@ -86,6 +83,23 @@ var pktApi = (function() {
return out;
}
var parseJSON = function(jsonString){
try {
var o = JSON.parse(jsonString);
// Handle non-exception-throwing cases:
// Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
// but... JSON.parse(null) returns 'null', and typeof null === "object",
// so we must check for that, too.
if (o && typeof o === "object" && o !== null) {
return o;
}
}
catch (e) { }
return undefined;
};
/**
* Settings
*/
@ -173,6 +187,29 @@ var pktApi = (function() {
return pocketCookies['ftv1'];
}
/**
* Get the current premium status of the user
* @return {number | undefined} Premium status of user
*/
function getPremiumStatus() {
var premiumStatus = getSetting("premium_status");
if (typeof premiumStatus === "undefined") {
// Premium status is not in settings try get it from cookie
var pocketCookies = getCookiesFromPocket();
premiumStatus = pocketCookies['ps'];
}
return premiumStatus;
}
/**
* Helper method to check if a user is premium or not
* @return {Boolean} Boolean if user is premium or not
*/
function isPremiumUser() {
return getPremiumStatus() == 1;
}
/**
* Returns users logged in status
* @return {Boolean} Users logged in status
@ -210,7 +247,9 @@ var pktApi = (function() {
var url = baseAPIUrl + options.path;
var data = options.data || {};
data.locale_lang = window.navigator.language;
data.locale_lang = Cc["@mozilla.org/chrome/chrome-registry;1"].
getService(Ci.nsIXULChromeRegistry).
getSelectedLocale("browser");
data.consumer_key = oAuthConsumerKey;
var request = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Components.interfaces.nsIXMLHttpRequest);
@ -218,11 +257,16 @@ var pktApi = (function() {
request.onreadystatechange = function(e){
if (request.readyState == 4) {
if (request.status === 200) {
if (options.success) {
options.success(JSON.parse(request.response), request);
// There could still be an error if the response is no valid json
// or does not have status = 1
var response = parseJSON(request.response);
if (options.success && response && response.status == 1) {
options.success(response, request);
return;
}
return;
}
// Handle error case
if (options.error) {
// In case the user did revoke the access token or it's not
// valid anymore clear the user data
@ -231,7 +275,11 @@ var pktApi = (function() {
}
// Handle error message
var errorMessage = request.getResponseHeader("X-Error") || request.statusText;
var errorMessage;
if (request.status !== 200) {
errorMessage = request.getResponseHeader("X-Error") || request.statusText;
errorMessage = JSON.parse('"' + errorMessage + '"');
}
var error = {message: errorMessage};
options.error(error, request);
}
@ -509,13 +557,6 @@ var pktApi = (function() {
}
}
/**
* Helper method to check if a user is premium or not
*/
function isPremiumUser() {
return getSetting('premium_status') == 1;
}
/**
* Fetch suggested tags for a given item id
* @param {string} itemId Item id of
@ -565,14 +606,22 @@ var pktApi = (function() {
function getSignupAB() {
if (!getSetting('signupAB'))
{
var rand = (Math.floor(Math.random()*2+1));
if (rand == 2)
var rand = (Math.floor(Math.random()*100+1));
if (rand > 95)
{
setSetting('signupAB','storyboard');
setSetting('signupAB','storyboard_nlm');
}
else if (rand > 90)
{
setSetting('signupAB','hero_nlm');
}
else if (rand > 45)
{
setSetting('signupAB','storyboard_lm');
}
else
{
setSetting('signupAB','hero');
setSetting('signupAB','hero_lm');
}
}

View File

@ -48,6 +48,7 @@ var gAdvancedPane = {
this.initSubmitHealthReport();
#endif
this.updateCacheSizeInputField();
this.updateActualCacheSize();
this.updateActualAppCacheSize();
@ -378,24 +379,31 @@ var gAdvancedPane = {
},
/**
* Converts the cache size from units of KB to units of MB and returns that
* value.
* Converts the cache size from units of KB to units of MB and stores it in
* the textbox element.
*/
readCacheSize: function ()
updateCacheSizeInputField()
{
var preference = document.getElementById("browser.cache.disk.capacity");
return preference.value / 1024;
let cacheSizeElem = document.getElementById("cacheSize");
let cachePref = document.getElementById("browser.cache.disk.capacity");
cacheSizeElem.value = cachePref.value / 1024;
if (cachePref.locked)
cacheSizeElem.disabled = true;
},
/**
* Converts the cache size as specified in UI (in MB) to KB and returns that
* value.
* Updates the cache size preference once user enters a new value.
* We intentionally do not set preference="browser.cache.disk.capacity"
* onto the textbox directly, as that would update the pref at each keypress
* not only after the final value is entered.
*/
writeCacheSize: function ()
updateCacheSizePref()
{
var cacheSize = document.getElementById("cacheSize");
var intValue = parseInt(cacheSize.value, 10);
return isNaN(intValue) ? 0 : intValue * 1024;
let cacheSizeElem = document.getElementById("cacheSize");
let cachePref = document.getElementById("browser.cache.disk.capacity");
// Converts the cache size as specified in UI (in MB) to KB.
let intValue = parseInt(cacheSizeElem.value, 10);
cachePref.value = isNaN(intValue) ? 0 : intValue * 1024;
},
/**

View File

@ -250,9 +250,7 @@
accesskey="&limitCacheSizeBefore.accesskey;"
value="&limitCacheSizeBefore.label;"/>
<textbox id="cacheSize" type="number" size="4" max="1024"
preference="browser.cache.disk.capacity"
onsyncfrompreference="return gAdvancedPane.readCacheSize();"
onsynctopreference="return gAdvancedPane.writeCacheSize();"
onchange="gAdvancedPane.updateCacheSizePref();"
aria-labelledby="useCacheBefore cacheSize useCacheAfter"/>
<label id="useCacheAfter" flex="1">&limitCacheSizeAfter.label;</label>
</hbox>

View File

@ -45,6 +45,7 @@ var gAdvancedPane = {
#ifdef MOZ_SERVICES_HEALTHREPORT
this.initSubmitHealthReport();
#endif
this.updateCacheSizeInputField();
this.updateActualCacheSize();
this.updateActualAppCacheSize();
@ -413,24 +414,31 @@ var gAdvancedPane = {
},
/**
* Converts the cache size from units of KB to units of MB and returns that
* value.
* Converts the cache size from units of KB to units of MB and stores it in
* the textbox element.
*/
readCacheSize: function ()
updateCacheSizeInputField()
{
var preference = document.getElementById("browser.cache.disk.capacity");
return preference.value / 1024;
let cacheSizeElem = document.getElementById("cacheSize");
let cachePref = document.getElementById("browser.cache.disk.capacity");
cacheSizeElem.value = cachePref.value / 1024;
if (cachePref.locked)
cacheSizeElem.disabled = true;
},
/**
* Converts the cache size as specified in UI (in MB) to KB and returns that
* value.
* Updates the cache size preference once user enters a new value.
* We intentionally do not set preference="browser.cache.disk.capacity"
* onto the textbox directly, as that would update the pref at each keypress
* not only after the final value is entered.
*/
writeCacheSize: function ()
updateCacheSizePref()
{
var cacheSize = document.getElementById("cacheSize");
var intValue = parseInt(cacheSize.value, 10);
return isNaN(intValue) ? 0 : intValue * 1024;
let cacheSizeElem = document.getElementById("cacheSize");
let cachePref = document.getElementById("browser.cache.disk.capacity");
// Converts the cache size as specified in UI (in MB) to KB.
let intValue = parseInt(cacheSizeElem.value, 10);
cachePref.value = isNaN(intValue) ? 0 : intValue * 1024;
},
/**

View File

@ -271,10 +271,8 @@
&limitCacheSizeBefore.label;
</label>
<textbox id="cacheSize" type="number" size="4" max="1024"
preference="browser.cache.disk.capacity"
onsyncfrompreference="return gAdvancedPane.readCacheSize();"
onsynctopreference="return gAdvancedPane.writeCacheSize();"
aria-labelledby="useCacheBefore cacheSize useCacheAfter"/>
onchange="gAdvancedPane.updateCacheSizePref();"
aria-labelledby="useCacheBefore cacheSize useCacheAfter"/>
<label id="useCacheAfter" flex="1">&limitCacheSizeAfter.label;</label>
</hbox>
</groupbox>

View File

@ -690,9 +690,7 @@ StackFrames.prototype = {
let { depth, source, where: { line } } = frame;
let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false;
let location = NetworkHelper.convertToUnicode(unescape(source.url || source.introductionUrl));
let title = StackFrameUtils.getFrameTitle(frame);
DebuggerView.StackFrames.addFrame(title, location, line, depth, isBlackBoxed);
DebuggerView.StackFrames.addFrame(frame, line, depth, isBlackBoxed);
}
DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0);

View File

@ -422,7 +422,8 @@ let DebuggerView = {
deferred.resolve([aSource, aText, aContentType]);
},
([, aError]) => {
let msg = L10N.getStr("errorLoadingText") + DevToolsUtils.safeErrorString(aError);
let url = aError;
let msg = L10N.getFormatStr("errorLoadingText2", url);
this._setEditorText(msg);
Cu.reportError(msg);
dumpn(msg);

View File

@ -129,6 +129,7 @@ skip-if = e10s || true # bug 1113935
[browser_dbg_blackboxing-05.js]
[browser_dbg_blackboxing-06.js]
[browser_dbg_breadcrumbs-access.js]
[browser_dbg_break-in-anon.js]
[browser_dbg_break-on-dom-01.js]
[browser_dbg_break-on-dom-02.js]
[browser_dbg_break-on-dom-03.js]

View File

@ -0,0 +1,40 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure anonymous eval scripts can still break with a `debugger`
* statement
*/
const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
function test() {
let gTab, gPanel, gDebugger;
let gSources, gBreakpoints;
initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
gTab = aTab;
gPanel = aPanel;
gDebugger = gPanel.panelWin;
gSources = gDebugger.DebuggerView.Sources;
gBreakpoints = gDebugger.DebuggerController.Breakpoints;
return Task.spawn(function*() {
yield waitForSourceShown(gPanel, "-eval.js");
is(gSources.values.length, 1, "Should have 1 source");
let hasFrames = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED);
callInTab(gTab, "evalSourceWithDebugger");
yield hasFrames;
is(gSources.values.length, 2, "Should have 2 sources");
let item = gSources.getItemForAttachment(e => e.label.indexOf("SCRIPT") === 0);
ok(item, "Source label is incorrect.");
is(item.attachment.group, gDebugger.L10N.getStr('anonymousSourcesLabel'),
'Source group is incorrect');
yield resumeDebuggerThenCloseAndFinish(gPanel);
});
});
}

View File

@ -36,7 +36,7 @@ function showBogusSource() {
}
function testDebuggerLoadingError() {
ok(gEditor.getText().includes(gL10N.getStr("errorLoadingText")),
ok(gEditor.getText().includes(gL10N.getFormatStr("errorLoadingText2", "noSuchActor")),
"The valid error loading message is displayed.");
}

View File

@ -41,7 +41,7 @@ function performTest() {
is(gFrames.getItemAtIndex(1).attachment.title,
"(eval)", "Newest frame name should be correct.");
is(gFrames.getItemAtIndex(1).attachment.url,
TAB_URL, "Newest frame url should be correct.");
'SCRIPT0', "Newest frame url should be correct.");
is(gClassicFrames.getItemAtIndex(1).attachment.depth,
1, "Newest frame name is mirrored correctly.");

View File

@ -8,3 +8,7 @@ function evalSource() {
function evalSourceWithSourceURL() {
eval('bar = function() {\nvar x = 6;\n} //# sourceURL=bar.js');
}
function evalSourceWithDebugger() {
eval('bar = function() {\nvar x = 7;\ndebugger; }\n bar();');
}

View File

@ -9,7 +9,7 @@ const KNOWN_SOURCE_GROUPS = {
"Add-on SDK": "resource://gre/modules/commonjs/",
};
KNOWN_SOURCE_GROUPS[L10N.getStr("evalGroupLabel")] = "eval";
KNOWN_SOURCE_GROUPS[L10N.getStr("anonymousSourcesLabel")] = "anonymous";
/**
* Functions handling the sources UI.
@ -53,6 +53,7 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
showArrows: true
});
this._unnamedSourceIndex = 0;
this.emptyText = L10N.getStr("noSourcesText");
this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip");
@ -121,6 +122,11 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
this._newTabMenuItem.removeEventListener("command", this._onNewTabCommand, false);
},
empty: function() {
WidgetMethods.empty.call(this);
this._unnamedSourceIndex = 0;
},
/**
* Add commands that XUL can fire.
*/
@ -161,7 +167,7 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
* - staged: true to stage the item to be appended later
*/
addSource: function(aSource, aOptions = {}) {
if (!aSource.url) {
if (!aSource.url && !aOptions.force) {
// We don't show any unnamed eval scripts yet (see bug 1124106)
return;
}
@ -195,14 +201,24 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
_parseUrl: function(aSource) {
let fullUrl = aSource.url;
let url = fullUrl.split(" -> ").pop();
let label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url);
let group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url);
let url, unicodeUrl, label, group;
if(!fullUrl) {
unicodeUrl = 'SCRIPT' + this._unnamedSourceIndex++;
label = unicodeUrl;
group = L10N.getStr("anonymousSourcesLabel");
}
else {
let url = fullUrl.split(" -> ").pop();
label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url);
group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url);
unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl));
}
return {
label: label,
group: group,
unicodeUrl: NetworkHelper.convertToUnicode(unescape(fullUrl))
unicodeUrl: unicodeUrl
};
},
@ -642,6 +658,13 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
return aLocation.actor;
},
getDisplayURL: function(source) {
if(!source.url) {
return this.getItemByValue(source.actor).attachment.label;
}
return NetworkHelper.convertToUnicode(unescape(source.url))
},
/**
* Marks a breakpoint as selected in this sources container.
*

View File

@ -62,27 +62,40 @@ StackFramesView.prototype = Heritage.extend(WidgetMethods, {
* @param boolean aIsBlackBoxed
* Whether or not the frame is black boxed.
*/
addFrame: function(aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) {
addFrame: function(aFrame, aLine, aDepth, aIsBlackBoxed) {
let { source } = aFrame;
// The source may not exist in the source listing yet because it's
// an unnamed eval source, which we hide, so we need to add it
if(!DebuggerView.Sources.getItemByValue(source.actor)) {
DebuggerView.Sources.addSource(source, { force: true });
}
let location = DebuggerView.Sources.getDisplayURL(source);
let title = StackFrameUtils.getFrameTitle(aFrame);
// Blackboxed stack frames are collapsed into a single entry in
// the view. By convention, only the first frame is displayed.
if (aIsBlackBoxed) {
if (this._prevBlackBoxedUrl == aUrl) {
if (this._prevBlackBoxedUrl == location) {
return;
}
this._prevBlackBoxedUrl = aUrl;
this._prevBlackBoxedUrl = location;
} else {
this._prevBlackBoxedUrl = null;
}
// Create the element node for the stack frame item.
let frameView = this._createFrameView.apply(this, arguments);
let frameView = this._createFrameView(
title, location, aLine, aDepth, aIsBlackBoxed
);
// Append a stack frame item to this container.
this.push([frameView], {
index: 0, /* specifies on which position should the item be appended */
attachment: {
title: aTitle,
url: aUrl,
title: title,
url: location,
line: aLine,
depth: aDepth
},
@ -92,7 +105,7 @@ StackFramesView.prototype = Heritage.extend(WidgetMethods, {
});
// Mirror this newly inserted item inside the "Call Stack" tab.
this._mirror.addFrame(aTitle, aUrl, aLine, aDepth);
this._mirror.addFrame(title, location, aLine, aDepth);
},
/**

View File

@ -182,3 +182,7 @@ ul.children + .tag-line::before {
display: none;
cursor: pointer;
}
.editor.text {
display: inline-block;
}

View File

@ -874,7 +874,7 @@ MarkupView.prototype = {
console.warn("Could not expand the node, the markup-view was destroyed");
return;
}
aContainer.expanded = true;
aContainer.setExpanded(true);
});
},
@ -919,7 +919,7 @@ MarkupView.prototype = {
*/
collapseNode: function(aNode) {
let container = this.getContainer(aNode);
container.expanded = false;
container.setExpanded(false);
},
/**
@ -1264,11 +1264,40 @@ MarkupView.prototype = {
return promise.resolve(aContainer);
}
if (aContainer.singleTextChild
&& aContainer.singleTextChild != aContainer.node.singleTextChild) {
// This container was doing double duty as a container for a single
// text child, back that out.
this._containers.delete(aContainer.singleTextChild);
aContainer.clearSingleTextChild();
if (aContainer.hasChildren && aContainer.selected) {
aContainer.setExpanded(true);
}
}
if (aContainer.node.singleTextChild) {
aContainer.setExpanded(false);
// this container will do double duty as the container for the single
// text child.
while (aContainer.children.firstChild) {
aContainer.children.removeChild(aContainer.children.firstChild);
}
aContainer.setSingleTextChild(aContainer.node.singleTextChild);
this._containers.set(aContainer.node.singleTextChild, aContainer);
aContainer.childrenDirty = false;
return promise.resolve(aContainer);
}
if (!aContainer.hasChildren) {
while (aContainer.children.firstChild) {
aContainer.children.removeChild(aContainer.children.firstChild);
}
aContainer.childrenDirty = false;
aContainer.setExpanded(false);
return promise.resolve(aContainer);
}
@ -1705,11 +1734,22 @@ MarkupContainer.prototype = {
set hasChildren(aValue) {
this._hasChildren = aValue;
this.updateExpander();
},
/**
* True if the current node can be expanded.
*/
get canExpand() {
return this._hasChildren && !this.node.singleTextChild;
},
updateExpander: function() {
if (!this.expander) {
return;
}
if (aValue) {
if (this.canExpand) {
this.expander.style.visibility = "visible";
} else {
this.expander.style.visibility = "hidden";
@ -1735,11 +1775,15 @@ MarkupContainer.prototype = {
return !this.elt.classList.contains("collapsed");
},
set expanded(aValue) {
setExpanded: function(aValue) {
if (!this.expander) {
return;
}
if (!this.canExpand) {
aValue = false;
}
if (aValue && this.elt.classList.contains("collapsed")) {
// Expanding a node means cloning its "inline" closing tag into a new
// tag-line that the user can interact with and showing the children.
@ -1766,6 +1810,7 @@ MarkupContainer.prototype = {
} else if (!aValue) {
if (this.closeTagLine) {
this.elt.removeChild(this.closeTagLine);
this.closeTagLine = undefined;
}
this.elt.classList.add("collapsed");
this.expander.removeAttribute("open");
@ -1816,7 +1861,7 @@ MarkupContainer.prototype = {
// line. So, if the click happened outside of a focusable element, do
// prevent the default behavior, so that the tagname or textcontent gains
// focus.
if (!target.closest(".open [tabindex]")) {
if (!target.closest(".editor [tabindex]")) {
event.preventDefault();
}
@ -2172,6 +2217,16 @@ MarkupElementContainer.prototype = Heritage.extend(MarkupContainer.prototype, {
clipboardHelper.copyString(str, this.markup.doc);
});
});
},
setSingleTextChild: function(singleTextChild) {
this.singleTextChild = singleTextChild;
this.editor.updateTextEditor();
},
clearSingleTextChild: function() {
this.singleTextChild = undefined;
this.editor.updateTextEditor();
}
});
@ -2199,7 +2254,9 @@ RootContainer.prototype = {
*/
getChildContainers: function() {
return [...this.children.children].map(node => node.container);
}
},
setExpanded: function(aValue) {}
};
/**
@ -2302,6 +2359,7 @@ TextEditor.prototype = {
longstr.release().then(null, console.error);
if (this.selected) {
this.value.textContent = str;
this.markup.emit("text-expand")
}
}).then(null, console.error);
}
@ -2387,6 +2445,12 @@ function ElementEditor(aContainer, aNode) {
ElementEditor.prototype = {
set selected(aValue) {
if (this.textEditor) {
this.textEditor.selected = aValue;
}
},
flashAttribute: function(attrName) {
if (this.animationTimers[attrName]) {
clearTimeout(this.animationTimers[attrName]);
@ -2439,6 +2503,33 @@ ElementEditor.prototype = {
}
}
}
this.updateTextEditor();
},
/**
* Update the inline text editor in case of a single text child node.
*/
updateTextEditor: function() {
let node = this.node.singleTextChild;
if (this.textEditor && this.textEditor.node != node) {
this.elt.removeChild(this.textEditor.elt);
this.textEditor = null;
}
if (node && !this.textEditor) {
// Create a text editor added to this editor.
// This editor won't receive an update automatically, so we rely on
// child text editors to let us know that we need updating.
this.textEditor = new TextEditor(this.container, node, "text");
this.elt.insertBefore(this.textEditor.elt,
this.elt.firstChild.nextSibling.nextSibling);
}
if (this.textEditor) {
this.textEditor.update();
}
},
_startModifyingAttributes: function() {

View File

@ -72,11 +72,74 @@ const TEST_DATA = [
node1.textContent = "newtext";
},
check: function*(inspector) {
let {children} = yield getContainerForSelector("#node1", inspector);
is(children.querySelector(".text").textContent.trim(), "newtext",
"The new textcontent was updated");
let container = yield getContainerForSelector("#node1", inspector);
ok(container.singleTextChild, "Has single text child.");
ok(!container.canExpand, "Can't expand container with singleTextChild.");
ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild.");
is(container.editor.elt.querySelector(".text").textContent.trim(), "newtext",
"Single text child editor updated.");
}
},
{
desc: "Adding a second text child",
test: () => {
let node1 = getNode("#node1");
let newText = node1.ownerDocument.createTextNode("more");
node1.appendChild(newText);
},
check: function*(inspector) {
let container = yield getContainerForSelector("#node1", inspector);
ok(!container.singleTextChild, "Does not have single text child.");
ok(container.canExpand, "Can expand container with child nodes.");
ok(container.editor.elt.querySelector(".text") == null,
"Single text child editor removed.");
},
},
{
desc: "Go from 2 to 1 text child",
test: () => {
let node1 = getNode("#node1");
node1.textContent = "newtext";
},
check: function*(inspector) {
let container = yield getContainerForSelector("#node1", inspector);
ok(container.singleTextChild, "Has single text child.");
ok(!container.canExpand, "Can't expand container with singleTextChild.");
ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild.");
ok(container.editor.elt.querySelector(".text").textContent.trim(), "newtext",
"Single text child editor updated.");
},
},
{
desc: "Removing an only text child",
test: () => {
let node1 = getNode("#node1");
node1.innerHTML = "";
},
check: function*(inspector) {
let container = yield getContainerForSelector("#node1", inspector);
ok(!container.singleTextChild, "Does not have single text child.");
ok(!container.canExpand, "Can't expand empty container.");
ok(container.editor.elt.querySelector(".text") == null,
"Single text child editor removed.");
},
},
{
desc: "Go from 0 to 1 text child",
test: () => {
let node1 = getNode("#node1");
node1.textContent = "newtext";
},
check: function*(inspector) {
let container = yield getContainerForSelector("#node1", inspector);
ok(container.singleTextChild, "Has single text child.");
ok(!container.canExpand, "Can't expand container with singleTextChild.");
ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild.");
ok(container.editor.elt.querySelector(".text").textContent.trim(), "newtext",
"Single text child editor updated.");
},
},
{
desc: "Updating the innerHTML",
test: () => {
@ -150,17 +213,20 @@ const TEST_DATA = [
node20.appendChild(node18);
},
check: function*(inspector) {
yield inspector.markup.expandAll();
let {children} = yield getContainerForSelector("#node1", inspector);
is(children.childNodes.length, 2,
"Node1 now has 2 children (textnode and node20)");
let node20 = children.childNodes[1];
let node20Children = node20.querySelector(".children")
is(node20Children.childNodes.length, 2, "Node20 has 2 children (21 and 18)");
let node20Children = node20.container.children;
is(node20Children.childNodes.length, 2,
"Node20 has 2 children (21 and 18)");
let node21 = node20Children.childNodes[0];
is(node21.querySelector(".children").textContent.trim(), "line21",
"Node21 only has a text node child");
is(node21.container.editor.elt.querySelector(".text").textContent.trim(), "line21",
"Node21 has a single text child");
let node18 = node20Children.childNodes[1];
is(node18.querySelector(".open .attreditor .attr-value").textContent.trim(),

View File

@ -33,8 +33,6 @@ const TEST_DATA = [
["right", "node7"],
["right", "*text*"],
["down", "node8"],
["right", "node8"],
["left", "node8"],
["down", "node9"],
["down", "node10"],
["down", "node11"],

View File

@ -15,17 +15,60 @@ add_task(function*() {
yield inspector.markup.expandAll();
yield waitForMultipleChildrenUpdates(inspector);
let node = getNode(".node6").firstChild;
is(node.nodeValue, "line6", "The test node's text content is correct");
yield editContainer(inspector, {
selector: ".node6",
newValue: "New text",
oldValue: "line6"
});
yield editContainer(inspector, {
selector: "#node17",
newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.",
oldValue: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere placerat magna et imperdiet.",
shortValue: true
});
yield editContainer(inspector, {
selector: "#node17",
newValue: "New value",
oldValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.",
shortValue: true
});
});
function* editContainer(inspector, {selector, newValue, oldValue, shortValue}) {
let node = getNode(selector).firstChild;
is(node.nodeValue, oldValue, "The test node's text content is correct");
info("Changing the text content");
let onMutated = inspector.once("markupmutation");
let container = yield getContainerForSelector(selector, inspector);
let field = container.elt.querySelector("pre");
if (shortValue) {
is (oldValue.indexOf(field.textContent.substring(0, field.textContent.length - 1)), 0,
"The shortened value starts with the full value " + field.textContent);
ok (oldValue.length > field.textContent.length, "The shortened value is short");
} else {
is (field.textContent, oldValue, "The text node has the correct original value");
}
inspector.markup.markNodeAsSelected(container.node);
if (shortValue) {
info("Waiting for the text to be updated");
yield inspector.markup.once("text-expand");
}
is (field.textContent, oldValue, "The text node has the correct original value after selecting");
setEditableFieldValue(field, newValue, inspector);
info("Listening to the markupmutation event");
let onMutated = inspector.once("markupmutation");
let container = yield getContainerForSelector(".node6", inspector);
let field = container.elt.querySelector("pre");
setEditableFieldValue(field, "New text", inspector);
yield onMutated;
is(node.nodeValue, "New text", "Test test node's text content has changed");
});
is(node.nodeValue, newValue, "The test node's text content has changed");
info("Selecting the <body> to reset the selection");
let bodyContainer = yield getContainerForSelector("body", inspector);
inspector.markup.markNodeAsSelected(bodyContainer.node);
}

View File

@ -20,7 +20,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=858038
</div>
<input id="anonymousParent" />
<span id="before">Before</span>
<span id="before">Before<!-- Force not-inline --></span>
<pre id="test">
<span id="firstChild">First</span>
<span id="middleChild">Middle</span>

View File

@ -22,7 +22,7 @@
<p class="node15">line15</p>
</div>
<div id="node16">
<p id="node17">line17</p>
<p id="node17">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere placerat magna et imperdiet.</p>
</div>
<div id="node18">
<div id="node19">

View File

@ -6,23 +6,23 @@
<body>
<ul>
<li>
<span>list <em>item</em></span>
<span>list <em>item<!-- force expand --></em></span>
</li>
<li>
<span>list <em>item</em></span>
<span>list <em>item<!-- force expand --></em></span>
</li>
<li>
<span>list <em>item</em></span>
<span>list <em>item<!-- force expand --></em></span>
</li>
<li>
<span>list <em>item</em></span>
<span>list <em>item<!-- force expand --></em></span>
</li>
<li>
<span>list <em>item</em></span>
<span>list <em>item<!-- force expand --></em></span>
</li>
<li>
<span>list <em>item</em></span>
<span>list <em>item<!-- force expand --></em></span>
</li>
</ul>
</body>
</html>
</html>

View File

@ -69,9 +69,15 @@ Cu.import("resource://gre/modules/devtools/event-emitter.js");
* This function is called before the editor has been torn down.
* {function} destroy:
* Called when the editor is destroyed and has been torn down.
* {string} advanceChars:
* If any characters in advanceChars are typed, focus will advance
* to the next element.
* {object} advanceChars:
* This can be either a string or a function.
* If it is a string, then if any characters in it are typed,
* focus will advance to the next element.
* Otherwise, if it is a function, then the function will
* be called with three arguments: a key code, the current text,
* and the insertion point. If the function returns true,
* then the focus advance takes place. If it returns false,
* then the character is inserted instead.
* {boolean} stopOnReturn:
* If true, the return key will not advance the editor to the next
* focusable element.
@ -212,10 +218,15 @@ function InplaceEditor(aOptions, aEvent)
// Pull out character codes for advanceChars, listing the
// characters that should trigger a blur.
this._advanceCharCodes = {};
let advanceChars = aOptions.advanceChars || '';
for (let i = 0; i < advanceChars.length; i++) {
this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
if (typeof(aOptions.advanceChars) === "function") {
this._advanceChars = aOptions.advanceChars;
} else {
let advanceCharcodes = {};
let advanceChars = aOptions.advanceChars || '';
for (let i = 0; i < advanceChars.length; i++) {
advanceCharcodes[advanceChars.charCodeAt(i)] = true;
}
this._advanceChars = aCharCode => aCharCode in advanceCharcodes;
}
// Hide the provided element and add our editor.
@ -931,7 +942,8 @@ InplaceEditor.prototype = {
aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
aEvent.shiftKey) {
prevent = false;
} else if (aEvent.charCode in this._advanceCharCodes
} else if (this._advanceChars(aEvent.charCode, this.input.value,
this.input.selectionStart)
|| aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
|| aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
prevent = true;

View File

@ -60,9 +60,7 @@ EXTRA_JS_MODULES.devtools.shared += [
'telemetry.js',
'theme-switching.js',
'theme.js',
'undo.js',
'worker-helper.js',
'worker.js',
'undo.js'
]
EXTRA_JS_MODULES.devtools.shared.widgets += [

View File

@ -24,9 +24,6 @@ support-files =
[browser_cubic-bezier-04.js]
[browser_cubic-bezier-05.js]
[browser_cubic-bezier-06.js]
[browser_devtools-worker-01.js]
[browser_devtools-worker-02.js]
[browser_devtools-worker-03.js]
[browser_filter-editor-01.js]
[browser_filter-editor-02.js]
[browser_filter-editor-03.js]

View File

@ -17,6 +17,7 @@ add_task(function*() {
yield testReturnCommit(doc);
yield testBlurCommit(doc);
yield testAdvanceCharCommit(doc);
yield testAdvanceCharsFunction(doc);
host.destroy();
gBrowser.removeCurrentTab();
@ -91,6 +92,37 @@ function testAdvanceCharCommit(doc) {
return def.promise;
}
function testAdvanceCharsFunction(doc) {
info("Testing advanceChars as a function");
let def = promise.defer();
let firstTime = true;
createInplaceEditorAndClick({
initial: "",
advanceChars: function(aCharCode, aText, aInsertionPoint) {
if (aCharCode !== Components.interfaces.nsIDOMKeyEvent.DOM_VK_COLON) {
return false;
}
if (firstTime) {
firstTime = false;
return false;
}
// Just to make sure we check it somehow.
return aText.length > 0;
},
start: function(editor) {
for each (let ch in ":Test:") {
EventUtils.sendChar(ch);
}
},
done: onDone(":Test", true, def)
}, doc);
return def.promise;
}
function testEscapeCancel(doc) {
info("Testing that escape cancels the new value");
let def = promise.defer();

View File

@ -0,0 +1,33 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests the advanceValidate function from rule-view.js.
const Cu = Components.utils;
const Ci = Components.interfaces;
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let require = devtools.require;
let {_advanceValidate} = require("devtools/styleinspector/rule-view");
// 1 2 3
// 0123456789012345678901234567890
let sampleInput = '\\symbol "string" url(somewhere)';
function testInsertion(where, result, testName) {
do_print(testName);
equal(_advanceValidate(Ci.nsIDOMKeyEvent.DOM_VK_SEMICOLON, sampleInput, where),
result, "testing _advanceValidate at " + where);
}
function run_test() {
testInsertion(4, true, "inside a symbol");
testInsertion(1, false, "after a backslash");
testInsertion(8, true, "after whitespace");
testInsertion(11, false, "inside a string");
testInsertion(24, false, "inside a URL");
testInsertion(31, true, "at the end");
}

View File

@ -5,6 +5,7 @@ tail =
firefox-appdir = browser
skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_advanceValidate.js]
[test_attribute-parsing-01.js]
[test_attribute-parsing-02.js]
[test_bezierCanvas.js]

View File

@ -9,7 +9,7 @@ Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
const {DevToolsWorker} = Cu.import("resource:///modules/devtools/shared/worker.js", {});
const {DevToolsWorker} = Cu.import("resource://gre/modules/devtools/shared/worker.js", {});
this.EXPORTED_SYMBOLS = [
"GraphCursor",

View File

@ -4,10 +4,10 @@
"use strict";
/**
* Import `createTask` to communicate with `devtools/shared/worker`.
* Import `createTask` to communicate with `devtools/toolkit/shared/worker`.
*/
importScripts("resource://gre/modules/workers/require.js");
const { createTask } = require("resource:///modules/devtools/shared/worker-helper");
const { createTask } = require("resource://gre/modules/devtools/shared/worker-helper");
/**
* @see LineGraphWidget.prototype.setDataFromTimestamps in Graphs.jsm

View File

@ -3,7 +3,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { Cc, Ci, Cu } = require('chrome');
const cssTokenizer = require("devtools/sourceeditor/css-tokenizer");
const {cssTokenizer, cssTokenizerWithLineColumn} =
require("devtools/sourceeditor/css-tokenizer");
const promise = Cu.import("resource://gre/modules/Promise.jsm");
/**
@ -180,18 +181,17 @@ CSSCompleter.prototype = {
else {
this.nullStates = [];
}
let tokens = cssTokenizer(source, {loc:true});
let tokens = cssTokenizerWithLineColumn(source);
let tokIndex = tokens.length - 1;
if (tokens[tokIndex].loc.end.line < line ||
(tokens[tokIndex].loc.end.line === line &&
tokens[tokIndex].loc.end.column < ch)) {
// If the last token is not an EOF, we didn't tokenize it correctly.
// This special case is handled in case we couldn't tokenize, but the last
// token that *could be tokenized* was an identifier.
if (tokIndex >=0 &&
(tokens[tokIndex].loc.end.line < line ||
(tokens[tokIndex].loc.end.line === line &&
tokens[tokIndex].loc.end.column < ch))) {
// If the last token ends before the cursor location, we didn't
// tokenize it correctly. This special case can happen if the
// final token is a comment.
return null;
}
// Since last token is EOF, the cursor token is last - 1
tokIndex--;
let cursor = 0;
// This will maintain a stack of paired elements like { & }, @m & }, : & ; etc
@ -202,13 +202,14 @@ CSSCompleter.prototype = {
case CSS_STATES.property:
// From CSS_STATES.property, we can either go to CSS_STATES.value state
// when we hit the first ':' or CSS_STATES.selector if "}" is reached.
switch(token.tokenType) {
if (token.tokenType === "symbol") {
switch(token.text) {
case ":":
scopeStack.push(":");
if (tokens[cursor - 2].tokenType != "WHITESPACE")
propertyName = tokens[cursor - 2].value;
if (tokens[cursor - 2].tokenType != "whitespace")
propertyName = tokens[cursor - 2].text;
else
propertyName = tokens[cursor - 3].value;
propertyName = tokens[cursor - 3].text;
_state = CSS_STATES.value;
break;
@ -224,13 +225,15 @@ CSSCompleter.prototype = {
}
}
break;
}
}
break;
case CSS_STATES.value:
// From CSS_STATES.value, we can go to one of CSS_STATES.property,
// CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null
switch(token.tokenType) {
if (token.tokenType === "symbol") {
switch(token.text) {
case ";":
if (/[:]/.test(peek(scopeStack))) {
scopeStack.pop();
@ -253,13 +256,14 @@ CSSCompleter.prototype = {
}
}
break;
}
}
break;
case CSS_STATES.selector:
// From CSS_STATES.selector, we can only go to CSS_STATES.property when
// we hit "{"
if (token.tokenType == "{") {
if (token.tokenType === "symbol" && token.text == "{") {
scopeStack.push("{");
_state = CSS_STATES.property;
selectors.push(selector);
@ -271,76 +275,71 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.class:
case SELECTOR_STATES.tag:
switch(token.tokenType) {
case "HASH":
case "hash":
case "id":
selectorState = SELECTOR_STATES.id;
selector += "#" + token.value;
selector += "#" + token.text;
break;
case "DELIM":
if (token.value == ".") {
case "symbol":
if (token.text == ".") {
selectorState = SELECTOR_STATES.class;
selector += ".";
if (cursor <= tokIndex &&
tokens[cursor].tokenType == "IDENT") {
tokens[cursor].tokenType == "ident") {
token = tokens[cursor++];
selector += token.value;
selector += token.text;
}
} else if (token.value == "#") {
} else if (token.text == "#") {
selectorState = SELECTOR_STATES.id;
selector += "#";
} else if (/[>~+]/.test(token.value)) {
} else if (/[>~+]/.test(token.text)) {
selectorState = SELECTOR_STATES.null;
selector += token.value;
} else if (token.value == ",") {
selector += token.text;
} else if (token.text == ",") {
selectorState = SELECTOR_STATES.null;
selectors.push(selector);
selector = "";
}
break;
case ":":
selectorState = SELECTOR_STATES.pseudo;
selector += ":";
if (cursor > tokIndex)
break;
token = tokens[cursor++];
switch(token.tokenType) {
case "FUNCTION":
if (token.value == "not") {
selectorBeforeNot = selector;
selector = "";
scopeStack.push("(");
} else {
selector += token.value + "(";
}
selectorState = SELECTOR_STATES.null;
} else if (token.text == ":") {
selectorState = SELECTOR_STATES.pseudo;
selector += ":";
if (cursor > tokIndex)
break;
case "IDENT":
selector += token.value;
break;
token = tokens[cursor++];
switch(token.tokenType) {
case "function":
if (token.text == "not") {
selectorBeforeNot = selector;
selector = "";
scopeStack.push("(");
} else {
selector += token.text + "(";
}
selectorState = SELECTOR_STATES.null;
break;
case "ident":
selector += token.text;
break;
}
} else if (token.text == "[") {
selectorState = SELECTOR_STATES.attribute;
scopeStack.push("[");
selector += "[";
} else if (token.text == ")") {
if (peek(scopeStack) == "(") {
scopeStack.pop();
selector = selectorBeforeNot + "not(" + selector + ")";
selectorBeforeNot = null;
} else {
selector += ")";
}
selectorState = SELECTOR_STATES.null;
}
break;
case "[":
selectorState = SELECTOR_STATES.attribute;
scopeStack.push("[");
selector += "[";
break;
case ")":
if (peek(scopeStack) == "(") {
scopeStack.pop();
selector = selectorBeforeNot + "not(" + selector + ")";
selectorBeforeNot = null;
} else {
selector += ")";
}
selectorState = SELECTOR_STATES.null;
break;
case "WHITESPACE":
case "whitespace":
selectorState = SELECTOR_STATES.null;
selector && (selector += " ");
break;
@ -351,83 +350,78 @@ CSSCompleter.prototype = {
// From SELECTOR_STATES.null state, we can go to one of
// SELECTOR_STATES.id, SELECTOR_STATES.class or SELECTOR_STATES.tag
switch(token.tokenType) {
case "HASH":
case "hash":
case "id":
selectorState = SELECTOR_STATES.id;
selector += "#" + token.value;
selector += "#" + token.text;
break;
case "IDENT":
case "ident":
selectorState = SELECTOR_STATES.tag;
selector += token.value;
selector += token.text;
break;
case "DELIM":
if (token.value == ".") {
case "symbol":
if (token.text == ".") {
selectorState = SELECTOR_STATES.class;
selector += ".";
if (cursor <= tokIndex &&
tokens[cursor].tokenType == "IDENT") {
tokens[cursor].tokenType == "ident") {
token = tokens[cursor++];
selector += token.value;
selector += token.text;
}
} else if (token.value == "#") {
} else if (token.text == "#") {
selectorState = SELECTOR_STATES.id;
selector += "#";
} else if (token.value == "*") {
} else if (token.text == "*") {
selectorState = SELECTOR_STATES.tag;
selector += "*";
} else if (/[>~+]/.test(token.value)) {
selector += token.value;
} else if (token.value == ",") {
} else if (/[>~+]/.test(token.text)) {
selector += token.text;
} else if (token.text == ",") {
selectorState = SELECTOR_STATES.null;
selectors.push(selector);
selector = "";
}
break;
case ":":
selectorState = SELECTOR_STATES.pseudo;
selector += ":";
if (cursor > tokIndex)
break;
token = tokens[cursor++];
switch(token.tokenType) {
case "FUNCTION":
if (token.value == "not") {
selectorBeforeNot = selector;
selector = "";
scopeStack.push("(");
} else {
selector += token.value + "(";
}
selectorState = SELECTOR_STATES.null;
} else if (token.text == ":") {
selectorState = SELECTOR_STATES.pseudo;
selector += ":";
if (cursor > tokIndex)
break;
case "IDENT":
selector += token.value;
break;
token = tokens[cursor++];
switch(token.tokenType) {
case "function":
if (token.text == "not") {
selectorBeforeNot = selector;
selector = "";
scopeStack.push("(");
} else {
selector += token.text + "(";
}
selectorState = SELECTOR_STATES.null;
break;
case "ident":
selector += token.text;
break;
}
} else if (token.text == "[") {
selectorState = SELECTOR_STATES.attribute;
scopeStack.push("[");
selector += "[";
} else if (token.text == ")") {
if (peek(scopeStack) == "(") {
scopeStack.pop();
selector = selectorBeforeNot + "not(" + selector + ")";
selectorBeforeNot = null;
} else {
selector += ")";
}
selectorState = SELECTOR_STATES.null;
}
break;
case "[":
selectorState = SELECTOR_STATES.attribute;
scopeStack.push("[");
selector += "[";
break;
case ")":
if (peek(scopeStack) == "(") {
scopeStack.pop();
selector = selectorBeforeNot + "not(" + selector + ")";
selectorBeforeNot = null;
} else {
selector += ")";
}
selectorState = SELECTOR_STATES.null;
break;
case "WHITESPACE":
case "whitespace":
selector && (selector += " ");
break;
}
@ -435,49 +429,45 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.pseudo:
switch(token.tokenType) {
case "DELIM":
if (/[>~+]/.test(token.value)) {
case "symbol":
if (/[>~+]/.test(token.text)) {
selectorState = SELECTOR_STATES.null;
selector += token.value;
} else if (token.value == ",") {
selector += token.text;
} else if (token.text == ",") {
selectorState = SELECTOR_STATES.null;
selectors.push(selector);
selector = "";
} else if (token.text == ":") {
selectorState = SELECTOR_STATES.pseudo;
selector += ":";
if (cursor > tokIndex)
break;
token = tokens[cursor++];
switch(token.tokenType) {
case "function":
if (token.text == "not") {
selectorBeforeNot = selector;
selector = "";
scopeStack.push("(");
} else {
selector += token.text + "(";
}
selectorState = SELECTOR_STATES.null;
break;
case "ident":
selector += token.text;
break;
}
} else if (token.text == "[") {
selectorState = SELECTOR_STATES.attribute;
scopeStack.push("[");
selector += "[";
}
break;
case ":":
selectorState = SELECTOR_STATES.pseudo;
selector += ":";
if (cursor > tokIndex)
break;
token = tokens[cursor++];
switch(token.tokenType) {
case "FUNCTION":
if (token.value == "not") {
selectorBeforeNot = selector;
selector = "";
scopeStack.push("(");
} else {
selector += token.value + "(";
}
selectorState = SELECTOR_STATES.null;
break;
case "IDENT":
selector += token.value;
break;
}
break;
case "[":
selectorState = SELECTOR_STATES.attribute;
scopeStack.push("[");
selector += "[";
break;
case "WHITESPACE":
case "whitespace":
selectorState = SELECTOR_STATES.null;
selector && (selector += " ");
break;
@ -486,31 +476,28 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.attribute:
switch(token.tokenType) {
case "DELIM":
if (/[~|^$*]/.test(token.value)) {
selector += token.value;
case "symbol":
if (/[~|^$*]/.test(token.text)) {
selector += token.text;
token = tokens[cursor++];
}
if(token.value == "=") {
} else if (token.text == "=") {
selectorState = SELECTOR_STATES.value;
selector += token.value;
selector += token.text;
} else if (token.text == "]") {
if (peek(scopeStack) == "[")
scopeStack.pop();
selectorState = SELECTOR_STATES.null;
selector += "]";
}
break;
case "IDENT":
case "STRING":
selector += token.value;
case "ident":
case "string":
selector += token.text;
break;
case "]":
if (peek(scopeStack) == "[")
scopeStack.pop();
selectorState = SELECTOR_STATES.null;
selector += "]";
break;
case "WHITESPACE":
case "whitespace":
selector && (selector += " ");
break;
}
@ -518,20 +505,22 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.value:
switch(token.tokenType) {
case "STRING":
case "IDENT":
selector += token.value;
case "string":
case "ident":
selector += token.text;
break;
case "]":
if (peek(scopeStack) == "[")
scopeStack.pop();
case "symbol":
if (token.text == "]") {
if (peek(scopeStack) == "[")
scopeStack.pop();
selectorState = SELECTOR_STATES.null;
selector += "]";
selectorState = SELECTOR_STATES.null;
selector += "]";
}
break;
case "WHITESPACE":
case "whitespace":
selector && (selector += " ");
break;
}
@ -543,89 +532,83 @@ CSSCompleter.prototype = {
// From CSS_STATES.null state, we can go to either CSS_STATES.media or
// CSS_STATES.selector.
switch(token.tokenType) {
case "HASH":
case "hash":
case "id":
selectorState = SELECTOR_STATES.id;
selector = "#" + token.value;
selector = "#" + token.text;
_state = CSS_STATES.selector;
break;
case "IDENT":
case "ident":
selectorState = SELECTOR_STATES.tag;
selector = token.value;
selector = token.text;
_state = CSS_STATES.selector;
break;
case "DELIM":
if (token.value == ".") {
case "symbol":
if (token.text == ".") {
selectorState = SELECTOR_STATES.class;
selector = ".";
_state = CSS_STATES.selector;
if (cursor <= tokIndex &&
tokens[cursor].tokenType == "IDENT") {
tokens[cursor].tokenType == "ident") {
token = tokens[cursor++];
selector += token.value;
selector += token.text;
}
} else if (token.value == "#") {
} else if (token.text == "#") {
selectorState = SELECTOR_STATES.id;
selector = "#";
_state = CSS_STATES.selector;
} else if (token.value == "*") {
} else if (token.text == "*") {
selectorState = SELECTOR_STATES.tag;
selector = "*";
_state = CSS_STATES.selector;
} else if (token.text == ":") {
_state = CSS_STATES.selector;
selectorState = SELECTOR_STATES.pseudo;
selector += ":";
if (cursor > tokIndex)
break;
token = tokens[cursor++];
switch(token.tokenType) {
case "function":
if (token.text == "not") {
selectorBeforeNot = selector;
selector = "";
scopeStack.push("(");
} else {
selector += token.text + "(";
}
selectorState = SELECTOR_STATES.null;
break;
case "ident":
selector += token.text;
break;
}
} else if (token.text == "[") {
_state = CSS_STATES.selector;
selectorState = SELECTOR_STATES.attribute;
scopeStack.push("[");
selector += "[";
} else if (token.text == "}") {
if (peek(scopeStack) == "@m")
scopeStack.pop();
}
break;
case ":":
_state = CSS_STATES.selector;
selectorState = SELECTOR_STATES.pseudo;
selector += ":";
if (cursor > tokIndex)
break;
token = tokens[cursor++];
switch(token.tokenType) {
case "FUNCTION":
if (token.value == "not") {
selectorBeforeNot = selector;
selector = "";
scopeStack.push("(");
} else {
selector += token.value + "(";
}
selectorState = SELECTOR_STATES.null;
break;
case "IDENT":
selector += token.value;
break;
}
break;
case "[":
_state = CSS_STATES.selector;
selectorState = SELECTOR_STATES.attribute;
scopeStack.push("[");
selector += "[";
break;
case "AT-KEYWORD":
_state = token.value.startsWith("m") ? CSS_STATES.media
case "at":
_state = token.text.startsWith("m") ? CSS_STATES.media
: CSS_STATES.keyframes;
break;
case "}":
if (peek(scopeStack) == "@m")
scopeStack.pop();
break;
}
break;
case CSS_STATES.media:
// From CSS_STATES.media, we can only go to CSS_STATES.null state when
// we hit the first '{'
if (token.tokenType == "{") {
if (token.tokenType == "symbol" && token.text == "{") {
scopeStack.push("@m");
_state = CSS_STATES.null;
}
@ -634,7 +617,7 @@ CSSCompleter.prototype = {
case CSS_STATES.keyframes:
// From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state
// when we hit the first '{'
if (token.tokenType == "{") {
if (token.tokenType == "symbol" && token.text == "{") {
scopeStack.push("@k");
_state = CSS_STATES.frame;
}
@ -643,14 +626,16 @@ CSSCompleter.prototype = {
case CSS_STATES.frame:
// From CSS_STATES.frame, we can either go to CSS_STATES.property state
// when we hit the first '{' or to CSS_STATES.selector when we hit '}'
if (token.tokenType == "{") {
scopeStack.push("f");
_state = CSS_STATES.property;
} else if (token.tokenType == "}") {
if (peek(scopeStack) == "@k")
scopeStack.pop();
if (token.tokenType == "symbol") {
if (token.text == "{") {
scopeStack.push("f");
_state = CSS_STATES.property;
} else if (token.text == "}") {
if (peek(scopeStack) == "@k")
scopeStack.pop();
_state = CSS_STATES.null;
_state = CSS_STATES.null;
}
}
break;
}
@ -682,10 +667,14 @@ CSSCompleter.prototype = {
}
this.selectors = selectors;
if (token && token.tokenType != "WHITESPACE") {
this.completing = ((token.value || token.repr || token.tokenType) + "")
.slice(0, ch - token.loc.start.column)
.replace(/^[.#]$/, "");
if (token && token.tokenType != "whitespace") {
let text;
if (token.tokenType == "dimension" || !token.text)
text = source.substring(token.startOffset, token.endOffset);
else
text = token.text;
this.completing = (text.slice(0, ch - token.loc.start.column)
.replace(/^[.#]$/, ""));
} else {
this.completing = "";
}
@ -695,7 +684,7 @@ CSSCompleter.prototype = {
this.completing = "";
// Special check for !important; case.
if (token && tokens[cursor - 2] && tokens[cursor - 2].value == "!" &&
if (token && tokens[cursor - 2] && tokens[cursor - 2].text == "!" &&
this.completing == "important".slice(0, this.completing.length)) {
this.completing = "!" + this.completing;
}
@ -989,38 +978,41 @@ CSSCompleter.prototype = {
if (line == caret.line)
lineText = lineText.substring(caret.ch);
let tokens = cssTokenizer(lineText, {loc: true});
let prevToken = undefined;
let tokens = cssTokenizer(lineText);
let found = false;
let ech = line == caret.line ? caret.ch : 0;
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
for (let token of tokens) {
// If the line is completely spaces, handle it differently
if (lineText.trim() == "") {
limitedSource += lineText;
} else {
limitedSource += sourceArray[line]
.substring(ech + token.loc.start.column,
ech + token.loc.end.column);
.substring(ech + token.startOffset,
ech + token.endOffset);
}
// Whitespace cannot change state.
if (token.tokenType == "WHITESPACE")
if (token.tokenType == "whitespace") {
prevToken = token;
continue;
}
let state = this.resolveState(limitedSource, {
line: line,
ch: token.loc.end.column + ech
ch: token.endOffset + ech
});
if (check(state)) {
if (tokens[i - 1] && tokens[i - 1].tokenType == "WHITESPACE")
token = tokens[i - 1];
if (prevToken && prevToken.tokenType == "whitespace")
token = prevToken;
location = {
line: line,
ch: token.loc.start.column + ech
ch: token.startOffset + ech
};
found = true;
break;
}
prevToken = token;
}
limitedSource += "\n";
if (found)
@ -1047,33 +1039,33 @@ CSSCompleter.prototype = {
if (line == caret.line)
lineText = lineText.substring(0, caret.ch);
let tokens = cssTokenizer(lineText, {loc: true});
let tokens = Array.from(cssTokenizer(lineText));
let found = false;
let ech = 0;
for (let i = tokens.length - 2; i >= 0; i--) {
for (let i = tokens.length - 1; i >= 0; i--) {
let token = tokens[i];
// If the line is completely spaces, handle it differently
if (lineText.trim() == "") {
limitedSource = limitedSource.slice(0, -1 * lineText.length);
} else {
let length = token.loc.end.column - token.loc.start.column;
let length = token.endOffset - token.startOffset;
limitedSource = limitedSource.slice(0, -1 * length);
}
// Whitespace cannot change state.
if (token.tokenType == "WHITESPACE")
if (token.tokenType == "whitespace")
continue;
let state = this.resolveState(limitedSource, {
line: line,
ch: token.loc.start.column
ch: token.startOffset
});
if (check(state)) {
if (tokens[i + 1] && tokens[i + 1].tokenType == "WHITESPACE")
if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace")
token = tokens[i + 1];
location = {
line: line,
ch: isValue ? token.loc.end.column: token.loc.start.column
ch: isValue ? token.endOffset: token.startOffset
};
found = true;
break;
@ -1121,21 +1113,23 @@ CSSCompleter.prototype = {
}
else if (state == CSS_STATES.property) {
// A property can only be a single word and thus very easy to calculate.
let tokens = cssTokenizer(sourceArray[line], {loc: true});
let tokens = cssTokenizer(sourceArray[line]);
for (let token of tokens) {
if (token.loc.start.column <= ch && token.loc.end.column >= ch) {
// Note that, because we're tokenizing a single line, the
// token's offset is also the column number.
if (token.startOffset <= ch && token.endOffset >= ch) {
return {
state: state,
propertyName: token.value,
propertyName: token.text,
selectors: this.selectors,
loc: {
start: {
line: line,
ch: token.loc.start.column
ch: token.startOffset
},
end: {
line: line,
ch: token.loc.end.column
ch: token.endOffset
}
}
};

View File

@ -1,717 +1,95 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cc, Ci} = require("chrome");
loader.lazyGetter(this, "DOMUtils", () => {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});
/**
* This file is taken from the below mentioned url and is under CC0 license.
* https://github.com/tabatkins/css-parser/blob/master/tokenizer.js
* Please retain this comment while updating this file from upstream.
* A generator function that lexes a CSS source string, yielding the
* CSS tokens. Comment tokens are dropped.
*
* @param {String} CSS source string
* @yield {CSSToken} The next CSSToken that is lexed
* @see CSSToken for details about the returned tokens
*/
(function (root, factory) {
// Universal Module Definition (UMD) to support AMD, CommonJS/Node.js,
// Rhino, and plain browser loading.
if (typeof define === 'function' && define.amd) {
define(['exports'], factory);
} else if (typeof exports !== 'undefined') {
factory(exports);
} else {
factory(root);
function* cssTokenizer(string) {
let lexer = DOMUtils.getCSSLexer(string);
while (true) {
let token = lexer.nextToken();
if (!token) {
break;
}
}(this, function (exports) {
var between = function (num, first, last) { return num >= first && num <= last; }
function digit(code) { return between(code, 0x30,0x39); }
function hexdigit(code) { return digit(code) || between(code, 0x41,0x46) || between(code, 0x61,0x66); }
function uppercaseletter(code) { return between(code, 0x41,0x5a); }
function lowercaseletter(code) { return between(code, 0x61,0x7a); }
function letter(code) { return uppercaseletter(code) || lowercaseletter(code); }
function nonascii(code) { return code >= 0xa0; }
function namestartchar(code) { return letter(code) || nonascii(code) || code == 0x5f; }
function namechar(code) { return namestartchar(code) || digit(code) || code == 0x2d; }
function nonprintable(code) { return between(code, 0,8) || between(code, 0xe,0x1f) || between(code, 0x7f,0x9f); }
function newline(code) { return code == 0xa || code == 0xc; }
function whitespace(code) { return newline(code) || code == 9 || code == 0x20; }
function badescape(code) { return newline(code) || isNaN(code); }
// Note: I'm not yet acting smart enough to actually handle astral characters.
var maximumallowedcodepoint = 0x10ffff;
function tokenize(str, options) {
if(options == undefined) options = {transformFunctionWhitespace:false, scientificNotation:false};
var i = -1;
var tokens = [];
var state = "data";
var code;
var currtoken;
// Line number information.
var line = 0;
var column = 0;
// The only use of lastLineLength is in reconsume().
var lastLineLength = 0;
var incrLineno = function() {
line += 1;
lastLineLength = column;
column = 0;
};
var locStart = {line:line, column:column};
var next = function(num) { if(num === undefined) num = 1; return str.charCodeAt(i+num); };
var consume = function(num) {
if(num === undefined)
num = 1;
i += num;
code = str.charCodeAt(i);
if (newline(code)) incrLineno();
else column += num;
//console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16));
return true;
};
var reconsume = function() {
i -= 1;
if (newline(code)) {
line -= 1;
column = lastLineLength;
} else {
column -= 1;
}
locStart.line = line;
locStart.column = column;
return true;
};
var eof = function() { return i >= str.length; };
var donothing = function() {};
var emit = function(token) {
if(token) {
token.finish();
} else {
token = currtoken.finish();
}
if (options.loc === true) {
token.loc = {};
token.loc.start = {line:locStart.line, column:locStart.column};
locStart = {line: line, column: column};
token.loc.end = locStart;
}
tokens.push(token);
//console.log('Emitting ' + token);
currtoken = undefined;
return true;
};
var create = function(token) { currtoken = token; return true; };
var parseerror = function() { console.log("Parse error at index " + i + ", processing codepoint 0x" + code.toString(16) + " in state " + state + ".");return true; };
var switchto = function(newstate) {
state = newstate;
//console.log('Switching to ' + state);
return true;
};
var consumeEscape = function() {
// Assume the the current character is the \
consume();
if(hexdigit(code)) {
// Consume 1-6 hex digits
var digits = [];
for(var total = 0; total < 6; total++) {
if(hexdigit(code)) {
digits.push(code);
consume();
} else { break; }
}
var value = parseInt(digits.map(String.fromCharCode).join(''), 16);
if( value > maximumallowedcodepoint ) value = 0xfffd;
// If the current char is whitespace, cool, we'll just eat it.
// Otherwise, put it back.
if(!whitespace(code)) reconsume();
return value;
} else {
return code;
}
};
for(;;) {
if(i > str.length*2) return "I'm infinite-looping!";
consume();
switch(state) {
case "data":
if(whitespace(code)) {
emit(new WhitespaceToken);
while(whitespace(next())) consume();
}
else if(code == 0x22) switchto("double-quote-string");
else if(code == 0x23) switchto("hash");
else if(code == 0x27) switchto("single-quote-string");
else if(code == 0x28) emit(new OpenParenToken);
else if(code == 0x29) emit(new CloseParenToken);
else if(code == 0x2b) {
if(digit(next()) || (next() == 0x2e && digit(next(2)))) switchto("number") && reconsume();
else emit(new DelimToken(code));
}
else if(code == 0x2d) {
if(next(1) == 0x2d && next(2) == 0x3e) consume(2) && emit(new CDCToken);
else if(digit(next()) || (next(1) == 0x2e && digit(next(2)))) switchto("number") && reconsume();
else if(namestartchar(next())) switchto("identifier") && reconsume();
else emit(new DelimToken(code));
}
else if(code == 0x2e) {
if(digit(next())) switchto("number") && reconsume();
else emit(new DelimToken(code));
}
else if(code == 0x2f) {
if(next() == 0x2a) switchto("comment");
else emit(new DelimToken(code));
}
else if(code == 0x3a) emit(new ColonToken);
else if(code == 0x3b) emit(new SemicolonToken);
else if(code == 0x3c) {
if(next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) consume(3) && emit(new CDOToken);
else emit(new DelimToken(code));
}
else if(code == 0x40) switchto("at-keyword");
else if(code == 0x5b) emit(new OpenSquareToken);
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new DelimToken(code));
else switchto("identifier") && reconsume();
}
else if(code == 0x5d) emit(new CloseSquareToken);
else if(code == 0x7b) emit(new OpenCurlyToken);
else if(code == 0x7d) emit(new CloseCurlyToken);
else if(digit(code)) switchto("number") && reconsume();
else if(code == 0x55 || code == 0x75) {
if(next(1) == 0x2b && hexdigit(next(2))) consume() && switchto("unicode-range");
else if((next(1) == 0x52 || next(1) == 0x72) && (next(2) == 0x4c || next(2) == 0x6c) && (next(3) == 0x28)) consume(3) && switchto("url");
else switchto("identifier") && reconsume();
}
else if(namestartchar(code)) switchto("identifier") && reconsume();
else if(eof()) { emit(new EOFToken); return tokens; }
else emit(new DelimToken(code));
break;
case "double-quote-string":
if(currtoken == undefined) create(new StringToken);
if(code == 0x22) emit() && switchto("data");
else if(eof()) parseerror() && emit() && switchto("data");
else if(newline(code)) parseerror() && emit(new BadStringToken) && switchto("data") && reconsume();
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new BadStringToken) && switchto("data");
else if(newline(next())) consume();
else currtoken.append(consumeEscape());
}
else currtoken.append(code);
break;
case "single-quote-string":
if(currtoken == undefined) create(new StringToken);
if(code == 0x27) emit() && switchto("data");
else if(eof()) parseerror() && emit() && switchto("data");
else if(newline(code)) parseerror() && emit(new BadStringToken) && switchto("data") && reconsume();
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new BadStringToken) && switchto("data");
else if(newline(next())) consume();
else currtoken.append(consumeEscape());
}
else currtoken.append(code);
break;
case "hash":
if(namechar(code)) create(new HashToken(code)) && switchto("hash-rest");
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume();
else create(new HashToken(consumeEscape())) && switchto('hash-rest');
}
else emit(new DelimToken(0x23)) && switchto('data') && reconsume();
break;
case "hash-rest":
if(namechar(code)) currtoken.append(code);
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume();
else currtoken.append(consumeEscape());
}
else emit() && switchto('data') && reconsume();
break;
case "comment":
if(code == 0x2a) {
if(next() == 0x2f) consume() && switchto('data');
else donothing();
}
else if(eof()) parseerror() && switchto('data') && reconsume();
else donothing();
break;
case "at-keyword":
if(code == 0x2d) {
if(namestartchar(next())) consume() && create(new AtKeywordToken([0x40,code])) && switchto('at-keyword-rest');
else emit(new DelimToken(0x40)) && switchto('data') && reconsume();
}
else if(namestartchar(code)) create(new AtKeywordToken(code)) && switchto('at-keyword-rest');
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume();
else create(new AtKeywordToken(consumeEscape())) && switchto('at-keyword-rest');
}
else emit(new DelimToken(0x40)) && switchto('data') && reconsume();
break;
case "at-keyword-rest":
if(namechar(code)) currtoken.append(code);
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit() && switchto("data") && reconsume();
else currtoken.append(consumeEscape());
}
else emit() && switchto('data') && reconsume();
break;
case "identifier":
if(code == 0x2d) {
if(namestartchar(next())) create(new IdentifierToken(code)) && switchto('identifier-rest');
else switchto('data') && reconsume();
}
else if(namestartchar(code)) create(new IdentifierToken(code)) && switchto('identifier-rest');
else if(code == 0x5c) {
if(badescape(next())) parseerror() && switchto("data") && reconsume();
else create(new IdentifierToken(consumeEscape())) && switchto('identifier-rest');
}
else switchto('data') && reconsume();
break;
case "identifier-rest":
if(namechar(code)) currtoken.append(code);
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit() && switchto("data") && reconsume();
else currtoken.append(consumeEscape());
}
else if(code == 0x28) emit(new FunctionToken(currtoken)) && switchto('data');
else if(whitespace(code) && options.transformFunctionWhitespace) switchto('transform-function-whitespace');
else emit() && switchto('data') && reconsume();
break;
case "transform-function-whitespace":
if(whitespace(code)) donothing();
else if(code == 0x28) emit(new FunctionToken(currtoken)) && switchto('data');
else emit() && switchto('data') && reconsume();
break;
case "number":
create(new NumberToken());
if(code == 0x2d) {
if(digit(next())) consume() && currtoken.append([0x2d,code]) && switchto('number-rest');
else if(next(1) == 0x2e && digit(next(2))) consume(2) && currtoken.append([0x2d,0x2e,code]) && switchto('number-fraction');
else switchto('data') && reconsume();
}
else if(code == 0x2b) {
if(digit(next())) consume() && currtoken.append([0x2b,code]) && switchto('number-rest');
else if(next(1) == 0x2e && digit(next(2))) consume(2) && currtoken.append([0x2b,0x2e,code]) && switchto('number-fraction');
else switchto('data') && reconsume();
}
else if(digit(code)) currtoken.append(code) && switchto('number-rest');
else if(code == 0x2e) {
if(digit(next())) consume() && currtoken.append([0x2e,code]) && switchto('number-fraction');
else switchto('data') && reconsume();
}
else switchto('data') && reconsume();
break;
case "number-rest":
if(digit(code)) currtoken.append(code);
else if(code == 0x2e) {
if(digit(next())) consume() && currtoken.append([0x2e,code]) && switchto('number-fraction');
else emit() && switchto('data') && reconsume();
}
else if(code == 0x25) emit(new PercentageToken(currtoken)) && switchto('data') && reconsume();
else if(code == 0x45 || code == 0x65) {
if(!options.scientificNotation) create(new DimensionToken(currtoken,code)) && switchto('dimension');
else if(digit(next())) consume() && currtoken.append([0x25,code]) && switchto('sci-notation');
else if((next(1) == 0x2b || next(1) == 0x2d) && digit(next(2))) currtoken.append([0x25,next(1),next(2)]) && consume(2) && switchto('sci-notation');
else create(new DimensionToken(currtoken,code)) && switchto('dimension');
}
else if(code == 0x2d) {
if(namestartchar(next())) consume() && create(new DimensionToken(currtoken,[0x2d,code])) && switchto('dimension');
else if(next(1) == 0x5c && badescape(next(2))) parseerror() && emit() && switchto('data') && reconsume();
else if(next(1) == 0x5c) consume() && create(new DimensionToken(currtoken, [0x2d,consumeEscape()])) && switchto('dimension');
else emit() && switchto('data') && reconsume();
}
else if(namestartchar(code)) create(new DimensionToken(currtoken, code)) && switchto('dimension');
else if(code == 0x5c) {
if(badescape(next)) emit() && switchto('data') && reconsume();
else create(new DimensionToken(currtoken,consumeEscape)) && switchto('dimension');
}
else emit() && switchto('data') && reconsume();
break;
case "number-fraction":
currtoken.type = "number";
if(digit(code)) currtoken.append(code);
else if(code == 0x2e) emit() && switchto('data') && reconsume();
else if(code == 0x25) emit(new PercentageToken(currtoken)) && switchto('data') && reconsume();
else if(code == 0x45 || code == 0x65) {
if(!options.scientificNotation) create(new DimensionToken(currtoken,code)) && switchto('dimension');
else if(digit(next())) consume() && currtoken.append([0x25,code]) && switchto('sci-notation');
else if((next(1) == 0x2b || next(1) == 0x2d) && digit(next(2))) currtoken.append([0x25,next(1),next(2)]) && consume(2) && switchto('sci-notation');
else create(new DimensionToken(currtoken,code)) && switchto('dimension');
}
else if(code == 0x2d) {
if(namestartchar(next())) consume() && create(new DimensionToken(currtoken,[0x2d,code])) && switchto('dimension');
else if(next(1) == 0x5c && badescape(next(2))) parseerror() && emit() && switchto('data') && reconsume();
else if(next(1) == 0x5c) consume() && create(new DimensionToken(currtoken, [0x2d,consumeEscape()])) && switchto('dimension');
else emit() && switchto('data') && reconsume();
}
else if(namestartchar(code)) create(new DimensionToken(currtoken, code)) && switchto('dimension');
else if(code == 0x5c) {
if(badescape(next)) emit() && switchto('data') && reconsume();
else create(new DimensionToken(currtoken,consumeEscape)) && switchto('dimension');
}
else emit() && switchto('data') && reconsume();
break;
case "dimension":
if(namechar(code)) currtoken.append(code);
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit() && switchto('data') && reconsume();
else currtoken.append(consumeEscape());
}
else emit() && switchto('data') && reconsume();
break;
case "sci-notation":
if(digit(code)) currtoken.append(code);
else emit() && switchto('data') && reconsume();
break;
case "url":
if(code == 0x22) switchto('url-double-quote');
else if(code == 0x27) switchto('url-single-quote');
else if(code == 0x29) emit(new URLToken) && switchto('data');
else if(whitespace(code)) donothing();
else switchto('url-unquoted') && reconsume();
break;
case "url-double-quote":
if(currtoken == undefined) create(new URLToken);
if(code == 0x22) switchto('url-end');
else if(newline(code)) parseerror() && switchto('bad-url');
else if(code == 0x5c) {
if(newline(next())) consume();
else if(badescape(next())) parseerror() && emit(new BadURLToken) && switchto('data') && reconsume();
else currtoken.append(consumeEscape());
}
else currtoken.append(code);
break;
case "url-single-quote":
if(currtoken == undefined) create(new URLToken);
if(code == 0x27) switchto('url-end');
else if(newline(code)) parseerror() && switchto('bad-url');
else if(code == 0x5c) {
if(newline(next())) consume();
else if(badescape(next())) parseerror() && emit(new BadURLToken) && switchto('data') && reconsume();
else currtoken.append(consumeEscape());
}
else currtoken.append(code);
break;
case "url-end":
if(whitespace(code)) donothing();
else if(code == 0x29) emit() && switchto('data');
else parseerror() && switchto('bad-url') && reconsume();
break;
case "url-unquoted":
if(currtoken == undefined) create(new URLToken);
if(whitespace(code)) switchto('url-end');
else if(code == 0x29) emit() && switchto('data');
else if(code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) parseerror() && switchto('bad-url');
else if(code == 0x5c) {
if(badescape(next())) parseerror() && switchto('bad-url');
else currtoken.append(consumeEscape());
}
else currtoken.append(code);
break;
case "bad-url":
if(code == 0x29) emit(new BadURLToken) && switchto('data');
else if(code == 0x5c) {
if(badescape(next())) donothing();
else consumeEscape()
}
else donothing();
break;
case "unicode-range":
// We already know that the current code is a hexdigit.
var start = [code], end = [code];
for(var total = 1; total < 6; total++) {
if(hexdigit(next())) {
consume();
start.push(code);
end.push(code);
}
else break;
}
if(next() == 0x3f) {
for(;total < 6; total++) {
if(next() == 0x3f) {
consume();
start.push("0".charCodeAt(0));
end.push("f".charCodeAt(0));
}
else break;
}
emit(new UnicodeRangeToken(start,end)) && switchto('data');
}
else if(next(1) == 0x2d && hexdigit(next(2))) {
consume();
consume();
end = [code];
for(var total = 1; total < 6; total++) {
if(hexdigit(next())) {
consume();
end.push(code);
}
else break;
}
emit(new UnicodeRangeToken(start,end)) && switchto('data');
}
else emit(new UnicodeRangeToken(start)) && switchto('data');
break;
default:
console.log("Unknown state '" + state + "'");
// None of the existing consumers want comments.
if (token.tokenType !== "comment") {
yield token;
}
}
}
function stringFromCodeArray(arr) {
return String.fromCharCode.apply(null,arr.filter(function(e){return e;}));
}
exports.cssTokenizer = cssTokenizer;
function CSSParserToken(options) { return this; }
CSSParserToken.prototype.finish = function() { return this; }
CSSParserToken.prototype.toString = function() { return this.tokenType; }
CSSParserToken.prototype.toJSON = function() { return this.toString(); }
/**
* Pass |string| to the CSS lexer and return an array of all the
* returned tokens. Comment tokens are not included. In addition to
* the usual information, each token will have starting and ending
* line and column information attached. Specifically, each token
* has an additional "loc" attribute. This attribute is an object
* of the form {line: L, column: C}. Lines and columns are both zero
* based.
*
* It's best not to add new uses of this function. In general it is
* simpler and better to use the CSSToken offsets, rather than line
* and column. Also, this function lexes the entire input string at
* once, rather than lazily yielding a token stream. Use
* |cssTokenizer| or |DOMUtils.getCSSLexer| instead.
*
* @param{String} string The input string.
* @return {Array} An array of tokens (@see CSSToken) that have
* line and column information.
*/
function cssTokenizerWithLineColumn(string) {
let lexer = DOMUtils.getCSSLexer(string);
let result = [];
let prevToken = undefined;
while (true) {
let token = lexer.nextToken();
let lineNumber = lexer.lineNumber;
let columnNumber = lexer.columnNumber;
function BadStringToken() { return this; }
BadStringToken.prototype = new CSSParserToken;
BadStringToken.prototype.tokenType = "BADSTRING";
function BadURLToken() { return this; }
BadURLToken.prototype = new CSSParserToken;
BadURLToken.prototype.tokenType = "BADURL";
function WhitespaceToken() { return this; }
WhitespaceToken.prototype = new CSSParserToken;
WhitespaceToken.prototype.tokenType = "WHITESPACE";
WhitespaceToken.prototype.toString = function() { return "WS"; }
function CDOToken() { return this; }
CDOToken.prototype = new CSSParserToken;
CDOToken.prototype.tokenType = "CDO";
function CDCToken() { return this; }
CDCToken.prototype = new CSSParserToken;
CDCToken.prototype.tokenType = "CDC";
function ColonToken() { return this; }
ColonToken.prototype = new CSSParserToken;
ColonToken.prototype.tokenType = ":";
function SemicolonToken() { return this; }
SemicolonToken.prototype = new CSSParserToken;
SemicolonToken.prototype.tokenType = ";";
function OpenCurlyToken() { return this; }
OpenCurlyToken.prototype = new CSSParserToken;
OpenCurlyToken.prototype.tokenType = "{";
function CloseCurlyToken() { return this; }
CloseCurlyToken.prototype = new CSSParserToken;
CloseCurlyToken.prototype.tokenType = "}";
function OpenSquareToken() { return this; }
OpenSquareToken.prototype = new CSSParserToken;
OpenSquareToken.prototype.tokenType = "[";
function CloseSquareToken() { return this; }
CloseSquareToken.prototype = new CSSParserToken;
CloseSquareToken.prototype.tokenType = "]";
function OpenParenToken() { return this; }
OpenParenToken.prototype = new CSSParserToken;
OpenParenToken.prototype.tokenType = "(";
function CloseParenToken() { return this; }
CloseParenToken.prototype = new CSSParserToken;
CloseParenToken.prototype.tokenType = ")";
function EOFToken() { return this; }
EOFToken.prototype = new CSSParserToken;
EOFToken.prototype.tokenType = "EOF";
function DelimToken(code) {
this.value = String.fromCharCode(code);
return this;
}
DelimToken.prototype = new CSSParserToken;
DelimToken.prototype.tokenType = "DELIM";
DelimToken.prototype.toString = function() { return "DELIM("+this.value+")"; }
function StringValuedToken() { return this; }
StringValuedToken.prototype = new CSSParserToken;
StringValuedToken.prototype.append = function(val) {
if(val instanceof Array) {
for(var i = 0; i < val.length; i++) {
this.value.push(val[i]);
if (prevToken) {
prevToken.loc.end = {
line: lineNumber,
column: columnNumber
};
}
} else {
this.value.push(val);
}
return true;
}
StringValuedToken.prototype.finish = function() {
this.value = stringFromCodeArray(this.value);
return this;
}
function IdentifierToken(val) {
this.value = [];
this.append(val);
}
IdentifierToken.prototype = new StringValuedToken;
IdentifierToken.prototype.tokenType = "IDENT";
IdentifierToken.prototype.toString = function() { return "IDENT("+this.value+")"; }
function FunctionToken(val) {
// These are always constructed by passing an IdentifierToken
this.value = val.finish().value;
}
FunctionToken.prototype = new CSSParserToken;
FunctionToken.prototype.tokenType = "FUNCTION";
FunctionToken.prototype.toString = function() { return "FUNCTION("+this.value+")"; }
function AtKeywordToken(val) {
this.value = [];
this.append(val);
}
AtKeywordToken.prototype = new StringValuedToken;
AtKeywordToken.prototype.tokenType = "AT-KEYWORD";
AtKeywordToken.prototype.toString = function() { return "AT("+this.value+")"; }
function HashToken(val) {
this.value = [];
this.append(val);
}
HashToken.prototype = new StringValuedToken;
HashToken.prototype.tokenType = "HASH";
HashToken.prototype.toString = function() { return "HASH("+this.value+")"; }
function StringToken(val) {
this.value = [];
this.append(val);
}
StringToken.prototype = new StringValuedToken;
StringToken.prototype.tokenType = "STRING";
StringToken.prototype.toString = function() { return "\""+this.value+"\""; }
function URLToken(val) {
this.value = [];
this.append(val);
}
URLToken.prototype = new StringValuedToken;
URLToken.prototype.tokenType = "URL";
URLToken.prototype.toString = function() { return "URL("+this.value+")"; }
function NumberToken(val) {
this.value = [];
this.append(val);
this.type = "integer";
}
NumberToken.prototype = new StringValuedToken;
NumberToken.prototype.tokenType = "NUMBER";
NumberToken.prototype.toString = function() {
if(this.type == "integer")
return "INT("+this.value+")";
return "NUMBER("+this.value+")";
}
NumberToken.prototype.finish = function() {
this.repr = stringFromCodeArray(this.value);
this.value = this.repr * 1;
if(Math.abs(this.value) % 1 != 0) this.type = "number";
return this;
}
function PercentageToken(val) {
// These are always created by passing a NumberToken as val
val.finish();
this.value = val.value;
this.repr = val.repr;
}
PercentageToken.prototype = new CSSParserToken;
PercentageToken.prototype.tokenType = "PERCENTAGE";
PercentageToken.prototype.toString = function() { return "PERCENTAGE("+this.value+")"; }
function DimensionToken(val,unit) {
// These are always created by passing a NumberToken as the val
val.finish();
this.num = val.value;
this.unit = [];
this.repr = val.repr;
this.append(unit);
}
DimensionToken.prototype = new CSSParserToken;
DimensionToken.prototype.tokenType = "DIMENSION";
DimensionToken.prototype.toString = function() { return "DIM("+this.num+","+this.unit+")"; }
DimensionToken.prototype.append = function(val) {
if(val instanceof Array) {
for(var i = 0; i < val.length; i++) {
this.unit.push(val[i]);
if (!token) {
break;
}
if (token.tokenType === "comment") {
// We've already dealt with the previous token's location.
prevToken = undefined;
} else {
let startLoc = {
line: lineNumber,
column: columnNumber
};
token.loc = {start: startLoc};
result.push(token);
prevToken = token;
}
} else {
this.unit.push(val);
}
return true;
}
DimensionToken.prototype.finish = function() {
this.unit = stringFromCodeArray(this.unit);
this.repr += this.unit;
return this;
return result;
}
function UnicodeRangeToken(start,end) {
// start and end are array of char codes, completely finished
start = parseInt(stringFromCodeArray(start),16);
if(end === undefined) end = start + 1;
else end = parseInt(stringFromCodeArray(end),16);
if(start > maximumallowedcodepoint) end = start;
if(end < start) end = start;
if(end > maximumallowedcodepoint) end = maximumallowedcodepoint;
this.start = start;
this.end = end;
return this;
}
UnicodeRangeToken.prototype = new CSSParserToken;
UnicodeRangeToken.prototype.tokenType = "UNICODE-RANGE";
UnicodeRangeToken.prototype.toString = function() {
if(this.start+1 == this.end)
return "UNICODE-RANGE("+this.start.toString(16).toUpperCase()+")";
if(this.start < this.end)
return "UNICODE-RANGE("+this.start.toString(16).toUpperCase()+"-"+this.end.toString(16).toUpperCase()+")";
return "UNICODE-RANGE()";
}
UnicodeRangeToken.prototype.contains = function(code) {
return code >= this.start && code < this.end;
}
// Exportation.
// TODO: also export the various tokens objects?
module.exports = tokenize;
}));
exports.cssTokenizerWithLineColumn = cssTokenizerWithLineColumn;

View File

@ -6,36 +6,7 @@
"use strict";
const cssTokenizer = require("devtools/sourceeditor/css-tokenizer");
/**
* Returns the string enclosed in quotes
*/
function quoteString(string) {
let hasDoubleQuotes = string.includes('"');
let hasSingleQuotes = string.includes("'");
let quote = '"';
if (hasDoubleQuotes && !hasSingleQuotes) {
quote = "'";
}
// Quote special characters as specified by the CSS grammar.
// See http://www.w3.org/TR/CSS2/syndata.html#tokenization
// and http://www.w3.org/TR/CSS2/syndata.html#strings
return quote +
string.replace(/[\\"]/g, match => {
switch (match) {
case '\\':
return '\\\\';
case '"':
if (quote == '"')
return '\\"';
return match;
}
}) +
quote;
}
const {cssTokenizer} = require("devtools/sourceeditor/css-tokenizer");
/**
* Returns an array of CSS declarations given an string.
@ -52,6 +23,10 @@ function quoteString(string) {
* [{"name": string, "value": string, "priority": string}, ...]
*/
function parseDeclarations(inputString) {
if (inputString === null || inputString === undefined) {
throw new Error("empty input string");
}
let tokens = cssTokenizer(inputString);
let declarations = [{name: "", value: "", priority: ""}];
@ -60,7 +35,7 @@ function parseDeclarations(inputString) {
for (let token of tokens) {
lastProp = declarations[declarations.length - 1];
if (token.tokenType === ":") {
if (token.tokenType === "symbol" && token.text === ":") {
if (!lastProp.name) {
// Set the current declaration name if there's no name yet
lastProp.name = current.trim();
@ -71,63 +46,29 @@ function parseDeclarations(inputString) {
// with colons)
current += ":";
}
} else if (token.tokenType === ";") {
} else if (token.tokenType === "symbol" && token.text === ";") {
lastProp.value = current.trim();
current = "";
hasBang = false;
declarations.push({name: "", value: "", priority: ""});
} else {
switch(token.tokenType) {
case "IDENT":
if (token.value === "important" && hasBang) {
lastProp.priority = "important";
hasBang = false;
} else {
if (hasBang) {
current += "!";
}
current += token.value;
}
break;
case "WHITESPACE":
current += " ";
break;
case "DIMENSION":
current += token.repr;
break;
case "HASH":
current += "#" + token.value;
break;
case "URL":
current += "url(" + quoteString(token.value) + ")";
break;
case "FUNCTION":
current += token.value + "(";
break;
case "(":
case ")":
current += token.tokenType;
break;
case "EOF":
break;
case "DELIM":
if (token.value === "!") {
hasBang = true;
} else {
current += token.value;
}
break;
case "STRING":
current += quoteString(token.value);
break;
case "{":
case "}":
current += token.tokenType;
break;
default:
current += token.value;
break;
} else if (token.tokenType === "ident") {
if (token.text === "important" && hasBang) {
lastProp.priority = "important";
hasBang = false;
} else {
if (hasBang) {
current += "!";
}
current += token.text;
}
} else if (token.tokenType === "symbol" && token.text === "!") {
hasBang = true;
} else if (token.tokenType === "whitespace") {
current += " ";
} else if (token.tokenType === "comment") {
// For now, just ignore.
} else {
current += inputString.substring(token.startOffset, token.endOffset);
}
}

View File

@ -2796,7 +2796,7 @@ TextPropertyEditor.prototype = {
done: this._onValueDone,
destroy: this.update,
validate: this._onValidate,
advanceChars: ';',
advanceChars: advanceValidate,
contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
property: this.prop,
popup: this.popup
@ -3525,6 +3525,48 @@ function getPropertyNameAndValue(node) {
}
}
/**
* Called when a character is typed in a value editor. This decides
* whether to advance or not, first by checking to see if ";" was
* typed, and then by lexing the input and seeing whether the ";"
* would be a terminator at this point.
*
* @param {number} aKeyCode Key code to be checked.
* @param {String} aValue Current text editor value.
* @param {number} aInsertionPoint The index of the insertion point.
* @return {Boolean} True if the focus should advance; false if
* the character should be inserted.
*/
function advanceValidate(aKeyCode, aValue, aInsertionPoint) {
// Only ";" has special handling here.
if (aKeyCode !== Ci.nsIDOMKeyEvent.DOM_VK_SEMICOLON) {
return false;
}
// Insert the character provisionally and see what happens. If we
// end up with a ";" symbol token, then the semicolon terminates the
// value. Otherwise it's been inserted in some spot where it has a
// valid meaning, like a comment or string.
aValue = aValue.slice(0, aInsertionPoint) + ';' + aValue.slice(aInsertionPoint);
let lexer = domUtils.getCSSLexer(aValue);
while (true) {
let token = lexer.nextToken();
if (token.endOffset > aInsertionPoint) {
if (token.tokenType === "symbol" && token.text === ";") {
// The ";" is a terminator.
return true;
} else {
// The ";" is not a terminator in this context.
break;
}
}
}
return false;
}
// We're exporting _advanceValidate for unit tests.
exports._advanceValidate = advanceValidate;
XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
return Cc["@mozilla.org/widget/clipboardhelper;1"].
getService(Ci.nsIClipboardHelper);

View File

@ -74,15 +74,15 @@ const TEST_DATA = [
// Test various types of background-image urls
{
input: "background-image: url(../../relative/image.png)",
expected: [{name: "background-image", value: "url(\"../../relative/image.png\")", priority: ""}]
expected: [{name: "background-image", value: "url(../../relative/image.png)", priority: ""}]
},
{
input: "background-image: url(http://site.com/test.png)",
expected: [{name: "background-image", value: "url(\"http://site.com/test.png\")", priority: ""}]
expected: [{name: "background-image", value: "url(http://site.com/test.png)", priority: ""}]
},
{
input: "background-image: url(wow.gif)",
expected: [{name: "background-image", value: "url(\"wow.gif\")", priority: ""}]
expected: [{name: "background-image", value: "url(wow.gif)", priority: ""}]
},
// Test that urls with :;{} characters in them are parsed correctly
{
@ -152,15 +152,15 @@ const TEST_DATA = [
{input: "wat: #XYZ", expected: [{name: "wat", value: "#XYZ", priority: ""}]},
// Test string/url quotes escaping
{input: "content: \"this is a 'string'\"", expected: [{name: "content", value: "\"this is a 'string'\"", priority: ""}]},
{input: 'content: "this is a \\"string\\""', expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]},
{input: 'content: "this is a \\"string\\""', expected: [{name: "content", value: '"this is a \\"string\\""', priority: ""}]},
{input: "content: 'this is a \"string\"'", expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]},
{input: "content: 'this is a \\'string\\'", expected: [{name: "content", value: '"this is a \'string\'"', priority: ""}]},
{input: "content: 'this \\' is a \" really strange string'", expected: [{name: "content", value: '"this \' is a \\\" really strange string"', priority: ""}]},
{input: "content: 'this is a \\'string\\''", expected: [{name: "content", value: "'this is a \\'string\\''", priority: ""}]},
{input: "content: 'this \\' is a \" really strange string'", expected: [{name: "content", value: "'this \\' is a \" really strange string'", priority: ""}]},
{
input: "content: \"a not s\\\
o very long title\"",
expected: [
{name: "content", value: '"a not s\
{name: "content", value: '"a not s\\\
o very long title"', priority: ""}
]
},

View File

@ -214,10 +214,6 @@ breakpointMenuItem.deleteAll=Remove all breakpoints
# yet.
loadingText=Loading\u2026
# LOCALIZATION NOTE (errorLoadingText): The text that is displayed in the debugger
# viewer when there is an error loading a file
errorLoadingText=Error loading source:\n
# LOCALIZATION NOTE (errorLoadingText2): The text that is displayed in the debugger
# viewer when there is an error loading a file
errorLoadingText2=Error loading this URL: %S
@ -324,5 +320,4 @@ variablesViewOptimizedOut=(optimized away)
variablesViewUninitialized=(uninitialized)
variablesViewMissingArgs=(unavailable)
evalGroupLabel=Evaluated Sources
anonymousSourcesLabel=Anonymous Sources

View File

@ -339,7 +339,7 @@ infobar_menuitem_dontshowagain_label=Don't show this again
infobar_menuitem_dontshowagain_accesskey=D
# Context in conversation strings
context_offer_label=Let's talk about this page
# LOCALIZATION NOTE (context_inroom_label): this string is followed by the
# title/URL of the website you are having a conversation about, displayed on a
# separate line. If this structure doesn't work for your locale, you might want

View File

@ -34,15 +34,9 @@ newtab.suggested.explain=This site is suggested to you by Mozilla. You can remov
# the gear icon used to customize the new tab window. %2$S will be replaced by
# an active link using string newtab.learn.link as text.
newtab.enhanced.explain=A Mozilla partner has visually enhanced this tile, replacing the screenshot. You can turn off enhanced tiles by clicking the %1$S button for your preferences. %2$S
# LOCALIZATION NOTE(newtab.intro.paragraph1): %1$S will be replaced inline by
# active link using string newtab.learn.link as text.
newtab.intro.paragraph1=When you open a new tab, youll see tiles from the sites you frequently visit, along with tiles that we think might be of interest to you. Some of these tiles may be sponsored by Mozilla partners. Well always indicate to you which tiles are sponsored. %1$S
# LOCALIZATION NOTE(newtab.intro.paragraph2): %1$S will be replaced inline by
# active link using string newtab.privacy.link as text.
newtab.intro.paragraph2=In order to provide this service, Mozilla collects and uses certain analytics information relating to your use of the tiles in accordance with our %1$S.
# LOCALIZATION NOTE(newtab.intro.paragraph3): %1$S will be replaced inline by
# the gear icon used to customize the new tab window.
newtab.intro.paragraph3=You can turn off the tiles feature by clicking the %1$S button for your preferences.
# LOCALIZATION NOTE(newtab.intro.paragraph4): %1$S will be replaced inline by
# the gear icon used to customize the new tab window. %2$S will be replaced by
# newtab.intro.controls as text

View File

@ -146,6 +146,7 @@ browser.jar:
skin/classic/browser/feeds/subscribe-ui.css (feeds/subscribe-ui.css)
* skin/classic/browser/newtab/newTab.css (newtab/newTab.css)
skin/classic/browser/newtab/controls.svg (../shared/newtab/controls.svg)
skin/classic/browser/newtab/whimsycorn.png (../shared/newtab/whimsycorn.png)
skin/classic/browser/panic-panel/header.png (../shared/panic-panel/header.png)
skin/classic/browser/panic-panel/header-small.png (../shared/panic-panel/header-small.png)
skin/classic/browser/panic-panel/icons.png (../shared/panic-panel/icons.png)

View File

@ -231,6 +231,7 @@ browser.jar:
skin/classic/browser/feeds/feedIcon16.png (feeds/feedIcon16.png)
* skin/classic/browser/newtab/newTab.css (newtab/newTab.css)
skin/classic/browser/newtab/controls.svg (../shared/newtab/controls.svg)
skin/classic/browser/newtab/whimsycorn.png (../shared/newtab/whimsycorn.png)
skin/classic/browser/setDesktopBackground.css
skin/classic/browser/monitor.png
skin/classic/browser/monitor_16-10.png

View File

@ -246,6 +246,11 @@ panelmultiview[nosubviews=true] > .panel-viewcontainer > .panel-viewstack > .pan
max-width: @standaloneSubviewWidth@;
}
/* Bug 1164419 - increase Pocket panel size to accomidate wider Russian text. */
panelmultiview[mainViewId=PanelUI-pocketView] > .panel-viewcontainer > .panel-viewstack > .panel-mainview:not([panelid="PanelUI-popup"]) {
max-width: 33em; /* standaloneSubviewWidth + 3 */
}
panelview:not([mainview]) .toolbarbutton-text,
.cui-widget-panel toolbarbutton > .toolbarbutton-text {
text-align: start;

View File

@ -10,7 +10,6 @@
padding: 0;
font: message-box;
font-size: 1.25rem;
line-height: 22px;
}
* {

View File

@ -8,11 +8,25 @@
max-height: 20em;
}
.login-fill-item[disabled] {
color: #888;
background-color: #fff;
}
.login-fill-item[disabled][selected] {
background-color: #eef;
}
.login-hostname {
margin: 4px;
font-weight: bold;
}
.login-fill-item.different-hostname > .login-hostname {
color: #888;
font-style: italic;
}
.login-username {
margin: 4px;
color: #888;

View File

@ -87,7 +87,9 @@
}
/* CELLS */
.newtab-cell {
.newtab-cell,
.newtab-intro-cell,
.newtab-intro-cell-hover {
background-color: rgba(255,255,255,.2);
border-radius: 8px;
}
@ -106,12 +108,19 @@
}
.newtab-cell:not([ignorehover]) > .newtab-site:hover,
.newtab-site[dragged] {
.newtab-site[dragged],
.newtab-intro-cell-hover {
border: 2px solid white;
box-shadow: 0 0 6px 2px #4cb1ff;
margin: -2px;
}
.newtab-intro-cell .newtab-thumbnail,
.newtab-intro-cell-hover .newtab-thumbnail {
background-color: #cae1f4;
background-image: url("chrome://browser/skin/newtab/whimsycorn.png");
}
.newtab-site[dragged] {
transition-property: box-shadow, background-color;
background-color: rgb(242,242,242);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -140,6 +140,12 @@
list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16.png);
}
#login-fill-notification-icon {
/* Temporary icon until the capture and fill doorhangers are unified. */
list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16.png);
transform: scaleX(-1);
}
.webapps-notification-icon,
#webapps-notification-icon {
list-style-image: url(chrome://global/skin/icons/webapps-16.png);
@ -311,6 +317,7 @@
list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric.png);
}
#login-fill-notification-icon,
#password-notification-icon {
list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16@2x.png);
}

View File

@ -187,6 +187,7 @@ browser.jar:
skin/classic/browser/feeds/subscribe-ui.css (feeds/subscribe-ui.css)
* skin/classic/browser/newtab/newTab.css (newtab/newTab.css)
skin/classic/browser/newtab/controls.svg (../shared/newtab/controls.svg)
skin/classic/browser/newtab/whimsycorn.png (../shared/newtab/whimsycorn.png)
skin/classic/browser/panic-panel/header.png (../shared/panic-panel/header.png)
skin/classic/browser/panic-panel/header-small.png (../shared/panic-panel/header-small.png)
skin/classic/browser/panic-panel/icons.png (../shared/panic-panel/icons.png)

View File

@ -254,7 +254,8 @@
<!-- The main reason for the Tab Queue build flag is to not mess with the VIEW intent filter
before the rest of the plumbing is in place -->
<service android:name="org.mozilla.gecko.tabqueue.TabQueueService" />
<service android:name="org.mozilla.gecko.tabqueue.TabQueueService"
android:taskAffinity="@ANDROID_PACKAGE_NAME@.TABQUEUE" />
<activity android:name="org.mozilla.gecko.tabqueue.TabQueuePrompt"
android:launchMode="singleTop"
@ -263,6 +264,7 @@
<activity android:name="org.mozilla.gecko.tabqueue.TabQueueDispatcher"
android:label="@MOZ_APP_DISPLAYNAME@"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:theme="@style/TabQueueActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

View File

@ -914,8 +914,9 @@ public class BrowserApp extends GeckoApp
checkFirstrun(this, new SafeIntent(getIntent()));
}
private void processTabQueue() {
if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_ANDROID_TAB_QUEUE) {
@Override
protected void processTabQueue() {
if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_ANDROID_TAB_QUEUE && mInitialized) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
@ -927,7 +928,8 @@ public class BrowserApp extends GeckoApp
}
}
private void openQueuedTabs() {
@Override
protected void openQueuedTabs() {
ThreadUtils.assertNotOnUiThread();
int queuedTabCount = TabQueueHelper.getTabQueueLength(BrowserApp.this);

View File

@ -5,24 +5,6 @@
package org.mozilla.gecko;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.db.BrowserDB;
@ -112,6 +94,24 @@ import android.widget.RelativeLayout;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public abstract class GeckoApp
extends GeckoActivity
@ -204,6 +204,10 @@ public abstract class GeckoApp
abstract protected String getDefaultProfileName() throws NoMozillaDirectoryException;
protected void processTabQueue() {};
protected void openQueuedTabs() {};
@SuppressWarnings("serial")
class SessionRestoreException extends Exception {
public SessionRestoreException(Exception e) {
@ -1509,17 +1513,24 @@ public abstract class GeckoApp
// Restore tabs before opening an external URL so that the new tab
// is animated properly.
Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL;
if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
flags |= Tabs.LOADURL_PINNED;
}
loadStartupTab(passedUri, intent, flags);
processActionViewIntent(new Runnable() {
@Override
public void run() {
int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL;
if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
flags |= Tabs.LOADURL_PINNED;
}
loadStartupTab(passedUri, intent, flags);
}
});
} else {
if (!mIsRestoringActivity) {
loadStartupTabWithAboutHome(Tabs.LOADURL_NEW_TAB);
}
Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
processTabQueue();
}
// If we're not restoring, move the session file so it can be read for
@ -1604,6 +1615,34 @@ public abstract class GeckoApp
}
}
protected void processActionViewIntent(final Runnable openTabsRunnable) {
// We need to ensure that if we receive a VIEW action and there are tabs queued then the
// site loaded from the intent is on top (last loaded) and selected with all other tabs
// being opened behind it. We process the tab queue first and request a callback from the JS - the
// listener will open the url from the intent as normal when the tab queue has been processed.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_ANDROID_TAB_QUEUE
&& TabQueueHelper.shouldOpenTabQueueUrls(GeckoApp.this)) {
EventDispatcher.getInstance().registerGeckoThreadListener(new NativeEventListener() {
@Override
public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
if ("Tabs:TabsOpened".equals(event)) {
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Tabs:TabsOpened");
openTabsRunnable.run();
}
}
}, "Tabs:TabsOpened");
TabQueueHelper.openQueuedUrls(GeckoApp.this, mProfile, TabQueueHelper.FILE_NAME, true);
} else {
openTabsRunnable.run();
}
}
});
}
private String restoreSessionTabs(final boolean isExternalURL) throws SessionRestoreException {
try {
String sessionString = getProfile().readSessionFile(false);
@ -1808,36 +1847,13 @@ public abstract class GeckoApp
String uri = intent.getDataString();
Tabs.getInstance().loadUrl(uri);
} else if (Intent.ACTION_VIEW.equals(action)) {
// We need to ensure that if we receive a VIEW action and there are tabs queued then the
// site loaded from the intent is op top (last loaded) and selected with all other tabs
// being opened behind it. We process the tab queue first and request a callback from the JS - the
// listener will open the url from the intent as normal when the tab queue has been processed.
ThreadUtils.postToBackgroundThread(new Runnable() {
processActionViewIntent(new Runnable() {
@Override
public void run() {
if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_ANDROID_TAB_QUEUE
&& TabQueueHelper.shouldOpenTabQueueUrls(GeckoApp.this)) {
EventDispatcher.getInstance().registerGeckoThreadListener(new NativeEventListener() {
@Override
public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
if ("Tabs:TabsOpened".equals(event)) {
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Tabs:TabsOpened");
String uri = intent.getDataString();
Tabs.getInstance().loadUrl(uri, Tabs.LOADURL_NEW_TAB |
Tabs.LOADURL_USER_ENTERED |
Tabs.LOADURL_EXTERNAL);
}
}
}, "Tabs:TabsOpened");
TabQueueHelper.openQueuedUrls(GeckoApp.this, mProfile, TabQueueHelper.FILE_NAME, true);
} else {
final String url = intent.getDataString();
Tabs.getInstance().loadUrlWithIntentExtras(url, intent, Tabs.LOADURL_NEW_TAB |
Tabs.LOADURL_USER_ENTERED |
Tabs.LOADURL_EXTERNAL);
}
final String url = intent.getDataString();
Tabs.getInstance().loadUrlWithIntentExtras(url, intent, Tabs.LOADURL_NEW_TAB |
Tabs.LOADURL_USER_ENTERED |
Tabs.LOADURL_EXTERNAL);
}
});
} else if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {

View File

@ -15,6 +15,8 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.ByteBuffer;
@ -1188,6 +1190,22 @@ public class GeckoAppShell
}
final String scheme = uri.getScheme();
if ("intent".equals(scheme)) {
final Intent intent;
try {
intent = Intent.parseUri(targetURI, Intent.URI_INTENT_SCHEME);
} catch (final URISyntaxException e) {
Log.e(LOGTAG, "Unable to parse URI - " + e);
return null;
}
// We only handle explicit Intents at the moment (see bug 851693 comment 20).
if (intent.getPackage() == null) {
return null;
}
return intent;
}
// Compute our most likely intent, then check to see if there are any
// custom handlers that would apply.

Some files were not shown because too many files have changed in this diff Show More