Merge fx-team to m-c.

This commit is contained in:
Ryan VanderMeulen 2013-08-09 19:28:34 -04:00
commit 3daac1182d
119 changed files with 5578 additions and 2027 deletions

View File

@ -140,6 +140,7 @@ We'd like to thank our many Jetpack project contributors! They include:
* Tim Taubert
* Shane Tomlinson
* Dave Townsend
* [Fraser Tweedale](https://github.com/frasertweedale)
* [Matthias Tylkowski](https://github.com/tylkomat)
### V ###

View File

@ -0,0 +1,272 @@
<!-- 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/. -->
#Classes and Inheritance
A class is a blueprint from which individual objects are created. These
individual objects are the instances of the class. Each class defines one or
more members, which are initialized to a given value when the class is
instantiated. Data members are properties that allow each instance to have
their own state, whereas member functions are properties that allow instances to
have behavior. Inheritance allows classes to inherit state and behavior from an
existing classes, known as the base class. Unlike languages like C++ and Java,
JavaScript does not have native support for classical inheritance. Instead, it
uses something called prototypal inheritance. As it turns out, it is possible to
emulate classical inheritance using prototypal inheritance, but not without
writing a significant amount of boilerplate code.
Classes in JavaScript are defined using constructor functions. Each constructor
function has an associated object, known as its prototype, which is shared
between all instances of that class. We will show how to define classes using
constructors, and how to use prototypes to efficiently define member functions
on each instance. Classical inheritance can be implemented in JavaScript using
constructors and prototypes. We will show how to make inheritance work correctly
with respect to constructors, prototypes, and the instanceof operator, and how
to override methods in subclasses. The SDK uses a special constructor internally,
known as `Class`, to create constructors that behave properly with respect to
inheritance. The last section shows how to work with the `Class` constructor. It
is possible to read this section on its own. However, to fully appreciate how
`Class` works, and the problem it is supposed to solve, it is recommended that
you read the entire article.
##Constructors
In JavaScript, a class is defined by defining a constructor function for that
class. To illustrate this, let's define a simple constructor for a class
`Shape`:
function Shape(x, y) {
this.x = x;
this.y = y;
}
We can now use this constructor to create instances of `Shape`:
let shape = new Shape(2, 3);
shape instanceof Shape; // => true
shape.x; // => 2
shape.y; // => 3
The keyword new tells JavaScript that we are performing a constructor call.
Constructor calls differ from ordinary function calls in that JavaScript
automatically creates a new object and binds it to the keyword this for the
duration of the call. Moreover, if the constructor does not return a value, the
result of the call defaults to the value of this. Constructors are just ordinary
functions, however, so it is perfectly legal to perform ordinary function calls
on them. In fact, some people (including the Add-on SDK team) prefer to use
constructors this way. However, since the value of this is undefined for
ordinary function calls, we need to add some boilerplate code to convert them to
constructor calls:
function Shape(x, y) {
if (!this)
return new Shape(x, y);
this.x = x;
this.y = y;
}
##Prototypes
Every object has an implicit property, known as its prototype. When JavaScript
looks for a property, it first looks for it in the object itself. If it cannot
find the property there, it looks for it in the object's prototype. If the
property is found on the prototype, the lookup succeeds, and JavaScript pretends
that it found the property on the original object. Every function has an
explicit property, known as `prototype`. When a function is used in a
constructor call, JavaScript makes the value of this property the prototype of
the newly created object:
let shape = Shape(2, 3);
Object.getPrototypeOf(shape) == Shape.prototype; // => true
All instances of a class have the same prototype. This makes the prototype the
perfect place to define properties that are shared between instances of the
class. To illustrate this, let's add a member function to the class `Shape`:
Shape.prototype.draw = function () {
throw Error("not yet implemented");
}
let shape = Shape(2, 3);
Shape.draw(); // => Error: not yet implemented
##Inheritance and Constructors
Suppose we want to create a new class, `Circle`, and inherit it from `Shape`.
Since every `Circle` is also a `Shape`, the constructor for `Circle` must be
called every time we call the constructor for `Shape`. Since JavaScript does
not have native support for inheritance, it doesn't do this automatically.
Instead, we need to call the constructor for `Shape` explicitly. The resulting
constructor looks as follows:
function Circle(x, y, radius) {
if (!this)
return new Circle(x, y, radius);
Shape.call(this, x, y);
this.radius = radius;
}
Note that the constructor for `Shape` is called as an ordinary function, and
reuses the object created for the constructor call to `Circle`. Had we used a
constructor call instead, the constructor for `Shape` would have been applied to
a different object than the constructor for `Circle`. We can now use the above
constructor to create instances of the class `Circle`:
let circle = Circle(2, 3, 5);
circle instanceof Circle; // => true
circle.x; // => 2
circle.y; // => 3
circle.radius; // => 5
##Inheritance and Prototypes
There is a problem with the definition of `Circle` in the previous section that
we have glossed over thus far. Consider the following:
let circle = Circle(2, 3, 5);
circle.draw(); // => TypeError: circle.draw is not a function
This is not quite right. The method `draw` is defined on instances of `Shape`,
so we definitely want it to be defined on instances of `Circle`. The problem is
that `draw` is defined on the prototype of `Shape`, but not on the prototype of
`Circle`. We could of course copy every property from the prototype of `Shape`
over to the prototype of `Circle`, but this is needlessly inefficient. Instead,
we use a clever trick, based on the observation that prototypes are ordinary
objects. Since prototypes are objects, they have a prototype as well. We can
thus override the prototype of `Circle` with an object which prototype is the
prototype of `Shape`.
Circle.prototype = Object.create(Shape.prototype);
Now when JavaScript looks for the method draw on an instance of Circle, it first
looks for it on the object itself. When it cannot find the property there, it
looks for it on the prototype of `Circle`. When it cannot find the property
there either, it looks for it on `Shape`, at which point the lookup succeeds.
The resulting behavior is what we were aiming for.
##Inheritance and Instanceof
The single line of code we added in the previous section solved the problem with
prototypes, but introduced a new problem with the **instanceof** operator.
Consider the following:
let circle = Circle(2, 3, 5);
circle instanceof Shape; // => false
Since instances of `Circle` inherit from `Shape`, we definitely want the result
of this expression to be true. To understand why it is not, we need to
understand how **instanceof** works. Every prototype has a `constructor`
property, which is a reference to the constructor for objects with this
prototype. In other words:
Circle.prototype.constructor == Circle // => true
The **instanceof** operator compares the `constructor` property of the prototype
of the left hand side with that of the right hand side, and returns true if they
are equal. Otherwise, it repeats the comparison for the prototype of the right
hand side, and so on, until either it returns **true**, or the prototype becomes
**null**, in which case it returns **false**. The problem is that when we
overrode the prototype of `Circle` with an object whose prototype is the
prototype of `Shape`, we didn't correctly set its `constructor` property. This
property is set automatically for the `prototype` property of a constructor, but
not for objects created with `Object.create`. The `constructor` property is
supposed to be non-configurable, non-enumberable, and non-writable, so the
correct way to define it is as follows:
Circle.prototype = Object.create(Shape.prototype, {
constructor: {
value: Circle
}
});
##Overriding Methods
As a final example, we show how to override the stub implementation of the
method `draw` in `Shape` with a more specialized one in `Circle`. Recall that
JavaScript returns the first property it finds when walking the prototype chain
of an object from the bottom up. Consequently, overriding a method is as simple
as providing a new definition on the prototype of the subclass:
Circle.prototype.draw = function (ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius,
0, 2 * Math.PI, false);
ctx.fill();
};
With this definition in place, we get:
let shape = Shape(2, 3);
shape.draw(); // Error: not yet implemented
let circle = Circle(2, 3, 5);
circle.draw(); // TypeError: ctx is not defined
which is the behavior we were aiming for.
##Classes in the Add-on SDK
We have shown how to emulate classical inheritance in JavaScript using
constructors and prototypes. However, as we have seen, this takes a significant
amount of boilerplate code. The Add-on SDK team consists of highly trained
professionals, but they are also lazy: that is why the SDK contains a helper
function that handles this boilerplate code for us. It is defined in the module
“core/heritage”:
const { Class } = require('sdk/core/heritage');
The function `Class` is a meta-constructor: it creates constructors that behave
properly with respect to inheritance. It takes a single argument, which is an
object which properties will be defined on the prototype of the resulting
constructor. The semantics of `Class` are based on what we've learned earlier.
For instance, to define a constructor for a class `Shape` in terms of `Class`,
we can write:
let Shape = Class({
initialize: function (x, y) {
this.x = x;
this.y = y;
},
draw: function () {
throw new Error("not yet implemented");
}
});
The property `initialize` is special. When it is present, the call to the
constructor is forwarded to it, as are any arguments passed to it (including the
this object). In effect, initialize specifies the body of the constructor. Note
that the constructors created with `Class` automatically check whether they are
called as constructors, so an explicit check is no longer necessary.
Another special property is `extends`. It specifies the base class from which
this class inherits, if any. `Class` uses this information to automatically set
up the prototype chain of the constructor. If the extends property is omitted,
`Class` itself is used as the base class:
var shape = new Shape(2, 3);
shape instanceof Shape; // => true
shape instanceof Class; // => true
To illustrate the use of the `extends` property, let's redefine the constructor
for the class `Circle` in terms of `Class`:
var Circle = Class({
extends: Shape,
initialize: function(x, y, radius) {
Shape.prototype.initialize.call(this, x, y);
this.radius = radius;
},
draw: function () {
context.beginPath();
context.arc(this.x, this.y, this.radius,
0, 2 * Math.PI, false);
context.fill();
}
});
Unlike the definition of `Circle` in the previous section, we no longer have to
override its prototype, or set its `constructor` property. This is all handled
automatically. On the other hand, the call to the constructor for `Shape` still
has to be made explicitly. This is done by forwarding to the initialize method
of the prototype of the base class. Note that this is always safe, even if there
is no `initialize` method defined on the base class: in that case the call is
forwarded to a stub implementation defined on `Class` itself.
The last special property we will look into is `implements`. It specifies a list
of objects, which properties are to be copied to the prototype of the
constructor. Note that only properties defined on the object itself are copied:
properties defined on one of its prototypes are not. This allows objects to
inherit from more than one class. It is not true multiple inheritance, however:
no constructors are called for objects inherited via `implements`, and
**instanceof** only works correctly for classes inherited via `extends`.

View File

@ -0,0 +1,149 @@
<!-- 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/. -->
#Content Processes
A content process was supposed to run all the code associated with a single tab.
Conversely, an add-on process was supposed to run all the code associated with a
single add-on. Neither content or add-on proceses were ever actually
implemented, but by the time they were cancelled, the SDK was already designed
with them in mind. To understand this article, it's probably best to read it as
if content and add-on processes actually exist.
To communicate between add-on and content processes, the SDK uses something
called content scripts. These are explained in the first section. Content
scripts communicate with add-on code using something called event emitters.
These are explained in the next section. Content workers combine these ideas,
allowing you to inject a content script into a content process, and
automatically set up a communication channel between them. These are explained
in the third section.
In the next section, we will look at how content scripts interact with the DOM
in a content process. There are several caveats here, all of them related to
security, that might cause things to not behave in the way you might expect.
The final section explains why the SDK still uses the notion of content scripts
and message passing, even though the multiprocess model for which they were
designed never materialized. This, too, is primarily related to security.
##Content Scripts
When the SDK was first designed, Firefox was being refactored towards a
multiprocess model. In this model, the UI would be rendered in one process
(called the chrome process), whereas each tab and each add-on would run in their
own dedicated process (called content and add-on processes, respectively). The
project behind this refactor was known as Electrolysis, or E10s. Although E10s
has now been suspended, the SDK was designed with this multiprocess model in
mind. Afterwards, it was decided to keep the design the way it is: even though
its no longer necessary, it turns out that from a security point of view there
are several important advantages to thinking about content and add-on code as
living in different processes.
Many add-ons have to interact with content. The problem with the multiprocess
model is that add-ons and content are now in different processes, and scripts in
one process cannot interact directly with scripts in another. We can, however,
pass JSON messages between scripts in different processes. The solution we've
come up with is to introduce the notion of content scripts. A content script is
a script that is injected into a content process by the main script running in
the add-on process. Content scripts differ from scripts that are loaded by the
page itself in that they are provided with a messaging API that can be used to
send messages back to the add-on script.
##Event Emitters
The messaging API we use to send JSON messages between scripts in different
processes is based on the use of event emitters. An event emitter maintains a
list of callbacks (or listeners) for one or more named events. Each event
emitter has several methods: the method on is used to add a listener for an
event. Conversely, the method removeListener is used to remove a listener for an
event. The method once is a helper function which adds a listener for an event,
and automatically removes it the first time it is called.
Each event emitter has two associated emit functions. One emit function is
associated with the event emitter itself. When this function is called with a
given event name, it calls all the listeners currently associated with that
event. The other emit function is associated with another event emitter: it was
passed as an argument to the constructor of this event emitter, and made into a
method. Calling this method causes an event to be emitted on the other event
emitter.
Suppose we have two event emitters in different processes, and we want them to
be able to emit events to each other. In this case, we would replace the emit
function passed to the constructor of each emitter with a function that sends a
message to the other process. We can then hook up a listener to be called when
this message arrives at the other process, which in turn calls the emit function
on the other event emitter. The combination of this function and the
corresponding listener is referred to as a pipe.
##Content Workers
A content worker is an object that is used to inject content scripts into a
content process, and to provide a pipe between each content script and the main
add-on script. The idea is to use a single content worker for each content
process. The constructor for the content worker takes an object containing one
or more named options. Among other things, this allows us to specify one or more
content scripts to be loaded.
When a content script is first loaded, the content worker automatically imports
a messaging API that allows the it to emit messages over a pipe. On the add-on
side, this pipe is exposed via the the port property on the worker. In addition
to the port property, workers also support the web worker API, which allows
scripts to send messages to each other using the postMessage function. This
function uses the same pipe internally, and causes a 'message' event to be
emitted on the other side.
As explained earlier, Firefox doesn't yet use separate processes for tabs or
add-ons, so instead, each content script is loaded in a sandbox. Sandboxes were
explained [this article]("dev-guide/guides/contributors-guide/modules.html").
##Accessing the DOM
The global for the content sandbox has the window object as its prototype. This
allows the content script to access any property on the window object, even
though that object lives outside the sandbox. Recall that the window object
inside the sandbox is actually a wrapper to the real object. A potential
problem with the content script having access to the window object is that a
malicious page could override methods on the window object that it knows are
being used by the add-on, in order to trick the add-on into doing something it
does not expect. Similarly, if the content script defines any values on the
window object, a malicious page could potentially steal that information.
To avoid problems like this, content scripts should always see the built-in
properties of the window object, even when they are overridden by another
script. Conversely, other scripts should not see any properties added to the
window object by the content script. This is where xray wrappers come in. Xray
wrappers automatically wrap native objects like the window object, and only
exposes their native properties, even if they have been overridden on the
wrapped object. Conversely, any properties defined on the wrapper are not
visible from the wrapped object. This avoids both problems we mentioned earlier.
The fact that you can't override the properties of the window object via a
content script is sometimes inconvenient, so it is possible to circumvent this:
by defining the property on window.wrappedObject, the property is defined on the
underlying object, rather than the wrapper itself. This feature should only be
used when you really need it, however.
##A few Notes on Security
As we stated earlier, the SDK was designed with multiprocess support in mind,
despite the fact that work on implementing this in Firefox has currently been
suspended. Since both add-on modules and content scripts are currently loaded in
sandboxes rather than separate processes, and sandboxes can communicate with
each other directly (using imports/exports), you might be wondering why we have
to go through all the trouble of passing messages between add-on and content
scripts. The reason for this extra complexity is that the code for add-on
modules and content scripts has different privileges. Every add-on module can
get chrome privileges simply by asking for them, whereas content scripts have
the same privileges as the page it is running on.
When two sandboxes have the same privileges, a wrapper in one sandbox provides
transparent access to an object in the other sandbox. When the two sandboxes
have different privileges, things become more complicated, however. Code with
content privileges should not be able to acces code with chrome privileges, so
we use specialized wrappers, called security wrappers, to limit access to the
object in the other sandbox. The xray wrappers we saw earlier are an example of
such a security wrapper. Security wrappers are created automatically, by the
underlying host application.
A full discussion of the different kinds of security wrappers and how they work
is out of scope for this document, but the main point is this: security wrappers
are very complex, and very error-prone. They are subject to change, in order to
fix some security leak that recently popped up. As a result, code that worked
just fine last week suddenly does not work the way you expect. By only passing
messages between add-on modules and content scripts, these problems can be
avoided, making your add-on both easier to debug and to maintain.

View File

@ -0,0 +1,318 @@
<!-- 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/. -->
#Getting Started
The contribution process consists of a number of steps. First, you need to get
a copy of the code. Next, you need to open a bug for the bug or feature you want
to work on, and assign it to yourself. Alternatively, you can take an existing
bug to work on. Once you've taken a bug, you can start writing a patch. Once
your patch is complete, you've made sure it doesn't break any tests, and you've
gotten a positive review for it, the last step is to request for your patch to
be merged with the main codebase.
Although these individual steps are all obvious, there are quite some details
involved. The rest of this article will cover each individual step of the
contribution process in more detail.
##Getting the Code
The Add-on SDK code is hosted on GitHub. GitHub is a web-based hosting service
for software projects that is based on Git, a distributed version control
system. Both GitHub and Git are an integral part of our workflow. If you haven't
familiarized yourself with Git before, I strongly suggest you do so now. You're
free to ignore that suggestion if you want, but it's going to hurt you later on
(don't come crying to me if you end up accidentally detaching your head, for
instance). A full explanation of how to use Git is out of scope for this
document, but a very good one
[can be found online here](http://git-scm.com/book). Reading at least sections
1-3 from that book should be enough to get you started.
If you're already familiar with Git, or if you decided to ignore my advice and
jump right in, the following steps will get you a local copy of the Add-on SDK
code on your machine:
1. Fork the SDK repository to your GitHub account
2. Clone the forked repository to your machine
A fork is similar to a clone in that it creates a complete copy of a repository,
including the history of every file. The difference is that a fork copies the
repository to your GitHub account, whereas a clone copies it to your machine. To
create a fork of the SDK repository, you need a GitHub account. If you don't
already have one, you can [create one here](https://github.com/) (don't worry:
it's free!). Once you got yourself an account, go to
[the Add-on SDK repository](https://github.com/mozilla/addon-sdk), and click the
fork button in the upper-right corner. This will start the forking process.
This could take anywhere between a couple of seconds and a couple of minutes.
Once the forking process is complete, the forked repository will be available at
https://github.com/\<your-username\>/addon-sdk. To create a clone of the this
repository, you need to have Git installed on your machine. If you dont have it
already, you can [download it here](http://git-scm.com/). Once you have Git
installed (make sure you also configured your name and e-mail
address), open your terminal, and enter the following command from the directory
where you want to have the clone stored:
> `git clone ssh://github.com/<your-username>/addon-sdk`
This will start the cloning process. Like the forking process, this could take
anywhere between a couple of seconds and a couple of minutes, depending on the
speed of your connection.
If you did everything correctly so far, once the cloning process is complete,
the cloned repository will have been stored inside the directory from which you
ran the clone command, in a new directory called addon-sdk. Now we can start
working with it. Yay!
As a final note: it is possible to skip step 1, and clone the SDK repository
directly to your machine. This is useful if you only want to study the SDK code.
However, if your goal is to actually contribute to the SDK, skipping step 1 is a
bad idea, because you wont be able to make pull requests in that case.
##Opening a Bug
In any large software project, keeping track of bugs is crucially important.
Without it, developers wouldn't be able to answer questions such as: what do I
need to work on, who is working on what, etc. Mozilla uses its own web-based,
general-purpose bugtracker, called Bugzilla, to keep track of bugs. Like GitHub
and Git, Bugzilla is an integral part of our workflow. When you discover a new
bug, or want to implement a new feature, you start by creating an entry for it
in Bugzilla. By doing so, you give the SDK team a chance to confirm whether your
bug isn't actually a feature, or your feature isn't actually a bug
(that is, a feature we feel doesn't belong into the SDK).
Within Bugzilla, the term _bug_ is often used interchangably to refer to both
bugs and features. Similarly, the Bugzilla entry for a bug is also named bug,
and the process of creating it is known as _opening a bug_. It is important that
you understand this terminology, as other people will regularly refer to it.
I really urge you to open a bug first and wait for it to get confirmed before
you start working on something. Nothing sucks more than your patch getting
rejected because we felt it shouldn't go into the SDK. Having this discussion
first saves you from doing useless work. If you have questions about a bug, but
don't know who to ask (or the person you need to ask isn't online), Bugzilla is
the communication channel of choice. When you open a bug, the relevant people
are automatically put on the cc-list, so they will get an e-mail every time you
write a comment in the bug.
To open a bug, you need a Bugzilla account. If you don't already have one, you
can [create it here](https://bugzilla.mozilla.org/). Once you got yourself an
account, click the "new" link in the upper-left corner. This will take you to a
page where you need to select the product that is affected by your bug. It isn't
immediately obvious what you should pick here (and with not immediately obvious
I mean completely non-obvious), so I'll just point it out to you: as you might
expect, the Add-on SDK is listed under "Other products", at the bottom of the
page.
After selecting the Add-on SDK, you will be taken to another page, where you
need to fill out the details for the bug. The important fields are the component
affected by this bug, the summary, and a short description of the bug (don't
worry about coming up with the perfect description for your bug. If something is
not clear, someone from the SDK team will simply write a comment asking for
clarification). The other fields are optional, and you can leave them as is, if
you so desire.
Note that when you fill out the summary field, Bugzilla automatically looks for
bugs that are possible duplicates of the one you're creating. If you spot such a
duplicate, there's no need to create another bug. In fact, doing so is
pointless, as duplicate bugs are almost always immediately closed. Don't worry
about accidentally opening a duplicate bug though. Doing so is not considered a
major offense (unless you do it on purpose, of course).
After filling out the details for the bug, the final step is to click the
"Submit Bug" button at the bottom of the page. Once you click this button, the
bug will be stored in Bugzillas database, and the creation process is
completed. The initial status of your bug will be `UNCONFIRMED`. All you need to
do now is wait for someone from the SDK team to change the status to either
`NEW` or `WONTFIX`.
##Taking a Bug
Since this is a contributor's guide, I've assumed until now that if you opened a
bug, you did so with the intention of fixing it. Simply because you're the one
that opened it doesn't mean you have to fix a bug, however. Conversely, simply
because you're _not_ the one that opened it doesn't mean you can't fix a bug. In
fact, you can work on any bug you like, provided nobody else is already working
on it. To check if somebody is already working on a bug, go to the entry for
that bug and check the "Assigned To" field. If it says "Nobody; OK to take it
and work on it", you're good to go: you can assign the bug to yourself by
clicking on "(take)" right next to it.
Keep in mind that taking a bug to creates the expectation that you will work on
it. It's perfectly ok to take your time, but if this is the first bug you're
working on, you might want to make sure that this isn't something that has very
high priority for the SDK team. You can do so by checking the importance field
on the bug page (P1 is the highest priority). If you've assigned a bug to
yourself that looked easy at the time, but turns out to be too hard for you to
fix, don't feel bad! It happens to all of us. Just remove yourself as the
assignee for the bug, and write a comment explaining why you're no longer able
to work on it, so somebody else can take a shot at it.
A word of warning: taking a bug that is already assigned to someone else is
considered extremely rude. Just imagine yourself working hard on a series of
patches, when suddenly this jerk comes out of nowhere and submits his own
patches for the bug. Not only is doing so an inefficient use of time, it also
shows a lack of respect for other the hard work of other contributors. The other
side of the coin is that contributors do get busy every now and then, so if you
stumble upon a bug that is already assigned to someone else but hasn't shown any
activity lately, chances are the person to which the bug is assigned will gladly
let you take it off his/her hands. The general rule is to always ask the person
assigned to the bug if it is ok for you to take it.
As a final note, if you're not sure what bug to work on, or having a hard time
finding a bug you think you can handle, a useful tip is to search for the term
"good first bug". Bugs that are particularly easy, or are particularly well
suited to familiarize yourself with the SDK, are often given this label by the
SDK team when they're opened.
##Writing a Patch
Once you've taken a bug, you're ready to start doing what you really want to do:
writing some code. The changes introduced by your code are known as a patch.
Your goal, of course, is to get this patch landed in the main SDK repository. In
case you aren't familiar with git, the following command will cause it to
generate a diff:
> `git diff`
A diff describes all the changes introduced by your patch. These changes are not
yet final, since they are not yet stored in the repository. Once your patch is
complete, you can _commit_ it to the repository by writing:
> `git commit`
After pressing enter, you will be prompted for a commit message. What goes in
the commit message is more or less up to you, but you should at least include
the bug number and a short summary (usually a single line) of what the patch
does. This makes it easier to find your commit later on.
It is considered good manners to write your code in the same style as the rest
of a file. It doesn't really matter what coding style you use, as long as it's
consistent. The SDK might not always use the exact same coding style for each
file, but it strives to be as consistent as possible. Having said that: if
you're not completely sure what coding style to use, just pick something and
don't worry about it. If the rest of the file doesn't make it clear what you
should do, it most likely doesn't matter.
##Making a Pull Request
To submit a patch for review, you need to make a pull request. Basically, a pull
request is a way of saying: "Hey, I've created this awesome patch on top of my
fork of the SDK repository, could you please merge it with the global
repository?". GitHub has built-in support for pull requests. However, you can
only make pull requests from repositories on your GitHub account, not from
repositories on your local machine. This is why I told you to fork the SDK
repository to your GitHub account first (you did listen to me, didn't you?).
In the previous section, you commited your patch to your local repository, so
here, the next step is to synchronize your local repository with the remote one,
by writing:
> `git push`
This pushes the changes from your local repository into the remote repository.
As you might have guessed, a push is the opposite of a pull, where somebody else
pulls changes from a remote repository into their own repository (hence the term
'pull request'). After pressing enter, GitHub will prompt you for your username
and password before actually allowing the push.
If you did everything correctly up until this point, your patch should now show
up in your remote repository (take a look at your repository on GitHub to make
sure). We're now ready to make a pull request. To do so, go to your repository
on GitHub and click the "Pull Request" button at the top of the page. This will
take you to a new page, where you need to fill out the title of your pull
request, as well as a short description of what the patch does. As we said
before, it is common practice to at least include the bug number and a short
summary in the title. After you've filled in both fields, click the "Send Pull
Request" button.
That's it, we're done! Or are we? This is software development after all, so
we'd expect there to be at least one redundant step. Luckily, there is such a
step, because we also have to submit our patch for review on Bugzilla. I imagine
you might be wondering to yourself right now: "WHY???". Let me try to explain.
The reason we have this extra step is that most Mozilla projects use Mercurial
and Bugzilla as their version control and project management tool, respectively.
To stay consistent with the rest of Mozilla, we provide a Mercurial mirror of
our Git repository, and submit our patches for review in both GitHub and
Bugzilla.
If that doesn't make any sense to you, that's ok: it doesn't to me, either. The
good news, however, is that you don't have to redo all the work you just did.
Normally, when you want to submit a patch for review on Bugzilla, you have to
create a diff for the patch and add it as an attachment to the bug (if you still
haven't opened one, this would be the time to do it). However, these changes are
also described by the commit of your patch, so its sufficient to attach a file
that links to the pull request. To find the link to your pull request, go to
your GitHub account and click the "Pull Requests" button at the top. This will
take you to a list of your active pull requests. You can use the template here
below as your attachment. Simply copy the link to your pull request, and use it
to replace all instances of \<YOUR_LINK_HERE\>:
<!DOCTYPE html>
<meta charset="utf-8">
<meta http-equiv="refresh" content="<YOUR_LINK_HERE>">
<title>Bugzilla Code Review</title>
<p>You can review this patch at <a href="<YOUR_LINK_HERE >"><YOUR_LINK_HERE></a>,
or wait 5 seconds to be redirected there automatically.</p>
Finally, to add the attachment to the bug, go to the bug in Bugzilla, and click
on "Add an attachment" right above the comments. Make sure you fill out a
description for the attachment, and to set the review flag to '?' (you can find
a list of reviewers on
[this page](https://github.com/mozilla/addon-sdk/wiki/contribute)). The '?' here
means that you're making a request. If your patch gets a positive review, the
reviewer will set this flag to '+'. Otherwise, he/she will set it to '-', with
some feedback on why your patch got rejected. Of course, since we also use
GitHub for our review process, you're most likely to get your feedback there,
instead of Bugzilla. If your patch didn't get a positive review right away,
don't sweat it. If you waited for your bug to get confirmed before submitting
your patch, you'll usually only have to change a few small things to get a
positive review for your next attempt. Once your patch gets a positive review,
you don't need to do anything else. Since you did a pull request, it will
automatically be merged into the remote repository, usually by the person that
reviewed your patch.
##Getting Additional Help
If something in this article wasn't clear to you, or if you need additional
help, the best place to go is irc. Mozilla relies heavily on irc for direct
communication between contributors. The SDK team hangs out on the #jetpack
channel on the irc.mozilla.org server (Jetpack was the original name of the
SDK, in case you're wondering).
Unless you are know what you are doing, it can be hard to get the information
you need from irc, uso here are a few useful tips:
* Mozilla is a global organization, with contributors all over the world, so the
people you are trying to reach are likely not in the same timezone as you.
Most contributors to the SDK are currently based in the US, so if you're in
Europe, and asking a question on irc in the early afternoon, you're not likely
to get many replies.
* Most members of the SDK team are also Mozilla employees, which means they're
often busy doing other stuff. That doesn't mean they don't want to help you.
On the contrary: Mozilla encourages employees to help out contributors
whenever they can. But it does mean that we're sometimes busy doing other
things than checking irc, so your question may go unnoticed. If that happens,
the best course of action is often to just ask again.
* If you direct your question to a specific person, rather than the entire
channel, your chances of getting an answer are a lot better. If you prefix
your message with that person's irc name, he/she will get a notification in
his irc client. Try to make sure that the person you're asking is actually the
one you need, though. Don't just ask random questions to random persons in the
hopes you'll get more response that way.
* If you're not familiar with irc, a common idiom is to send someone a message
saying "ping" to ask if that person is there. When that person actually shows
up and sees the ping, he will send you a message back saying "pong". Cute,
isn't it? But hey, it works.
* Even if someone does end up answering your questions, it can happen that that
person gets distracted by some other task and forget he/she was talking to
you. Please don't take that as a sign we don't care about your questions. We
do, but we too get busy sometimes: we're only human. If you were talking to
somebody and haven't gotten any reply to your last message for some time, feel
free to just ask again.
* If you've decided to pick up a good first bug, you can (in theory at least)
get someone from the SDK team to mentor you. A mentor is someone who is
already familiar with the code who can walk you through it, and who is your go
to guy in case you have any questions about it. The idea of mentoring was
introduced a while ago to make it easier for new contributors to familiarize
themselves with the code. Unfortunately, it hasn't really caught on yet, but
we're trying to change that. So by all means: ask!

View File

@ -0,0 +1,316 @@
<!-- 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/. -->
#Modules
A module is a self-contained unit of code, which is usually stored in a file,
and has a well defined interface. The use of modules greatly improves the
maintainability of code, by splitting it up into independent components, and
enforcing logical boundaries between them. Unfortunately, JavaScript does not
yet have native support for modules: it has to rely on the host application to
provide it with functionality such as loading subscripts, and exporting/
importing names. We will show how to do each of these things using the built-in
Components object provided by Xulrunner application such as Firefox and
Thunderbird.
To improve encapsulation, each module should be defined in the scope of its own
global object. This is made possible by the use of sandboxes. Each sandbox lives
in its own compartment. A compartment is a separate memory space. Each
compartment has a set of privileges that determines what scripts running in that
compartment can and cannot do. We will show how sandboxes and compartments can
be used to improve security in our module system.
The module system used by the SDK is based on the CommonJS specification: it is
implemented using a loader object, which handles all the bookkeeping related to
module loading, such as resolving and caching URLs. We show how to create your
own custom loaders, using the `Loader` constructor provided by the SDK. The SDK
uses its own internal loader, known as Cuddlefish. All modules within the SDK
are loaded using Cuddlefish by default. Like any other custom loader, Cuddlefish
is created using the `Loader` constructor. In the final section, we will take a
look at some of the options passed by the SDK to the `Loader` constructor to
create the Cuddlefish loader.
##Loading Subscripts
When a JavaScript project reaches a certain size, it becomes necessary to split
it up into multiple files. Unfortunately, JavaScript does not provide any means
to load scripts from other locations: we have to rely on the host application to
provide us with this functionality. Applications such as Firefox and Thunderbird
are based on Xulrunner. Xulrunner adds a built-in object, known as `Components`,
to the global scope. This object forms the central access point for all
functionality provided by the host application. A complete explanation of how to
use `Components` is out of scope for this document. However, the following
example shows how it can be used to load scripts from other locations:
const {
classes: Cc
interfaces: Ci
} = Components;
var instance = Cc["@mozilla.org/moz/jssubscript-loader;1"];
var loader = instance.getService(Ci.mozIJSSubScriptLoader);
function loadScript(url) {
loader.loadSubScript(url);
}
When a script is loaded, it is evaluated in the scope of the global object of
the script that loaded it. Any property defined on the global object will be
accessible from both scripts:
index.js:
loadScript("www.foo.com/a.js");
foo; // => 3
a.js:
foo = 3;
##Exporting Names
The script loader we obtained from the `Components` object allows us load
scripts from other locations, but its API is rather limited. For instance, it
does not know how to handle relative URLs, which is cumbersome if you want to
organize your project hierarchically. A more serious problem with the
`loadScript` function, however, is that it evaluates all scripts in the scope of
the same global object. This becomes a problem when two scripts try to define
the same property:
index.js:
loadScript("www.foo.com/a.js");
loadScript("www.foo.com/b.js");
foo; // => 5
a.js:
foo = 3;
b.js:
foo = 5;
In the above example, the value of `foo` depends on the order in which the
subscripts are loaded: there is no way to access the property foo defined by
"a.js", since it is overwritten by "b.js". To prevent scripts from interfering
with each other, `loadScript` should evaluate each script to be loaded in the
scope of their own global object, and then return the global object as its
result. In effect, any properties defined by the script being loaded on its
global object are exported to the loading script. The script loader we obtained
from `Components` allows us to do just that:
function loadScript(url) {
let global = {};
loader.loadSubScript(url, global);
return global;
}
If present, the `loadSubScript` function evaluates the script to be loaded in
the scope of the second argument. Using this new version of `loadScript`, we can
now rewrite our earlier example as follows
index.js:
let a = loadScript("www.foo.com/a.js");
let b = loadScript("www.foo.com/b.js");
a.foo // => 3
b.foo; // => 5
a.js:
foo = 3;
b.js:
foo = 5;:
##Importing Names
In addition to exporting properties from the script being loaded to the loading
script, we can also import properties from the loading script to the script
being loaded:
function loadScript(url, imports) {
let global = {
imports: imports,
exports: {}
};
loader.loadSubScript(url, global);
return global.exports;
}
Among other things, this allows us to import `loadScript` to scripts being
loaded, allowing them to load further scripts:
index.js:
loadScript("www.foo.com/a.js", {
loadScript: loadScript
}).foo; => 5
a.js:
exports.foo = imports.loadScript("www.foo.com/b.js").bar;
b.js:
exports.bar = 5;
##Sandboxes and Compartments
The `loadScript` function as defined int the previous section still has some
serious shortcomings. The object it passed to the `loadSubScript` function is an
ordinary object, which has the global object of the loading script as its
prototype. This breaks encapsulation, as it allows the script being loaded to
access the built-in constructors of the loading script, which are defined on its
global object. The problem with breaking encapsulation like this is that
malicious scripts can use it to get the loading script to execute arbitrary
code, by overriding one of the methods on the built-in constructors. If the
loading script has chrome privileges, then so will any methods called by the
loading script, even if that method was installed by a malicious script.
To avoid problems like this, the object passed to `loadSubScript` should be a
true global object, having its own instances of the built-in constructors. This
is exactly what sandboxes are for. A sandbox is a global object that lives in a
separate compartment. Compartments are a fairly recent addition to SpiderMonkey,
and can be seen as a separate memory space. Objects living in one compartment
cannot be accessed directly from another compartment: they need to be accessed
through an intermediate object, known as a wrapper. Compartments are very
useful from a security point of view: each compartment has a set of privileges
that determines what a script running in that compartment can and cannot do.
Compartments with chrome privileges have access to the `Components` object,
giving them full access to the host platform. In contrast, compartments with
content privileges can only use those features available to ordinary websites.
The `Sandbox` constructor takes a `URL` parameter, which is used to determine
the set of privileges for the compartment in which the sandbox will be created.
Passing an XUL URL will result in a compartment with chrome privileges (note,
however, that if you ever actually do this in any of your code, Gabor will be
forced to hunt you down and kill you). Otherwise, the compartment will have
content privileges by default. Rewriting the `loadScript` function using
sandboxes, we end up with:
function loadScript(url, imports) {
let global = Components.utils.Sandbox(url);
global.imports = imports;
global.exports = {};
loader.loadSubScript(url, global);
return global.exports;
}
Note that the object returned by `Sandbox` is a wrapper to the sandbox, not the
sandbox itself. A wrapper behaves exactly like the wrapped object, with one
difference: for each property access/function it performs an access check to
make sure that the calling script is actually allowed to access/call that
property/function. If the script being loaded is less privileged than the
loading script, the access is prevented, as the following example shows:
index.js:
let a = loadScript("www.foo.com/a.js", {
Components: Components
});
// index.js has chrome privileges
Components.utils; // => [object nsXPCComponents_Utils]
a.js:
// a.js has content privileges
imports.Components.utils; // => undefined
##Modules in the Add-on SDK
The module system used by the SDK is based on what we learned so far: it follows
the CommonJS specification, which attempts to define a standardized module API.
A CommonJS module defines three global variables: `require`, which is a function
that behaves like `loadScript` in our examples, `exports`, which behaves
like the `exports` object, and `module`, which is an object representing
the module itself. The `require` function has some extra features not provided
by `loadScript`: it solves the problem of resolving relative URLs (which we have
left unresolved), and provides a caching mechanism, so that when the same module
is loaded twice, it returns the cached module object rather than triggering
another download. The module system is implemented using a loader object, which
is actually provided as a module itself. It is defined in the module
“toolkit/loader”:
const { Loader } = require('toolkit/loader')
The `Loader` constructor allows you to create your own custom loader objects. It
takes a single argument, which is a named options object. For instance, the
option `paths` is used to specify a list of paths to be used by the loader to
resolve relative URLs:
let loader = Loader({
paths: ["./": http://www.foo.com/"]
});
CommonJS also defines the notion of a main module. The main module is always the
first to be loaded, and differs from ordinary modules in two respects. Firstly,
since they do not have a requiring module. Instead, the main module is loaded
using a special function, called `main`:
const { Loader, main } = require('toolkit/loader');
let loader = Loader({
paths: ["./": http://www.foo.com/"]
});
main(loader, "./main.js");
Secondly, the main module is defined as a property on `require`. This allows
modules to check if it they have been loaded as the main module:
function main() {
...
}
if (require.main === module)
main();
##The Cuddlefish Loader
The SDK uses its own internal loader, known as Cuddlefish (because we like crazy
names). Like any other custom loader, Cuddlefish is created using the `Loader`
constructor: Let's take a look at some of the options used by Cuddlefish to
customize its behavior. The way module ids are resolved can be customized by
passing a custom `resolve` function as an option. This function takes the id to
be resolved and the requiring module as an argument, and returns the resolved id
as its result. The resolved id is then further resolved using the paths array:
const { Loader, main } = require('toolkit/loader');
let loader = Loader({
paths: ["./": "http://www.foo.com/"],
resolve: function (id, requirer) {
// Your code here
return id;
}
});
main(loader, "./main.js");
Cuddlefish uses a custom `resolve` function to implement a form of access
control: modules can only require modules for which they have been explicitly
granted access. A whitelist of modules is generated statically when the add-on
is linked. It is possible to pass a list of predefined modules as an option to
the `Loader` constructor. This is useful if the API to be exposed does not have
a corresponding JS file, or is written in an incompatible format. Cuddlefish
uses this option to expose the `Components` object as a module called `chrome`,
in a way similar to the code here below:
const {
classes: Cc,
Constructor: CC,
interfaces: Ci,
utils: Cu,
results: Cr,
manager: Cm
} = Components;
let loader = Loader({
paths: ["./": "http://www.foo.com/"],
resolve: function (id, requirer) {
// Your logic here
return id;
},
modules: {
'chrome': {
components: Components,
Cc: Cc,
CC: bind(CC, Components),
Ci: Ci,
Cu: Cu,
Cr: Cr,
Cm: Cm
}
}
});
All accesses to the `chrome` module go through this one point. As a result, we
don't have to give modules chrome privileges on a case by case basis. More
importantly, however, any module that wants access to `Components` has to
explicitly express its intent via a call to `require("chrome")`. This makes it
possible to reason about which modules have chrome capabilities and which don't.

View File

@ -0,0 +1,261 @@
<!-- 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/. -->
#Private Properties
A private property is a property that is only accessible to member
functions of instances of the same class. Unlike other languages, JavaScript
does not have native support for private properties. However, people have come
up with several ways to emulate private properties using existing language
features. We will take a look at two different techniques, using prefixes, and
closures, respectively.
Prefixes and closures both have drawbacks in that they are either not
restrictive enough or too restrictive, respectively. We will therefore introduce
a third technique, based on the use of WeakMaps, that solves both these
problems. Note, however, that WeakMaps might not be supported by all
implementations yet. Next, we generalize the idea of using WeakMaps from
associating one or more private properties with an object to associating one or
more namespaces with each object. A namespace is simply an object on which one
or more private properties are defined.
The SDK uses namespaces internally to implement private properties. The last
section explains how to work with the particular namespace implementation used
by the SDK. It is possible to read this section on its own, but to fully
appreciate how namespaces work, and the problem they are supposed to solve, it
is recommended that you read the entire article.
##Using Prefixes
A common technique to implement private properties is to prefix each private
property name with an underscore. Consider the following example:
function Point(x, y) {
this._x = x;
this._y = y;
}
The properties `_x` and `_y` are private, and should only be accessed by member
functions.
To make a private property readable/writable from any function, it is common to
define a getter/setter function for the property, respectively:
Point.prototype.getX = function () {
return this._x;
};
Point.prototype.setX = function (x) {
this._x = x;
};
Point.prototype.getY = function () {
return this._y;
};
Point.prototype.setY = function (y) {
this._y = y;
};
The above technique is simple, and clearly expresses our intent. However, the
use of an underscore prefix is just a coding convention, and is not enforced by
the language: there is nothing to prevent a user from directly accessing a
property that is supposed to be private.
##Using Closures
Another common technique is to define private properties as variables, and their
getter and/or setter function as a closure over these variables:
function Point(_x, _y) {
this.getX = function () {
return _x;
};
this.setX = function (x) {
_x = x;
};
this.getY = function () {
return _y;
};
this.setY = function (y) {
_y = y;
};
}
Note that this technique requires member functions that need access to private
properties to be defined on the object itself, instead of its prototype. This is
slightly less efficient, but this is probably acceptable.
The advantage of this technique is that it offers more protection: there is no
way for the user to access a private property except by using its getter and/or
setter function. However, the use of closures makes private properties too
restrictive: since there is no way to access variables in one closure from
within another closure, there is no way for objects of the same class to access
each other's private properties.
##Using WeakMaps
The techniques we've seen so far ar either not restrictive enough (prefixes) or
too restrictive (closures). Until recently, a technique that solves both these
problems didn't exist. That changed with the introduction of WeakMaps. WeakMaps
were introduced to JavaScript in ES6, and have recently been implemented in
SpiderMonkey. Before we explain how WeakMaps work, let's take a look at how
ordinary objects can be used as hash maps, by creating a simple image cache:
let images = {};
function getImage(name) {
let image = images[name];
if (!image) {
image = loadImage(name);
images[name] = image;
}
return image;
}
Now suppose we want to associate a thumbnail with each image. Moreover, we want
to create each thumbnail lazily, when it is first required:
function getThumbnail(image) {
let thumbnail = image._thumbnail;
if (!thumbnail) {
thumbnail = createThumbnail(image);
image._thumbnail = thumbnail;
}
return thumbnail;
}
This approach is straightforward, but relies on the use of prefixes. A better
approach would be to store thumbnails in their own, separate hash map:
let thumbnails = {};
function getThumbnail(image) {
let thumbnail = thumbnails[image];
if (!thumbnail) {
thumbnail = createThumbnail(image);
thumbnails[image] = thumbnail;
}
return thumbnail;
}
There are two problems with the above approach. First, it's not possible to use
objects as keys. When an object is used as a key, it is converted to a string
using its toString method. To make the above code work, we'd have to associate a
unique identifier with each image, and override its `toString` method. The
second problem is more severe: the thumbnail cache maintains a strong reference
to each thumbnail object, so they will never be freed, even when their
corresponding image has gone out of scope. This is a memory leak waiting to
happen.
The above two problems are exactly what WeakMaps were designed to solve. A
WeakMap is very similar to an ordinary hash map, but differs from it in two
crucial ways:
1. It can use ordinary objects as keys
2. It does not maintain a strong reference to its values
To understand how WeakMaps are used in practice, let's rewrite the thumbnail
cache using WeakMaps:
let thumbnails = new WeakMap();
function getThumbnail(image) {
let thumbnail = thumbnails.get(image);
if (!thumbnail) {
thumbnail = createThumbnail(image);
thumbnails.set(image, thumbnail);
}
return thumbnail;
}
This version suffers of none of the problems we mentioned earlier. When a
thumbnail's image goes out of scope, the WeakMap ensures that its entry in the
thumbnail cache will eventually be garbage collected. As a final caveat: the
image cache we created earlier suffers from the same problem, so for the above
code to work properly, we'd have to rewrite the image cache using WeakMaps, too.
#From WeakMaps to Namespaces
In the previous section we used a WeakMap to associate a private property with
each object. Note that we need a separate WeakMap for each private property.
This is cumbersome if the number of private properties becomes large. A better
solution would be to store all private properties on a single object, called a
namespace, and then store the namespace as a private property on the original
object. Using namespaces, our earlier example can be rewritten as follows:
let map = new WeakMap();
let internal = function (object) {
if (!map.has(object))
map.set(object, {});
return map.get(object);
}
function Point(x, y) {
internal(this).x = x;
internal(this).y = y;
}
Point.prototype.getX = function () {
return internal(shape).x;
};
Point.prototype.setX = function (x) {
internal(shape).x = x;
};
Point.prototype.getY = function () {
return internal(shape).y;
};
Point.prototype.setY = function () {
internal(shape).y = y;
};
The only way for a function to access the properties `x` and `y` is if it has a
reference to an instance of `Point` and its `internal` namespace. By keeping the
namespace hidden from all functions except members of `Point`, we have
effectively implemented private properties. Moreover, because members of `Point`
have a reference to the `internal` namespace, they can access private properties
on other instances of `Point`.
##Namespaces in the Add-on SDK
The Add-on SDK is built on top of XPCOM, the interface between JavaScript and
C++ code. Since XPCOM allows the user to do virtually anything, security is very
important. Among other things, we don't want add-ons to be able to access
variables that are supposed to be private. The SDK uses namespaces internally to
ensure this. As always with code that is heavily reused, the SDK defines a
helper function to create namespaces. It is defined in the module
"core/namespace", and it's usage is straightforward. To illustrate this, let's
reimplement the class `Point` using namespaces:
const { ns } = require("./core/namespace");
var internal = ns();
function Point(x, y) {
internal(this).x = x;
internal(this).y = y;
}
Point.prototype.getX = function () {
return internal(shape).x;
};
Point.prototype.setX = function (x) {
internal(shape).x = x;
};
Point.prototype.getY = function () {
return internal(shape).y;
};
Point.prototype.setY = function () {
internal(shape).y = y;
};
As a final note, the function `ns` returns a namespace that uses the namespace
associated with the prototype of the object as its prototype.

View File

@ -8,6 +8,59 @@ This page lists more theoretical in-depth articles about the SDK.
<hr>
<h2><a name="contributors-guide">Contributor's Guide</a></h2>
<table class="catalog">
<colgroup>
<col width="50%">
<col width="50%">
</colgroup>
<tr>
<td>
<h4><a href="dev-guide/guides/contributors-guide/getting-started.html">Getting Started</a></h4>
Learn how to contribute to the SDK: getting the code, opening/taking a
bug, filing a patch, getting reviews, and getting help.
</td>
<td>
<h4><a href="dev-guide/guides/contributors-guide/private-properties.html">Private Properties</a></h4>
Learn how private properties can be implemented in JavaScript using
prefixes, closures, and WeakMaps, and how the SDK supports private
properties by using namespaces (which are a generalization of WeakMaps).
</td>
</tr>
<tr>
<td>
<h4><a href="dev-guide/guides/contributors-guide/modules.html">Modules</a></h4>
Learn about the module system used by the SDK (which is based on the
CommonJS specification), how sandboxes and compartments can be used to
improve security, and about the built-in SDK module loader, known as
Cuddlefish.
</td>
<td>
<h4><a href="dev-guide/guides/contributors-guide/content-processes.html">Content Processes</a></h4>
The SDK was designed to work in an environment where the code to
manipulate web content runs in a different process from the main add-on
code. This article highlights the main features of that design.
</td>
</tr>
<tr>
<td>
<h4><a href="dev-guide/guides/contributors-guide/classes-and-inheritance.html">Classes and Inheritance</a></h4>
Learn how classes and inheritance can be implemented in JavaScript, using
constructors and prototypes, and about the helper functions provided by
the SDK to simplify this.
</td>
<td>
</td>
</tr>
</table>
<h2><a name="sdk-infrastructure">SDK Infrastructure</a></h2>
<table class="catalog">

View File

@ -31,67 +31,21 @@ SDK-based add-ons.
## SDK Modules ##
All the modules supplied with the SDK can be found in the "lib"
directory under the SDK root. The following diagram shows a reduced view
of the SDK tree, with the "lib" directory highlighted.
The modules supplied by the SDK are divided into two sorts:
<ul class="tree">
<li>addon-sdk
<ul>
<li>app-extension</li>
<li>bin</li>
<li>data</li>
<li>doc</li>
<li>examples</li>
<li class="highlight-tree-node">lib
<ul>
<li>sdk
<ul>
<li>core
<ul>
<li>heritage.js</li>
<li>namespace.js</li>
</ul>
</li>
<li>panel.js</li>
<li>page-mod.js</li>
</ul>
</li>
<li>toolkit
<ul>
<li>loader.js</li>
</ul>
</li>
</ul>
</li>
<li>python-lib</li>
<li>test</li>
</ul>
</li>
</ul>
All modules that are specifically intended for users of the
SDK are stored under "lib" in the "sdk" directory.
[High-level modules](dev-guide/high-level-apis.html) like
* [High-level modules](dev-guide/high-level-apis.html) like
[`panel`](modules/sdk/panel.html) and
[`page-mod`](modules/sdk/page-mod.html) are directly underneath
the "sdk" directory.
[Low-level modules](dev-guide/low-level-apis.html) like
[`page-mod`](modules/sdk/page-mod.html) provide relatively simple,
stable APIs for the most common add-on development tasks.
* [Low-level modules](dev-guide/low-level-apis.html) like
[`heritage`](modules/sdk/core/heritage.html) and
[`namespace`](modules/sdk/core/heritage.html) are grouped in subdirectories
of "sdk" such as "core".
[`namespace`](modules/sdk/core/heritage.html) provide more
powerful functionality, and are typically less stable and more
complex.
Very generic, platform-agnostic modules that are shared with other
projects, such as [`loader`](modules/toolkit/loader.html), are stored
in "toolkit".
<div style="clear:both"></div>
To use SDK modules, you can pass `require` a complete path from
(but not including) the "lib" directory to the module you want to use.
For high-level modules this is just `sdk/<module_name>`, and for low-level
To use SDK modules, you can pass `require()` a complete path, starting with
"sdk", to the module you want to use. For high-level modules this is just
`sdk/<module_name>`, and for low-level
modules it is `sdk/<path_to_module>/<module_name>`:
// load the high-level "tabs" module
@ -100,13 +54,19 @@ modules it is `sdk/<path_to_module>/<module_name>`:
// load the low-level "uuid" module
var uuid = require('sdk/util/uuid');
For high-level modules only, you can also pass just the name of the module:
The path to specify for a low-level module is given along with the module
name itself in the title of the module's documentation page (for example,
[system/environment](modules/sdk/system/environment.html)).
var tabs = require("tabs");
However, this is ambiguous, as it could also refer to a local module in your
add-on named `tabs`. For this reason it is better to use the full path from
"lib".
Although the [SDK repository in GitHub](https://github.com/mozilla/addon-sdk)
includes copies of these modules, they are built into Firefox and by
default, when you run or build an add-on using
[`cfx run`](dev-guide/cfx-tool.html#cfx-run)
or [`cfx xpi`](dev-guide/cfx-tool.html#cfx-xpi), it is the versions of
the modules in Firefox that are used. If you need to use a different version
of the modules, you can do this by checking out the version of the SDK
that you need and passing the `-o` or
`--overload-modules` option to `cfx run` or `cfx xpi`.
## Local Modules ##

View File

@ -77,6 +77,27 @@ Learn about common development techniques, such as
<col width="50%">
<col width="50%">
</colgroup>
<tr>
<td>
<h4><a href="dev-guide/guides/index.html#contributors-guide">Contributor's Guide</a></h4>
Learn
<a href="dev-guide/guides/contributors-guide/getting-started.html">how to start contributing</a> to the SDK,
and about the most important idioms used in the SDK code, such as
<a href="dev-guide/guides/contributors-guide/modules.html">modules</a>,
<a href="dev-guide/guides/contributors-guide/classes-and-inheritance.html">classes and inheritance</a>,
<a href="dev-guide/guides/contributors-guide/private-properties.html">private properties</a>, and
<a href="dev-guide/guides/contributors-guide/content-processes.html">content processes</a>.
</td>
<td>
<h4><a href="dev-guide/guides/index.html#sdk-idioms">SDK idioms</a></h4>
The SDK's
<a href="dev-guide/guides/events.html">event framework</a> and the
<a href="dev-guide/guides/two-types-of-scripts.html">distinction between add-on scripts and content scripts</a>.
</td>
</tr>
<tr>
<td>
<h4><a href="dev-guide/guides/index.html#sdk-infrastructure">SDK infrastructure</a></h4>
@ -88,10 +109,11 @@ Learn about common development techniques, such as
</td>
<td>
<h4><a href="dev-guide/guides/index.html#sdk-idioms">SDK idioms</a></h4>
The SDK's
<a href="dev-guide/guides/events.html">event framework</a> and the
<a href="dev-guide/guides/two-types-of-scripts.html">distinction between add-on scripts and content scripts</a>.
<h4><a href="dev-guide/guides/index.html#xul-migration">XUL migration</a></h4>
A guide to <a href="dev-guide/guides/xul-migration.html">porting XUL add-ons to the SDK</a>.
This guide includes a
<a href="dev-guide/guides/sdk-vs-xul.html">comparison of the two toolsets</a> and a
<a href="dev-guide/guides/library-detector.html">worked example</a> of porting a XUL add-on.
</td>
</tr>
@ -106,11 +128,6 @@ Learn about common development techniques, such as
</td>
<td>
<h4><a href="dev-guide/guides/index.html#xul-migration">XUL migration</a></h4>
A guide to <a href="dev-guide/guides/xul-migration.html">porting XUL add-ons to the SDK</a>.
This guide includes a
<a href="dev-guide/guides/sdk-vs-xul.html">comparison of the two toolsets</a> and a
<a href="dev-guide/guides/library-detector.html">worked example</a> of porting a XUL add-on.
</td>
</tr>

View File

@ -168,3 +168,24 @@ add-on.
Now you have the basic `cfx` commands, you can try out the
[SDK's features](dev-guide/tutorials/index.html).
## Overriding the Built-in Modules ##
The SDK modules you use to implement your add-on are built into Firefox.
When you run or package an add-on using `cfx run` or `cfx xpi`, the add-on
will use the versions of the modules in the version of Firefox that hosts
it.
As an add-on developer, this is usually what you want. But if you're
developing the SDK modules themselves, of course it won't work at all.
In this case it's assumed that you will have checked out the SDK from
its [GitHub repo](https://github.com/mozilla/addon-sdk) and will have
run [`source/activate`](dev-guide/tutorials/installation.html) from
the root of your checkout.
Then when you invoke `cfx run` or `cfx xpi`, you pass the `"-o"` option:
<pre>cfx run -o</pre>
This instructs cfx to use the local copies of the SDK modules, not the
ones in Firefox.

View File

@ -32,20 +32,128 @@ So you can use the `indexed-db` module to access the same API:
console.log("success");
};
This module also exports all the other objects that implement
the IndexedDB API, listed below under
[API Reference](modules/sdk/indexed-db.html#API Reference).
Most of the objects that implement the IndexedDB API, such as
[IDBTransaction](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBTransaction),
[IDBOpenDBRequest](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBOpenDBRequest),
and [IDBObjectStore](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBObjectStore),
are accessible through the indexedDB object itself.
The API exposed by `indexed-db` is almost identical to the DOM IndexedDB API,
so we haven't repeated its documentation here, but refer you to the
[IndexedDB API documentation](https://developer.mozilla.org/en-US/docs/IndexedDB)
for all the details.
The database created will be unique and private per addon, and is not linked to any website database. The module cannot be used to interact with a given website database. See [bug 778197](https://bugzilla.mozilla.org/show_bug.cgi?id=779197) and [bug 786688](https://bugzilla.mozilla.org/show_bug.cgi?id=786688).
The database created will be unique and private per add-on, and is not linked
to any website database. The module cannot be used to interact with a given
website database. See
[bug 778197](https://bugzilla.mozilla.org/show_bug.cgi?id=779197) and
[bug 786688](https://bugzilla.mozilla.org/show_bug.cgi?id=786688).
## Example of Usage
## Example
[Promise-based example using indexedDB for record storage](https://github.com/gregglind/micropilot/blob/ec65446d611a65b0646be1806359c463193d5a91/lib/micropilot.js#L80-L198).
Here's a complete add-on that adds two widgets to the browser: the widget labeled
"Add" add the title of the current tab to a database, while the widget labeled
"List" lists all the titles in the database.
The add-on implements helper functions `open()`, `addItem()` and `getItems()`
to open the database, add a new item to the database, and get all items in the
database.
var { indexedDB, IDBKeyRange } = require('sdk/indexed-db');
var widgets = require("sdk/widget");
var database = {};
database.onerror = function(e) {
console.error(e.value)
}
function open(version) {
var request = indexedDB.open("stuff", version);
request.onupgradeneeded = function(e) {
var db = e.target.result;
e.target.transaction.onerror = database.onerror;
if(db.objectStoreNames.contains("items")) {
db.deleteObjectStore("items");
}
var store = db.createObjectStore("items",
{keyPath: "time"});
};
request.onsuccess = function(e) {
database.db = e.target.result;
};
request.onerror = database.onerror;
};
function addItem(name) {
var db = database.db;
var trans = db.transaction(["items"], "readwrite");
var store = trans.objectStore("items");
var time = new Date().getTime();
var request = store.put({
"name": name,
"time": time
});
request.onerror = database.onerror;
};
function getItems(callback) {
var cb = callback;
var db = database.db;
var trans = db.transaction(["items"], "readwrite");
var store = trans.objectStore("items");
var items = new Array();
trans.oncomplete = function() {
cb(items);
}
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
cursorRequest.onsuccess = function(e) {
var result = e.target.result;
if(!!result == false)
return;
items.push(result.value.name);
result.continue();
};
cursorRequest.onerror = database.onerror;
};
function listItems(itemList) {
console.log(itemList);
}
open("1");
widgets.Widget({
id: "add-it",
width: 50,
label: "Add",
content: "Add",
onClick: function() {
addItem(require("sdk/tabs").activeTab.title);
}
});
widgets.Widget({
id: "list-them",
width: 50,
label: "List",
content: "List",
onClick: function() {
getItems(listItems);
}
});
<api name="indexedDB">
@property {object}
@ -61,71 +169,6 @@ Defines a range of keys.
See the [IDBKeyRange documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBKeyRange).
</api>
<api name="IDBCursor">
@property {object}
For traversing or iterating records in a database.
See the [IDBCursor documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBCursor).
</api>
<api name="IDBTransaction">
@property {object}
Represents a database transaction.
See the [IDBTransaction documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBTransaction).
</api>
<api name="IDBOpenDBRequest">
@property {object}
Represents an asynchronous request to open a database.
See the [IDBOpenDBRequest documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBOpenDBRequest).
</api>
<api name="IDBVersionChangeEvent">
@property {object}
Event indicating that the database version has changed.
See the [IDBVersionChangeEvent documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBVersionChangeEvent).
</api>
<api name="IDBDatabase">
@property {object}
Represents a connection to a database.
See the [IDBDatabase documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBDatabase).
</api>
<api name="IDBFactory">
@property {object}
Enables you to create, open, and delete databases.
See the [IDBFactory documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBFactory).
</api>
<api name="IDBIndex">
@property {object}
Provides access to a database index.
See the [IDBIndex documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBIndex).
</api>
<api name="IDBObjectStore">
@property {object}
Represents an object store in a database.
See the [IDBObjectStore documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBObjectStore).
</api>
<api name="IDBRequest">
@property {object}
Provides access to the results of asynchronous requests to databases
and database objects.
See the [IDBRequest documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBRequest).
</api>
<api name="DOMException">
@property {object}

View File

@ -0,0 +1,450 @@
<!-- 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/. -->
The `places/bookmarks` module provides functions for creating, modifying and searching bookmark items. It exports:
* three constructors: [Bookmark](modules/sdk/places/bookmarks.html#Bookmark), [Group](modules/sdk/places/bookmarks.html#Group), and [Separator](modules/sdk/places/bookmarks.html#Separator), corresponding to the types of objects, referred to as **bookmark items**, in the Bookmarks database in Firefox
* two additional functions, [`save()`](modules/sdk/places/bookmarks.html#save(bookmarkItems%2C%20options)) to create, update, and remove bookmark items, and [`search()`](modules/sdk/places/bookmarks.html#search(queries%2C%20options)) to retrieve the bookmark items that match a particular set of criteria.
`save()` and `search()` are both asynchronous functions: they synchronously return a [`PlacesEmitter`](modules/sdk/places/bookmarks.html#PlacesEmitter) object, which then asynchronously emits events as the operation progresses and completes.
Each retrieved bookmark item represents only a snapshot of state at a specific time. The module does not automatically sync up a `Bookmark` instance with ongoing changes to that item in the database from the same add-on, other add-ons, or the user.
## Examples
### Creating a new bookmark
let { Bookmark, save } = require("sdk/places/bookmarks");
// Create a new bookmark instance, unsaved
let bookmark = Bookmark({ title: "Mozilla", url: "http://mozila.org" });
// Attempt to save the bookmark instance to the Bookmarks database
// and store the emitter
let emitter = save(bookmark);
// Listen for events
emitter.on("data", function (saved, inputItem) {
// on a "data" event, an item has been updated, passing in the
// latest snapshot from the server as `saved` (with properties
// such as `updated` and `id`), as well as the initial input
// item as `inputItem`
console.log(saved.title === inputItem.title); // true
console.log(saved !== inputItem); // true
console.log(inputItem === bookmark); // true
}).on("end", function (savedItems, inputItems) {
// Similar to "data" events, except "end" is an aggregate of
// all progress events, with ordered arrays as `savedItems`
// and `inputItems`
});
### Creating several bookmarks with a new group
let { Bookmark, Group, save } = require("sdk/places/bookmarks");
let group = Group({ title: "Guitars" });
let bookmarks = [
Bookmark({ title: "Ran", url: "http://ranguitars.com", group: group }),
Bookmark({ title: "Ibanez", url: "http://ibanez.com", group: group }),
Bookmark({ title: "ESP", url: "http://espguitars.com", group: group })
];
// Save `bookmarks` array -- notice we don't have `group` in the array,
// although it needs to be saved since all bookmarks are children
// of `group`. This will be saved implicitly.
save(bookmarks).on("data", function (saved, input) {
// A data event is called once for each item saved, as well
// as implicit items, like `group`
console.log(input === group || ~bookmarks.indexOf(input)); // true
}).on("end", function (saves, inputs) {
// like the previous example, the "end" event returns an
// array of all of our updated saves. Only explicitly saved
// items are returned in this array -- the `group` won't be
// present here.
console.log(saves[0].title); // "Ran"
console.log(saves[2].group.title); // "Guitars"
});
### Searching for bookmarks
Bookmarks can be queried with the [`search()`](modules/sdk/places/bookmarks.html#search(queries%2C%20options)) function, which accepts a query object or an array of query objects, as well as a query options object. Query properties are AND'd together within a single query object, but are OR'd together across multiple query objects.
let { search, UNSORTED } = require("sdk/places/bookmarks");
// Simple query with one object
search(
{ query: "firefox" },
{ sort: "title" }
).on(end, function (results) {
// results matching any bookmark that has "firefox"
// in its URL, title or tag, sorted by title
});
// Multiple queries are OR'd together
search(
[{ query: "firefox" }, { group: UNSORTED, tags: ["mozilla"] }],
{ sort: "title" }
).on("end", function (results) {
// Our first query is the same as the simple query above;
// all of those results are also returned here. Since multiple
// queries are OR'd together, we also get bookmarks that
// match the second query. The second query's properties
// are AND'd together, so results that are in the platform's unsorted
// bookmarks folder, AND are also tagged with 'mozilla', get returned
// as well in this query
});
<api name="Bookmark">
@class
<api name="Bookmark">
@constructor
Creates an unsaved bookmark instance.
@param options {object}
Options for the bookmark, with the following parameters:
@prop title {string}
The title for the bookmark. Required.
@prop url {string}
The URL for the bookmark. Required.
@prop [group] {Group}
The parent group that the bookmark lives under. Defaults to the [Bookmarks.UNSORTED](modules/sdk/places/bookmarks.html#UNSORTED) group.
@prop [index] {number}
The index of the bookmark within its group. Last item within the group by default.
@prop [tags] {set}
A set of tags to be applied to the bookmark.
</api>
<api name="title">
@property {string}
The bookmark's title.
</api>
<api name="url">
@property {string}
The bookmark's URL.
</api>
<api name="group">
@property {Group}
The group instance that the bookmark lives under.
</api>
<api name="index">
@property {number}
The index of the bookmark within its group.
</api>
<api name="updated">
@property {number}
A Unix timestamp indicating when the bookmark was last updated on the platform.
</api>
<api name="tags">
@property {set}
A [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) of tags that the bookmark is tagged with.
</api>
</api>
<api name="Group">
@class
<api name="Group">
@constructor
Creates an unsaved bookmark group instance.
@param options {object}
Options for the bookmark group, with the following parameters:
@prop title {string}
The title for the group. Required.
@prop [group] {Group}
The parent group that the bookmark group lives under. Defaults to the [Bookmarks.UNSORTED](modules/sdk/places/bookmarks.html#UNSORTED) group.
@prop [index] {number}
The index of the bookmark group within its parent group. Last item within the group by default.
</api>
<api name="title">
@property {string}
The bookmark group's title.
</api>
<api name="group">
@property {Group}
The group instance that the bookmark group lives under.
</api>
<api name="index">
@property {number}
The index of the bookmark group within its group.
</api>
<api name="updated">
@property {number}
A Unix timestamp indicating when the bookmark was last updated on the platform.
</api>
</api>
<api name="Separator">
@class
<api name="Separator">
@constructor
Creates an unsaved bookmark separator instance.
@param options {object}
Options for the bookmark group, with the following parameters:
@prop [group] {Group}
The parent group that the bookmark group lives under. Defaults to the [Bookmarks.UNSORTED](modules/sdk/places/bookmarks.html#UNSORTED) group.
@prop [index] {number}
The index of the bookmark group within its parent group. Last item within the group by default.
</api>
<api name="group">
@property {Group}
The group instance that the bookmark group lives under.
</api>
<api name="index">
@property {number}
The index of the bookmark group within its group.
</api>
<api name="updated">
@property {number}
A Unix timestamp indicating when the bookmark was last updated on the platform.
</api>
</api>
<api name="save">
@function
Creating, saving, and deleting bookmarks are all done with the `save()` function. This function takes in any of:
* a bookmark item (Bookmark, Group, Separator)
* a duck-typed object (the relative properties for a bookmark item, in addition to a `type` property of `'bookmark'`, `'group'`, or `'separator'`)
* an array of bookmark items.
All of the items passed in are pushed to the platform and are either created, updated or deleted.
* adding items: if passing in freshly instantiated bookmark items or a duck-typed object, the item is created on the platform.
* updating items: for an item referenced from a previous `save()` or from the result of a `search()` query, changing the properties and calling `save()` will update the item on the server.
* deleting items: to delete a bookmark item, pass in a bookmark item with a property `remove` set to `true`.
The function returns a [`PlacesEmitter`](modules/sdk/places/bookmarks.html#PlacesEmitter) that emits a `data` event for each item as it is saved, and an `end` event when all items have been saved.
let { Bookmark, Group } = require("sdk/places/bookmarks");
let myGroup = Group({ title: "My Group" });
let myBookmark = Bookmark({
title: "Moz",
url: "http://mozilla.com",
group: myGroup
});
save(myBookmark).on("data", function (item, inputItem) {
// The `data` event returns the latest snapshot from the
// host, so this is a new instance of the bookmark item,
// where `item !== myBookmark`. To match it with the input item,
// use the second argument, so `inputItem === myBookmark`
// All explicitly saved items have data events called, as
// well as implicitly saved items. In this case,
// `myGroup` has to be saved before `myBookmark`, since
// `myBookmark` is a child of `myGroup`. `myGroup` will
// also have a `data` event called for it.
}).on("end", function (items, inputItems) {
// The `end` event returns all items that are explicitly
// saved. So `myGroup` will not be in this array,
// but `myBookmark` will be.
// `inputItems` matches the initial input as an array,
// so `inputItems[0] === myBookmark`
});
// Saving multiple bookmarks, as duck-types in this case
let bookmarks = [
{ title: "mozilla", url: "http://mozilla.org", type: "bookmark" },
{ title: "firefox", url: "http://firefox.com", type: "bookmark" },
{ title: "twitter", url: "http://twitter.com", type: "bookmark" }
];
save(bookmarks).on("data", function (item, inputItem) {
// Each item in `bookmarks` has its own `data` event
}).on("end", function (results, inputResults) {
// `results` is an array of items saved in the same order
// as they were passed in.
});
@param bookmarkItems {bookmark|group|separator|array}
A bookmark item ([Bookmark](modules/sdk/places/bookmarks.html#Bookmark), [Group](modules/sdk/places/bookmarks.html#Group), [Separator](modules/sdk/places/bookmarks.html#Separator)), or an array of bookmark items to be saved.
@param [options] {object}
An optional options object that takes the following properties:
@prop [resolve] {function}
A resolution function that is invoked during an attempt to save
a bookmark item that is not derived from the latest state from
the platform. Invoked with two arguments, `mine` and `platform`, where
`mine` is the item that is being saved, and `platform` is the
current state of the item on the item. The object returned from
this function is what is saved on the platform. By default, all changes
on an outdated bookmark item overwrite the platform's bookmark item.
@returns {PlacesEmitter}
Returns a [PlacesEmitter](modules/sdk/places/bookmarks.html#PlacesEmitter).
</api>
<api name="remove">
@function
A helper function that takes in a bookmark item, or an `Array` of several bookmark items, and sets each item's `remove` property to true. This does not remove the bookmark item from the database: it must be subsequently saved.
let { search, save, remove } = require("sdk/places/bookmarks");
search({ tags: ["php"] }).on("end", function (results) {
// The search returns us all bookmark items that are
// tagged with `"php"`.
// We then pass `results` into the remove function to mark
// all items to be removed, which returns the new modified `Array`
// of items, which is passed into save.
save(remove(results)).on("end", function (results) {
// items tagged with `"php"` are now removed!
});
})
@param items {Bookmark|Group|Separator|array}
A bookmark item, or `Array` of bookmark items to be transformed to set their `remove` property to `true`.
@returns {array}
An array of the transformed bookmark items.
</api>
<api name="search">
@function
Queries can be performed on bookmark items by passing in one or more query objects, each of which is given one or more properties.
Within each query object, the properties are AND'd together: so only objects matching all properties are retrieved. Across query objects, the results are OR'd together, meaning that if an item matches any of the query objects, it will be retrieved.
For example, suppose we called `search()` with two query objects:
<pre>[{ url: "mozilla.org", tags: ["mobile"]},
{ tags: ["firefox-os"]}]</pre>
This will return:
* all bookmark items from mozilla.org that are also tagged "mobile"
* all bookmark items that are tagged "firefox-os"
An `options` object may be used to determine overall settings such as sort order and how many objects should be returned.
@param queries {object|array}
An `Object` representing a query, or an `Array` of `Objects` representing queries. Each query object can take several properties, which are queried against the bookmarks database. Each property is AND'd together, meaning that bookmarks must match each property within a query object. Multiple query objects are then OR'd together.
@prop [group] {Group}
Group instance that should be owners of the returned children bookmarks. If no `group` specified, all bookmarks are under the search space.
@prop [tags] {set|array}
Bookmarks with corresponding tags. These are AND'd together.
@prop [url] {string}
A string that matches bookmarks' URL. The following patterns are accepted:
`'*.mozilla.com'`: matches any URL with 'mozilla.com' as the host, accepting any subhost.
`'mozilla.com'`: matches any URL with 'mozilla.com' as the host.
`'http://mozilla.com'`: matches 'http://mozilla.com' exactly.
`'http://mozilla.com/*'`: matches any URL that starts with 'http://mozilla.com/'.
@prop [query] {string}
A string that matches bookmarks' URL, title and tags.
@param [options] {object}
An `Object` with options for the search query.
@prop [count] {number}
The number of bookmark items to return. If left undefined, no limit is set.
@prop [sort] {string}
A string specifying how the results should be sorted. Possible options are `'title'`, `'date'`, `'url'`, `'visitCount'`, `'dateAdded'` and `'lastModified'`.
@prop [descending] {boolean}
A boolean specifying whether the results should be in descending order. By default, results are in ascending order.
</api>
<api name="PlacesEmitter">
@class
The `PlacesEmitter` is not exported from the module, but returned from the `save` and `search` functions. The `PlacesEmitter` inherits from [`event/target`](modules/sdk/event/target.html), and emits `data`, `error`, and `end`.
`data` events are emitted for every individual operation (such as: each item saved, or each item found by a search query), whereas `end` events are emitted as the aggregate of an operation, passing an array of objects into the handler.
<api name="data">
@event
The `data` event is emitted when a bookmark item that was passed into the `save` method has been saved to the platform. This includes implicit saves that are dependencies of the explicit items saved. For example, when creating a new bookmark group with two bookmark items as its children, and explicitly saving the two bookmark children, the unsaved parent group will also emit a `data` event.
let { Bookmark, Group, save } = require("sdk/places/bookmarks");
let group = Group({ title: "my group" });
let bookmarks = [
Bookmark({ title: "mozilla", url: "http://mozilla.com", group: group }),
Bookmark({ title: "w3", url: "http://w3.org", group: group })
];
save(bookmarks).on("data", function (item) {
// This function will be called three times:
// once for each bookmark saved
// once for the new group specified implicitly
// as the parent of the two items
});
The `data` event is also called for `search` requests, with each result being passed individually into its own `data` event.
let { search } = require("sdk/places/bookmarks");
search({ query: "firefox" }).on("data", function (item) {
// each bookmark item that matches the query will
// be called in this function
});
@argument {Bookmark|Group|Separator}
For the `save` function, this is the saved, latest snapshot of the bookmark item. For `search`, this is a snapshot of a bookmark returned from the search query.
@argument {Bookmark|Group|Separator|object}
Only in `save` data events. The initial instance of the item that was used for the save request.
</api>
<api name="error">
@event
The `error` event is emitted whenever a bookmark item's save could not be completed.
@argument {string}
A string indicating the error that occurred.
@argument {Bookmark|Group|Separator|object}
Only in `save` error events. The initial instance of the item that was used for the save request.
</api>
<api name="end">
@event
The `end` event is called when all bookmark items and dependencies
have been saved, or an aggregate of all items returned from a search query.
@argument {array}
The array is an ordered list of the input bookmark items, replaced
with their updated, latest snapshot instances (the first argument
in the `data` handler), or in the case of an error, the initial instance
of the item that was used for the save request
(the second argument in the `data` or `error` handler).
</api>
</api>
<api name="MENU">
@property {group}
This is a constant, default [`Group`](modules/sdk/places/bookmarks.html#Group) on the Firefox platform, the **Bookmarks Menu**. It can be used in queries or specifying the parent of a bookmark item, but it cannot be modified.
</api>
<api name="TOOLBAR">
@property {group}
This is a constant, default [`Group`](modules/sdk/places/bookmarks.html#Group) on the Firefox platform, the **Bookmarks Toolbar**. It can be used in queries or specifying the parent of a bookmark item, but it cannot be modified.
</api>
<api name="UNSORTED">
@property {group}
This is a constant, default [`Group`](modules/sdk/places/bookmarks.html#Group) on the Firefox platform, the **Unsorted Bookmarks** group. It can be used in queries or specifying the parent of a bookmark item, but it cannot be modified.
</api>

View File

@ -0,0 +1,110 @@
<!-- 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/. -->
The `places/history` module provides a single function, [`search()`](modules/sdk/places/history.html#search(queries%2C%20options)), for querying the user's browsing history.
It synchronously returns a [`PlacesEmitter`](modules/sdk/places/history.html#PlacesEmitter) object which then asynchronously emits [`data`](modules/sdk/places/history.html#data) and [`end`](modules/sdk/places/history.html#end) or [`error`](modules/sdk/places/history.html#error) events that contain information about the state of the operation.
## Example
let { search } = require("sdk/places/history");
// Simple query
search(
{ url: "https://developers.mozilla.org/*" },
{ sort: "visitCount" }
).on("end", function (results) {
// results is an array of objects containing
// data about visits to any site on developers.mozilla.org
// ordered by visit count
});
// Complex query
// The query objects are OR'd together
// Let's say we want to retrieve all visits from before a week ago
// with the query of 'ruby', but from last week onwards, we want
// all results with 'javascript' in the URL or title.
// We'd compose the query with the following options
let lastWeek = Date.now - (1000*60*60*24*7);
search(
// First query looks for all entries before last week with 'ruby'
[{ query: "ruby", to: lastWeek },
// Second query searches all entries after last week with 'javascript'
{ query: "javascript", from: lastWeek }],
// We want to order chronologically by visit date
{ sort: "date" }
).on("end", function (results) {
// results is an array of objects containing visit data,
// sorted by visit date, with all entries from more than a week ago
// that contain 'ruby', *in addition to* entries from this last week
// that contain 'javascript'
});
<api name="search">
@function
Queries can be performed on history entries by passing in one or more query options. Each query option can take several properties, which are **AND**'d together to make one complete query. For additional queries within the query, passing more query options in will **OR** the total results. An `options` object may be specified to determine overall settings, like sorting and how many objects should be returned.
@param queries {object|array}
An `Object` representing a query, or an `Array` of `Objects` representing queries. Each query object can take several properties, which are queried against the history database. Each property is **AND**'d together, meaning that bookmarks must match each property within a query object. Multiple query objects are then **OR**'d together.
@prop [url] {string}
A string that matches bookmarks' URL. The following patterns are accepted:
`'*.mozilla.com'`: matches any URL with 'mozilla.com' as the host, accepting any subhost.
`'mozilla.com'`: matches any URL with 'mozilla.com' as the host.
`'http://mozilla.com'`: matches 'http://mozilla.com' directlry.
`'http://mozilla.com/*'`: matches any URL that starts with 'http://mozilla.com/'.
@prop [query] {string}
A string that matches bookmarks' URL, or title.
@prop [from] {number|date}
Time relative from the [Unix epoch](http://en.wikipedia.org/wiki/Unix_time) that history results should be limited to occuring after. Can accept a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object, or milliseconds from the epoch. Default is to return all items since the epoch (all time).
@prop [to] {number|date}
Time relative from the [Unix epoch](http://en.wikipedia.org/wiki/Unix_time) that history results should be limited to occuring before. Can accept a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object, or milliseconds from the epoch. Default is the current time.
@param [options] {object}
An `Object` with options for the search query.
@prop [count] {number}
The number of bookmark items to return. If left undefined, no limit is set.
@prop [sort] {string}
A string specifying how the results should be sorted. Possible options are `'title'`, `'date'`, `'url'`, `'visitCount'`, `'keyword'`, `'dateAdded'` and `'lastModified'`.
@prop [descending] {boolean}
A boolean specifying whether the results should be in descending order. By default, results are in ascending order.
</api>
<api name="PlacesEmitter">
@class
The `PlacesEmitter` is not exposed in the module, but returned from the `search` functions. The `PlacesEmitter` inherits from [`event/target`](modules/sdk/event/target.html), and emits `data`, `error`, and `end`. `data` events are emitted for every individual search result found, whereas `end` events are emitted as an aggregate of an entire search, passing in an array of all results into the handler.
<api name="data">
@event
The `data` event is emitted for every item returned from a search.
@argument {Object}
This is an object representing a history entry. Contains `url`, `time`, `accessCount` and `title` of the entry.
</api>
<api name="error">
@event
The `error` event is emitted whenever a search could not be completed.
@argument {string}
A string indicating the error that occurred.
</api>
<api name="end">
@event
The `end` event is called when all search results have returned.
@argument {array}
The value passed into the handler is an array of all entries found in the
history search. Each entry is an object containing the properties
`url`, `time`, `accessCount` and `title`.
</api>
</api>

View File

@ -2,7 +2,7 @@
- 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/. -->
The `request` module lets you make simple yet powerful network requests.
The `request` module lets you make simple yet powerful network requests. For more advanced usage, check out the [net/xhr](modules/sdk/net/xhr.html) module, based on the browser's [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) object.
<api name="Request">
@class

View File

@ -100,6 +100,23 @@ These are attributes that all settings *may* have:
this may be an integer, string, or boolean value.</td>
</tr>
<tr>
<td><code>hidden</code></td>
<td><p>A boolean value which, if present and set to <code>true</code>,
means that the setting won't appear in the Add-ons Manager interface,
so users of your add-on won't be able to see or alter it.</p>
<pre>
{
"name": "myHiddenInteger",
"type": "integer",
"title": "How Many?",
"hidden": true
}</pre>
<p>Your add-on's code will still be able to access and modify it,
just like any other preference you define.</p>
</td>
</tr>
</table>
### Setting-Specific Attributes ###

View File

@ -369,7 +369,7 @@ const WorkerSandbox = EventEmitter.compose({
/**
* Message-passing facility for communication between code running
* in the content and add-on process.
* @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/content/worker
* @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
*/
const Worker = EventEmitter.compose({
on: Trait.required,

View File

@ -5,12 +5,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "deprecated"
};
const memory = require("./memory");
const { merge } = require("../util/object");
const { union } = require("../util/array");
const { isNil } = require("../lang/type");
// The possible return values of getTypeOf.
const VALID_TYPES = [
"array",
@ -23,6 +27,8 @@ const VALID_TYPES = [
"undefined",
];
const { isArray } = Array;
/**
* Returns a function C that creates instances of privateCtor. C may be called
* with or without the new keyword. The prototype of each instance returned
@ -86,6 +92,7 @@ exports.validateOptions = function validateOptions(options, requirements) {
let validatedOptions = {};
for (let key in requirements) {
let isOptional = false;
let mapThrew = false;
let req = requirements[key];
let [optsVal, keyInOpts] = (key in options) ?
@ -103,17 +110,27 @@ exports.validateOptions = function validateOptions(options, requirements) {
}
}
if (req.is) {
// Sanity check the caller's type names.
req.is.forEach(function (typ) {
if (VALID_TYPES.indexOf(typ) < 0) {
let msg = 'Internal error: invalid requirement type "' + typ + '".';
throw new Error(msg);
}
});
if (req.is.indexOf(getTypeOf(optsVal)) < 0)
throw new RequirementError(key, req);
let types = req.is;
if (!isArray(types) && isArray(types.is))
types = types.is;
if (isArray(types)) {
isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v));
// Sanity check the caller's type names.
types.forEach(function (typ) {
if (VALID_TYPES.indexOf(typ) < 0) {
let msg = 'Internal error: invalid requirement type "' + typ + '".';
throw new Error(msg);
}
});
if (types.indexOf(getTypeOf(optsVal)) < 0)
throw new RequirementError(key, req);
}
}
if (req.ok && !req.ok(optsVal))
if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal)))
throw new RequirementError(key, req);
if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined))
@ -142,7 +159,7 @@ let getTypeOf = exports.getTypeOf = function getTypeOf(val) {
if (typ === "object") {
if (!val)
return "null";
if (Array.isArray(val))
if (isArray(val))
return "array";
}
return typ;
@ -164,3 +181,38 @@ function RequirementError(key, requirement) {
this.message = msg;
}
RequirementError.prototype = Object.create(Error.prototype);
let string = { is: ['string', 'undefined', 'null'] };
exports.string = string;
let number = { is: ['number', 'undefined', 'null'] };
exports.number = number;
let boolean = { is: ['boolean', 'undefined', 'null'] };
exports.boolean = boolean;
let object = { is: ['object', 'undefined', 'null'] };
exports.object = object;
let isTruthyType = type => !(type === 'undefined' || type === 'null');
let findTypes = v => { while (!isArray(v) && v.is) v = v.is; return v };
function required(req) {
let types = (findTypes(req) || VALID_TYPES).filter(isTruthyType);
return merge({}, req, {is: types});
}
exports.required = required;
function optional(req) {
req = merge({is: []}, req);
req.is = findTypes(req).filter(isTruthyType).concat('undefined', 'null');
return req;
}
exports.optional = optional;
function either(...types) {
return union.apply(null, types.map(findTypes));
}
exports.either = either;

View File

@ -4,7 +4,7 @@
"use strict";
module.metadata = {
"stability": "unstable"
"stability": "stable"
};
const { deprecateFunction } = require("../util/deprecate");
@ -33,4 +33,4 @@ function forceAllowThirdPartyCookie(xhr) {
exports.forceAllowThirdPartyCookie = forceAllowThirdPartyCookie;
// No need to handle add-on unloads as addon/window is closed at unload
// and it will take down all the associated requests.
// and it will take down all the associated requests.

View File

@ -0,0 +1,121 @@
/* 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';
module.metadata = {
'stability': 'experimental',
'engines': {
'Firefox': '*'
}
};
const { Cc, Ci } = require('chrome');
const { Unknown } = require('../platform/xpcom');
const { Class } = require('../core/heritage');
const { merge } = require('../util/object');
const bookmarkService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
.getService(Ci.nsINavBookmarksService);
const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
.getService(Ci.nsINavHistoryService);
const { mapBookmarkItemType } = require('./utils');
const { EventTarget } = require('../event/target');
const { emit } = require('../event/core');
const emitter = EventTarget();
let HISTORY_ARGS = {
onBeginUpdateBatch: [],
onEndUpdateBatch: [],
onClearHistory: [],
onDeleteURI: ['url'],
onDeleteVisits: ['url', 'visitTime'],
onPageChanged: ['url', 'property', 'value'],
onTitleChanged: ['url', 'title'],
onVisit: [
'url', 'visitId', 'time', 'sessionId', 'referringId', 'transitionType'
]
};
let HISTORY_EVENTS = {
onBeginUpdateBatch: 'history-start-batch',
onEndUpdateBatch: 'history-end-batch',
onClearHistory: 'history-start-clear',
onDeleteURI: 'history-delete-url',
onDeleteVisits: 'history-delete-visits',
onPageChanged: 'history-page-changed',
onTitleChanged: 'history-title-changed',
onVisit: 'history-visit'
};
let BOOKMARK_ARGS = {
onItemAdded: [
'id', 'parentId', 'index', 'type', 'url', 'title', 'dateAdded'
],
onItemChanged: [
'id', 'property', null, 'value', 'lastModified', 'type', 'parentId'
],
onItemMoved: [
'id', 'previousParentId', 'previousIndex', 'currentParentId',
'currentIndex', 'type'
],
onItemRemoved: ['id', 'parentId', 'index', 'type', 'url'],
onItemVisited: ['id', 'visitId', 'time', 'transitionType', 'url', 'parentId']
};
let BOOKMARK_EVENTS = {
onItemAdded: 'bookmark-item-added',
onItemChanged: 'bookmark-item-changed',
onItemMoved: 'bookmark-item-moved',
onItemRemoved: 'bookmark-item-removed',
onItemVisited: 'bookmark-item-visited',
};
function createHandler (type, propNames) {
propNames = propNames || [];
return function (...args) {
let data = propNames.reduce((acc, prop, i) => {
if (prop)
acc[prop] = formatValue(prop, args[i]);
return acc;
}, {});
emit(emitter, 'data', {
type: type,
data: data
});
};
}
/*
* Creates an observer, creating handlers based off of
* the `events` names, and ordering arguments from `propNames` hash
*/
function createObserverInstance (events, propNames) {
let definition = Object.keys(events).reduce((prototype, eventName) => {
prototype[eventName] = createHandler(events[eventName], propNames[eventName]);
return prototype;
}, {});
return Class(merge(definition, { extends: Unknown }))();
}
/*
* Formats `data` based off of the value of `type`
*/
function formatValue (type, data) {
if (type === 'type')
return mapBookmarkItemType(data);
if (type === 'url' && data)
return data.spec;
return data;
}
let historyObserver = createObserverInstance(HISTORY_EVENTS, HISTORY_ARGS);
historyService.addObserver(historyObserver, false);
let bookmarkObserver = createObserverInstance(BOOKMARK_EVENTS, BOOKMARK_ARGS);
bookmarkService.addObserver(bookmarkObserver, false);
exports.events = emitter;

View File

@ -104,17 +104,22 @@ function saveBookmarkItem (data) {
let group = bmsrv.getFolderIdForItem(id);
let index = bmsrv.getItemIndex(id);
let type = bmsrv.getItemType(id);
let title = typeMap(type) !== 'separator' ?
bmsrv.getItemTitle(id) :
undefined;
let url = typeMap(type) === 'bookmark' ?
bmsrv.getBookmarkURI(id).spec :
undefined;
if (data.url) {
if (url != data.url)
bmsrv.changeBookmarkURI(id, newURI(data.url));
}
else if (typeMap(type) === 'bookmark')
data.url = bmsrv.getBookmarkURI(id).spec;
data.url = url;
if (data.title)
if (title != data.title)
bmsrv.setItemTitle(id, data.title);
else if (typeMap(type) !== 'separator')
data.title = bmsrv.getItemTitle(id);
data.title = title;
if (data.group && data.group !== group)
bmsrv.moveItem(id, data.group, data.index || -1);
@ -123,7 +128,7 @@ function saveBookmarkItem (data) {
// so we don't have to manage the indicies of the siblings
bmsrv.moveItem(id, group, data.index);
} else if (data.index == null)
data.index = bmsrv.getItemIndex(id);
data.index = index;
data.updated = bmsrv.getItemLastModified(data.id);

View File

@ -235,3 +235,16 @@ function createQueryOptions (type, options) {
}
exports.createQueryOptions = createQueryOptions;
function mapBookmarkItemType (type) {
if (typeof type === 'number') {
if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark';
if (bmsrv.TYPE_FOLDER === type) return 'group';
if (bmsrv.TYPE_SEPARATOR === type) return 'separator';
} else {
if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK;
if ('group' === type) return bmsrv.TYPE_FOLDER;
if ('separator' === type) return bmsrv.TYPE_SEPARATOR;
}
}
exports.mapBookmarkItemType = mapBookmarkItemType;

View File

@ -26,7 +26,14 @@ exports.EVENTS = EVENTS;
Object.keys(EVENTS).forEach(function(name) {
EVENTS[name] = {
name: name,
listener: ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1),
listener: createListenerName(name),
dom: EVENTS[name]
}
});
function createListenerName (name) {
if (name === 'pageshow')
return 'onPageShow';
else
return ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1);
}

View File

@ -33,6 +33,9 @@ const Tab = Class({
// TabReady
let onReady = tabInternals.onReady = onTabReady.bind(this);
tab.browser.addEventListener(EVENTS.ready.dom, onReady, false);
let onPageShow = tabInternals.onPageShow = onTabPageShow.bind(this);
tab.browser.addEventListener(EVENTS.pageshow.dom, onPageShow, false);
// TabClose
let onClose = tabInternals.onClose = onTabClose.bind(this);
@ -180,8 +183,10 @@ function cleanupTab(tab) {
if (tabInternals.tab.browser) {
tabInternals.tab.browser.removeEventListener(EVENTS.ready.dom, tabInternals.onReady, false);
tabInternals.tab.browser.removeEventListener(EVENTS.pageshow.dom, tabInternals.onPageShow, false);
}
tabInternals.onReady = null;
tabInternals.onPageShow = null;
tabInternals.window.BrowserApp.deck.removeEventListener(EVENTS.close.dom, tabInternals.onClose, false);
tabInternals.onClose = null;
rawTabNS(tabInternals.tab).tab = null;
@ -198,6 +203,12 @@ function onTabReady(event) {
}
}
function onTabPageShow(event) {
let win = event.target.defaultView;
if (win === win.top)
emit(this, 'pageshow', this, event.persisted);
}
// TabClose
function onTabClose(event) {
let rawTab = getTabForBrowser(event.target);

View File

@ -1,7 +1,6 @@
/* 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";
module.metadata = {
@ -277,3 +276,16 @@ let isValidURI = exports.isValidURI = function (uri) {
}
return true;
}
function isLocalURL(url) {
if (String.indexOf(url, './') === 0)
return true;
try {
return ['resource', 'data', 'chrome'].indexOf(URL(url).scheme) > -1;
}
catch(e) {}
return false;
}
exports.isLocalURL = isLocalURL;

View File

@ -72,12 +72,22 @@ exports.remove = function remove(array, element) {
* Source array.
* @returns {Array}
*/
exports.unique = function unique(array) {
return array.reduce(function(values, element) {
add(values, element);
return values;
function unique(array) {
return array.reduce(function(result, item) {
add(result, item);
return result;
}, []);
};
exports.unique = unique;
/**
* Produce an array that contains the union: each distinct element from all
* of the passed-in arrays.
*/
function union() {
return unique(Array.concat.apply(null, arguments));
};
exports.union = union;
exports.flatten = function flatten(array){
var flat = [];

View File

@ -77,6 +77,9 @@ const Tabs = Class({
if (options.onReady)
tab.on('ready', options.onReady);
if (options.onPageShow)
tab.on('pageshow', options.onPageShow);
if (options.onActivate)
tab.on('activate', options.onActivate);
@ -131,9 +134,12 @@ function onTabOpen(event) {
tab.on('ready', function() emit(gTabs, 'ready', tab));
tab.once('close', onTabClose);
tab.on('pageshow', function(_tab, persisted)
emit(gTabs, 'pageshow', tab, persisted));
emit(tab, 'open', tab);
emit(gTabs, 'open', tab);
};
}
// TabSelect
function onTabSelect(event) {
@ -153,10 +159,10 @@ function onTabSelect(event) {
emit(t, 'deactivate', t);
emit(gTabs, 'deactivate', t);
}
};
}
// TabClose
function onTabClose(tab) {
removeTab(tab);
emit(gTabs, EVENTS.close.name, tab);
};
}

View File

@ -406,7 +406,8 @@ class Runner(object):
def find_binary(self):
"""Finds the binary for self.names if one was not provided."""
binary = None
if sys.platform in ('linux2', 'sunos5', 'solaris'):
if sys.platform in ('linux2', 'sunos5', 'solaris') \
or sys.platform.startswith('freebsd'):
for name in reversed(self.names):
binary = findInPath(name)
elif os.name == 'nt' or sys.platform == 'cygwin':
@ -578,7 +579,8 @@ class FirefoxRunner(Runner):
def names(self):
if sys.platform == 'darwin':
return ['firefox', 'nightly', 'shiretoko']
if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
if sys.platform in ('linux2', 'sunos5', 'solaris') \
or sys.platform.startswith('freebsd'):
return ['firefox', 'mozilla-firefox', 'iceweasel']
if os.name == 'nt' or sys.platform == 'cygwin':
return ['firefox']

View File

@ -257,7 +257,8 @@ class Popen(subprocess.Popen):
self.kill(group)
else:
if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
if sys.platform in ('linux2', 'sunos5', 'solaris') \
or sys.platform.startswith('freebsd'):
def group_wait(timeout):
try:
os.waitpid(self.pid, 0)

View File

@ -10,15 +10,20 @@ const { isGlobalPBSupported } = require('sdk/private-browsing/utils');
merge(module.exports,
require('./test-tabs'),
require('./test-page-mod'),
require('./test-selection'),
require('./test-panel'),
require('./test-private-browsing'),
isGlobalPBSupported ? require('./test-global-private-browsing') : {}
);
// Doesn't make sense to test window-utils and windows on fennec,
// as there is only one window which is never private
if (!app.is('Fennec'))
merge(module.exports, require('./test-windows'));
// as there is only one window which is never private. Also ignore
// unsupported modules (panel, selection)
if (!app.is('Fennec')) {
merge(module.exports,
require('./test-selection'),
require('./test-panel'),
require('./test-window-tabs'),
require('./test-windows')
);
}
require('sdk/test/runner').runTestsFromModule(module);

View File

@ -1,12 +1,9 @@
'use strict';
const tabs = require('sdk/tabs');
const { is } = require('sdk/system/xul-app');
const { isPrivate } = require('sdk/private-browsing');
const pbUtils = require('sdk/private-browsing/utils');
const { getOwnerWindow } = require('sdk/private-browsing/window/utils');
const { promise: windowPromise, close, focus } = require('sdk/window/helpers');
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
exports.testPrivateTabsAreListed = function (assert, done) {
let originalTabCount = tabs.length;
@ -32,82 +29,5 @@ exports.testPrivateTabsAreListed = function (assert, done) {
tab.close(done);
}
});
}
};
exports.testOpenTabWithPrivateActiveWindowNoIsPrivateOption = function(assert, done) {
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
windowPromise(window, 'load').then(focus).then(function (window) {
assert.ok(isPrivate(window), 'new window is private');
tabs.open({
url: 'about:blank',
onOpen: function(tab) {
assert.ok(isPrivate(tab), 'new tab is private');
assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private');
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same');
close(window).then(done, assert.fail);
}
})
}, assert.fail).then(null, assert.fail);
}
exports.testOpenTabWithNonPrivateActiveWindowNoIsPrivateOption = function(assert, done) {
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false });
windowPromise(window, 'load').then(focus).then(function (window) {
assert.equal(isPrivate(window), false, 'new window is not private');
tabs.open({
url: 'about:blank',
onOpen: function(tab) {
assert.equal(isPrivate(tab), false, 'new tab is not private');
assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private');
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same');
close(window).then(done, assert.fail);
}
})
}, assert.fail).then(null, assert.fail);
}
exports.testOpenTabWithPrivateActiveWindowWithIsPrivateOptionTrue = function(assert, done) {
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
windowPromise(window, 'load').then(focus).then(function (window) {
assert.ok(isPrivate(window), 'new window is private');
tabs.open({
url: 'about:blank',
isPrivate: true,
onOpen: function(tab) {
assert.ok(isPrivate(tab), 'new tab is private');
assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private');
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same');
close(window).then(done, assert.fail);
}
})
}, assert.fail).then(null, assert.fail);
}
exports.testOpenTabWithNonPrivateActiveWindowWithIsPrivateOptionFalse = function(assert, done) {
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false });
windowPromise(window, 'load').then(focus).then(function (window) {
assert.equal(isPrivate(window), false, 'new window is not private');
tabs.open({
url: 'about:blank',
isPrivate: false,
onOpen: function(tab) {
assert.equal(isPrivate(tab), false, 'new tab is not private');
assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private');
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same');
close(window).then(done, assert.fail);
}
})
}, assert.fail).then(null, assert.fail);
}

View File

@ -0,0 +1,85 @@
'use strict';
const tabs = require('sdk/tabs');
const { isPrivate } = require('sdk/private-browsing');
const { getOwnerWindow } = require('sdk/private-browsing/window/utils');
const { promise: windowPromise, close, focus } = require('sdk/window/helpers');
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
exports.testOpenTabWithPrivateActiveWindowNoIsPrivateOption = function(assert, done) {
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
windowPromise(window, 'load').then(focus).then(function (window) {
assert.ok(isPrivate(window), 'new window is private');
tabs.open({
url: 'about:blank',
onOpen: function(tab) {
assert.ok(isPrivate(tab), 'new tab is private');
assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private');
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same');
close(window).then(done, assert.fail);
}
})
}, assert.fail).then(null, assert.fail);
}
exports.testOpenTabWithNonPrivateActiveWindowNoIsPrivateOption = function(assert, done) {
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false });
windowPromise(window, 'load').then(focus).then(function (window) {
assert.equal(isPrivate(window), false, 'new window is not private');
tabs.open({
url: 'about:blank',
onOpen: function(tab) {
assert.equal(isPrivate(tab), false, 'new tab is not private');
assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private');
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same');
close(window).then(done, assert.fail);
}
})
}, assert.fail).then(null, assert.fail);
}
exports.testOpenTabWithPrivateActiveWindowWithIsPrivateOptionTrue = function(assert, done) {
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
windowPromise(window, 'load').then(focus).then(function (window) {
assert.ok(isPrivate(window), 'new window is private');
tabs.open({
url: 'about:blank',
isPrivate: true,
onOpen: function(tab) {
assert.ok(isPrivate(tab), 'new tab is private');
assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private');
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same');
close(window).then(done, assert.fail);
}
})
}, assert.fail).then(null, assert.fail);
}
exports.testOpenTabWithNonPrivateActiveWindowWithIsPrivateOptionFalse = function(assert, done) {
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false });
windowPromise(window, 'load').then(focus).then(function (window) {
assert.equal(isPrivate(window), false, 'new window is not private');
tabs.open({
url: 'about:blank',
isPrivate: false,
onOpen: function(tab) {
assert.equal(isPrivate(tab), false, 'new tab is not private');
assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private');
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same');
close(window).then(done, assert.fail);
}
})
}, assert.fail).then(null, assert.fail);
}

View File

@ -4,10 +4,14 @@
"use strict";
const { Panel } = require("sdk/panel")
const { data } = require("sdk/self")
const app = require("sdk/system/xul-app");
exports["test addon globa"] = app.is("Firefox") ? testAddonGlobal : unsupported;
function testAddonGlobal (assert, done) {
const { Panel } = require("sdk/panel")
const { data } = require("sdk/self")
exports["test addon global"] = function(assert, done) {
let panel = Panel({
contentURL: //"data:text/html,now?",
data.url("./index.html"),
@ -17,10 +21,14 @@ exports["test addon global"] = function(assert, done) {
done();
},
onError: function(error) {
asser.fail(Error("failed to recieve message"));
assert.fail(Error("failed to recieve message"));
done();
}
});
};
function unsupported (assert) {
assert.pass("privileged-panel unsupported on platform");
}
require("sdk/test/runner").runTestsFromModule(module);

View File

@ -13,42 +13,18 @@ const basePath = pathFor('ProfD');
const { atob } = Cu.import("resource://gre/modules/Services.jsm", {});
const historyService = Cc["@mozilla.org/browser/nav-history-service;1"]
.getService(Ci.nsINavHistoryService);
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
const ObserverShimMethods = ['onBeginUpdateBatch', 'onEndUpdateBatch',
'onVisit', 'onTitleChanged', 'onDeleteURI', 'onClearHistory',
'onPageChanged', 'onDeleteVisits'];
const { events } = require('sdk/places/events');
/*
* Shims NavHistoryObserver
*/
let noop = function () {}
let NavHistoryObserver = function () {};
ObserverShimMethods.forEach(function (method) {
NavHistoryObserver.prototype[method] = noop;
});
NavHistoryObserver.prototype.QueryInterface = XPCOMUtils.generateQI([
Ci.nsINavHistoryObserver
]);
/*
* Uses history observer to watch for an onPageChanged event,
* which detects when a favicon is updated in the registry.
*/
function onFaviconChange (uri, callback) {
let observer = Object.create(NavHistoryObserver.prototype, {
onPageChanged: {
value: function onPageChanged(aURI, aWhat, aValue, aGUID) {
if (aWhat !== Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON)
return;
if (aURI.spec !== uri)
return;
historyService.removeObserver(this);
callback(aValue);
}
}
});
historyService.addObserver(observer, false);
function onFaviconChange (url, callback) {
function handler ({data, type}) {
if (type !== 'history-page-changed' ||
data.url !== url ||
data.property !== Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON)
return;
events.off('data', handler);
callback(data.value);
}
events.on('data', handler);
}
exports.onFaviconChange = onFaviconChange;

View File

@ -106,6 +106,13 @@ function addVisits (urls) {
}
exports.addVisits = addVisits;
function removeVisits (urls) {
[].concat(urls).map(url => {
hsrv.removePage(newURI(url));
});
}
exports.removeVisits = removeVisits;
// Creates a mozIVisitInfo object
function createVisit (url) {
let place = {}

View File

@ -949,40 +949,6 @@ exports.testOnLoadEventWithImage = function(test) {
});
};
exports.testOnPageShowEvent = function (test) {
test.waitUntilDone();
let firstUrl = 'data:text/html;charset=utf-8,First';
let secondUrl = 'data:text/html;charset=utf-8,Second';
openBrowserWindow(function(window, browser) {
let counter = 0;
tabs.on('pageshow', function onPageShow(tab, persisted) {
counter++;
if (counter === 1) {
test.assert(!persisted, 'page should not be cached on initial load');
tab.url = secondUrl;
}
else if (counter === 2) {
test.assert(!persisted, 'second test page should not be cached either');
tab.attach({
contentScript: 'setTimeout(function () { window.history.back(); }, 0)'
});
}
else {
test.assert(persisted, 'when we get back to the fist page, it has to' +
'come from cache');
tabs.removeListener('pageshow', onPageShow);
closeBrowserWindow(window, function() test.done());
}
});
tabs.open({
url: firstUrl
});
});
};
exports.testFaviconGetterDeprecation = function (test) {
const { LoaderWithHookedConsole } = require("sdk/test/loader");
let { loader, messages } = LoaderWithHookedConsole(module);

View File

@ -6,92 +6,94 @@
const apiUtils = require("sdk/deprecated/api-utils");
exports.testPublicConstructor = function (test) {
exports.testPublicConstructor = function (assert) {
function PrivateCtor() {}
PrivateCtor.prototype = {};
let PublicCtor = apiUtils.publicConstructor(PrivateCtor);
test.assert(
assert.ok(
PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype),
"PrivateCtor.prototype should be prototype of PublicCtor.prototype"
);
function testObj(useNew) {
let obj = useNew ? new PublicCtor() : PublicCtor();
test.assert(obj instanceof PublicCtor,
assert.ok(obj instanceof PublicCtor,
"Object should be instance of PublicCtor");
test.assert(obj instanceof PrivateCtor,
assert.ok(obj instanceof PrivateCtor,
"Object should be instance of PrivateCtor");
test.assert(PublicCtor.prototype.isPrototypeOf(obj),
assert.ok(PublicCtor.prototype.isPrototypeOf(obj),
"PublicCtor's prototype should be prototype of object");
test.assertEqual(obj.constructor, PublicCtor,
assert.equal(obj.constructor, PublicCtor,
"Object constructor should be PublicCtor");
}
testObj(true);
testObj(false);
};
exports.testValidateOptionsEmpty = function (test) {
exports.testValidateOptionsEmpty = function (assert) {
let val = apiUtils.validateOptions(null, {});
assertObjsEqual(test, val, {});
assert.deepEqual(val, {});
val = apiUtils.validateOptions(null, { foo: {} });
assertObjsEqual(test, val, {});
assert.deepEqual(val, {});
val = apiUtils.validateOptions({}, {});
assertObjsEqual(test, val, {});
assert.deepEqual(val, {});
val = apiUtils.validateOptions({}, { foo: {} });
assertObjsEqual(test, val, {});
assert.deepEqual(val, {});
};
exports.testValidateOptionsNonempty = function (test) {
exports.testValidateOptionsNonempty = function (assert) {
let val = apiUtils.validateOptions({ foo: 123 }, {});
assertObjsEqual(test, val, {});
assert.deepEqual(val, {});
val = apiUtils.validateOptions({ foo: 123, bar: 456 },
{ foo: {}, bar: {}, baz: {} });
assertObjsEqual(test, val, { foo: 123, bar: 456 });
assert.deepEqual(val, { foo: 123, bar: 456 });
};
exports.testValidateOptionsMap = function (test) {
exports.testValidateOptionsMap = function (assert) {
let val = apiUtils.validateOptions({ foo: 3, bar: 2 }, {
foo: { map: function (v) v * v },
bar: { map: function (v) undefined }
});
assertObjsEqual(test, val, { foo: 9, bar: undefined });
assert.deepEqual(val, { foo: 9, bar: undefined });
};
exports.testValidateOptionsMapException = function (test) {
exports.testValidateOptionsMapException = function (assert) {
let val = apiUtils.validateOptions({ foo: 3 }, {
foo: { map: function () { throw new Error(); }}
});
assertObjsEqual(test, val, { foo: 3 });
assert.deepEqual(val, { foo: 3 });
};
exports.testValidateOptionsOk = function (test) {
exports.testValidateOptionsOk = function (assert) {
let val = apiUtils.validateOptions({ foo: 3, bar: 2, baz: 1 }, {
foo: { ok: function (v) v },
bar: { ok: function (v) v }
});
assertObjsEqual(test, val, { foo: 3, bar: 2 });
assert.deepEqual(val, { foo: 3, bar: 2 });
test.assertRaises(
assert.throws(
function () apiUtils.validateOptions({ foo: 2, bar: 2 }, {
bar: { ok: function (v) v > 2 }
}),
'The option "bar" is invalid.',
/^The option "bar" is invalid/,
"ok should raise exception on invalid option"
);
test.assertRaises(
assert.throws(
function () apiUtils.validateOptions(null, { foo: { ok: function (v) v }}),
'The option "foo" is invalid.',
/^The option "foo" is invalid/,
"ok should raise exception on invalid option"
);
};
exports.testValidateOptionsIs = function (test) {
exports.testValidateOptionsIs = function (assert) {
let opts = {
array: [],
boolean: true,
@ -114,18 +116,137 @@ exports.testValidateOptionsIs = function (test) {
undef2: { is: ["undefined"] }
};
let val = apiUtils.validateOptions(opts, requirements);
assertObjsEqual(test, val, opts);
assert.deepEqual(val, opts);
test.assertRaises(
assert.throws(
function () apiUtils.validateOptions(null, {
foo: { is: ["object", "number"] }
}),
'The option "foo" must be one of the following types: object, number',
/^The option "foo" must be one of the following types: object, number/,
"Invalid type should raise exception"
);
};
exports.testValidateOptionsMapIsOk = function (test) {
exports.testValidateOptionsIsWithExportedValue = function (assert) {
let { string, number, boolean, object } = apiUtils;
let opts = {
boolean: true,
number: 1337,
object: {},
string: "foo"
};
let requirements = {
string: { is: string },
number: { is: number },
boolean: { is: boolean },
object: { is: object }
};
let val = apiUtils.validateOptions(opts, requirements);
assert.deepEqual(val, opts);
// Test the types are optional by default
val = apiUtils.validateOptions({foo: 'bar'}, requirements);
assert.deepEqual(val, {});
};
exports.testValidateOptionsIsWithEither = function (assert) {
let { string, number, boolean, either } = apiUtils;
let text = { is: either(string, number) };
let requirements = {
text: text,
boolOrText: { is: either(text, boolean) }
};
let val = apiUtils.validateOptions({text: 12}, requirements);
assert.deepEqual(val, {text: 12});
val = apiUtils.validateOptions({text: "12"}, requirements);
assert.deepEqual(val, {text: "12"});
val = apiUtils.validateOptions({boolOrText: true}, requirements);
assert.deepEqual(val, {boolOrText: true});
val = apiUtils.validateOptions({boolOrText: "true"}, requirements);
assert.deepEqual(val, {boolOrText: "true"});
val = apiUtils.validateOptions({boolOrText: 1}, requirements);
assert.deepEqual(val, {boolOrText: 1});
assert.throws(
() => apiUtils.validateOptions({text: true}, requirements),
/^The option "text" must be one of the following types/,
"Invalid type should raise exception"
);
assert.throws(
() => apiUtils.validateOptions({boolOrText: []}, requirements),
/^The option "boolOrText" must be one of the following types/,
"Invalid type should raise exception"
);
};
exports.testValidateOptionsWithRequiredAndOptional = function (assert) {
let { string, number, required, optional } = apiUtils;
let opts = {
number: 1337,
string: "foo"
};
let requirements = {
string: required(string),
number: number
};
let val = apiUtils.validateOptions(opts, requirements);
assert.deepEqual(val, opts);
val = apiUtils.validateOptions({string: "foo"}, requirements);
assert.deepEqual(val, {string: "foo"});
assert.throws(
() => apiUtils.validateOptions({number: 10}, requirements),
/^The option "string" must be one of the following types/,
"Invalid type should raise exception"
);
// Makes string optional
requirements.string = optional(requirements.string);
val = apiUtils.validateOptions({number: 10}, requirements),
assert.deepEqual(val, {number: 10});
};
exports.testValidateOptionsWithExportedValue = function (assert) {
let { string, number, boolean, object } = apiUtils;
let opts = {
boolean: true,
number: 1337,
object: {},
string: "foo"
};
let requirements = {
string: string,
number: number,
boolean: boolean,
object: object
};
let val = apiUtils.validateOptions(opts, requirements);
assert.deepEqual(val, opts);
// Test the types are optional by default
val = apiUtils.validateOptions({foo: 'bar'}, requirements);
assert.deepEqual(val, {});
};
exports.testValidateOptionsMapIsOk = function (assert) {
let [map, is, ok] = [false, false, false];
let val = apiUtils.validateOptions({ foo: 1337 }, {
foo: {
@ -134,48 +255,48 @@ exports.testValidateOptionsMapIsOk = function (test) {
ok: function (v) v.length > 0
}
});
assertObjsEqual(test, val, { foo: "1337" });
assert.deepEqual(val, { foo: "1337" });
let requirements = {
foo: {
is: ["object"],
ok: function () test.fail("is should have caused us to throw by now")
ok: function () assert.fail("is should have caused us to throw by now")
}
};
test.assertRaises(
assert.throws(
function () apiUtils.validateOptions(null, requirements),
'The option "foo" must be one of the following types: object',
/^The option "foo" must be one of the following types: object/,
"is should be used before ok is called"
);
};
exports.testValidateOptionsErrorMsg = function (test) {
test.assertRaises(
exports.testValidateOptionsErrorMsg = function (assert) {
assert.throws(
function () apiUtils.validateOptions(null, {
foo: { ok: function (v) v, msg: "foo!" }
}),
"foo!",
/^foo!/,
"ok should raise exception with customized message"
);
};
exports.testValidateMapWithMissingKey = function (test) {
exports.testValidateMapWithMissingKey = function (assert) {
let val = apiUtils.validateOptions({ }, {
foo: {
map: function (v) v || "bar"
}
});
assertObjsEqual(test, val, { foo: "bar" });
assert.deepEqual(val, { foo: "bar" });
val = apiUtils.validateOptions({ }, {
foo: {
map: function (v) { throw "bar" }
}
});
assertObjsEqual(test, val, { });
assert.deepEqual(val, { });
};
exports.testValidateMapWithMissingKeyAndThrown = function (test) {
exports.testValidateMapWithMissingKeyAndThrown = function (assert) {
let val = apiUtils.validateOptions({}, {
bar: {
map: function(v) { throw "bar" }
@ -184,10 +305,10 @@ exports.testValidateMapWithMissingKeyAndThrown = function (test) {
map: function(v) "foo"
}
});
assertObjsEqual(test, val, { baz: "foo" });
assert.deepEqual(val, { baz: "foo" });
};
exports.testAddIterator = function testAddIterator(test) {
exports.testAddIterator = function testAddIterator (assert) {
let obj = {};
let keys = ["foo", "bar", "baz"];
let vals = [1, 2, 3];
@ -203,34 +324,20 @@ exports.testAddIterator = function testAddIterator(test) {
let keysItr = [];
for (let key in obj)
keysItr.push(key);
test.assertEqual(keysItr.length, keys.length,
assert.equal(keysItr.length, keys.length,
"the keys iterator returns the correct number of items");
for (let i = 0; i < keys.length; i++)
test.assertEqual(keysItr[i], keys[i], "the key is correct");
assert.equal(keysItr[i], keys[i], "the key is correct");
let valsItr = [];
for each (let val in obj)
valsItr.push(val);
test.assertEqual(valsItr.length, vals.length,
assert.equal(valsItr.length, vals.length,
"the vals iterator returns the correct number of items");
for (let i = 0; i < vals.length; i++)
test.assertEqual(valsItr[i], vals[i], "the val is correct");
assert.equal(valsItr[i], vals[i], "the val is correct");
};
function assertObjsEqual(test, obj1, obj2) {
var items = 0;
for (let key in obj1) {
items++;
test.assert(key in obj2, "obj1 key should be present in obj2");
test.assertEqual(obj2[key], obj1[key], "obj1 value should match obj2 value");
}
for (let key in obj2) {
items++;
test.assert(key in obj1, "obj2 key should be present in obj1");
test.assertEqual(obj1[key], obj2[key], "obj2 value should match obj1 value");
}
if (!items)
test.assertEqual(JSON.stringify(obj1), JSON.stringify(obj2),
"obj1 should have same JSON representation as obj2");
}
require('test').run(exports);

View File

@ -5,67 +5,67 @@
const array = require('sdk/util/array');
exports.testHas = function(test) {
exports.testHas = function(assert) {
var testAry = [1, 2, 3];
test.assertEqual(array.has([1, 2, 3], 1), true);
test.assertEqual(testAry.length, 3);
test.assertEqual(testAry[0], 1);
test.assertEqual(testAry[1], 2);
test.assertEqual(testAry[2], 3);
test.assertEqual(array.has(testAry, 2), true);
test.assertEqual(array.has(testAry, 3), true);
test.assertEqual(array.has(testAry, 4), false);
test.assertEqual(array.has(testAry, '1'), false);
assert.equal(array.has([1, 2, 3], 1), true);
assert.equal(testAry.length, 3);
assert.equal(testAry[0], 1);
assert.equal(testAry[1], 2);
assert.equal(testAry[2], 3);
assert.equal(array.has(testAry, 2), true);
assert.equal(array.has(testAry, 3), true);
assert.equal(array.has(testAry, 4), false);
assert.equal(array.has(testAry, '1'), false);
};
exports.testHasAny = function(test) {
exports.testHasAny = function(assert) {
var testAry = [1, 2, 3];
test.assertEqual(array.hasAny([1, 2, 3], [1]), true);
test.assertEqual(array.hasAny([1, 2, 3], [1, 5]), true);
test.assertEqual(array.hasAny([1, 2, 3], [5, 1]), true);
test.assertEqual(array.hasAny([1, 2, 3], [5, 2]), true);
test.assertEqual(array.hasAny([1, 2, 3], [5, 3]), true);
test.assertEqual(array.hasAny([1, 2, 3], [5, 4]), false);
test.assertEqual(testAry.length, 3);
test.assertEqual(testAry[0], 1);
test.assertEqual(testAry[1], 2);
test.assertEqual(testAry[2], 3);
test.assertEqual(array.hasAny(testAry, [2]), true);
test.assertEqual(array.hasAny(testAry, [3]), true);
test.assertEqual(array.hasAny(testAry, [4]), false);
test.assertEqual(array.hasAny(testAry), false);
test.assertEqual(array.hasAny(testAry, '1'), false);
assert.equal(array.hasAny([1, 2, 3], [1]), true);
assert.equal(array.hasAny([1, 2, 3], [1, 5]), true);
assert.equal(array.hasAny([1, 2, 3], [5, 1]), true);
assert.equal(array.hasAny([1, 2, 3], [5, 2]), true);
assert.equal(array.hasAny([1, 2, 3], [5, 3]), true);
assert.equal(array.hasAny([1, 2, 3], [5, 4]), false);
assert.equal(testAry.length, 3);
assert.equal(testAry[0], 1);
assert.equal(testAry[1], 2);
assert.equal(testAry[2], 3);
assert.equal(array.hasAny(testAry, [2]), true);
assert.equal(array.hasAny(testAry, [3]), true);
assert.equal(array.hasAny(testAry, [4]), false);
assert.equal(array.hasAny(testAry), false);
assert.equal(array.hasAny(testAry, '1'), false);
};
exports.testAdd = function(test) {
exports.testAdd = function(assert) {
var testAry = [1];
test.assertEqual(array.add(testAry, 1), false);
test.assertEqual(testAry.length, 1);
test.assertEqual(testAry[0], 1);
test.assertEqual(array.add(testAry, 2), true);
test.assertEqual(testAry.length, 2);
test.assertEqual(testAry[0], 1);
test.assertEqual(testAry[1], 2);
assert.equal(array.add(testAry, 1), false);
assert.equal(testAry.length, 1);
assert.equal(testAry[0], 1);
assert.equal(array.add(testAry, 2), true);
assert.equal(testAry.length, 2);
assert.equal(testAry[0], 1);
assert.equal(testAry[1], 2);
};
exports.testRemove = function(test) {
exports.testRemove = function(assert) {
var testAry = [1, 2];
test.assertEqual(array.remove(testAry, 3), false);
test.assertEqual(testAry.length, 2);
test.assertEqual(testAry[0], 1);
test.assertEqual(testAry[1], 2);
test.assertEqual(array.remove(testAry, 2), true);
test.assertEqual(testAry.length, 1);
test.assertEqual(testAry[0], 1);
assert.equal(array.remove(testAry, 3), false);
assert.equal(testAry.length, 2);
assert.equal(testAry[0], 1);
assert.equal(testAry[1], 2);
assert.equal(array.remove(testAry, 2), true);
assert.equal(testAry.length, 1);
assert.equal(testAry[0], 1);
};
exports.testFlatten = function(test) {
test.assertEqual(array.flatten([1, 2, 3]).length, 3);
test.assertEqual(array.flatten([1, [2, 3]]).length, 3);
test.assertEqual(array.flatten([1, [2, [3]]]).length, 3);
test.assertEqual(array.flatten([[1], [[2, [3]]]]).length, 3);
exports.testFlatten = function(assert) {
assert.equal(array.flatten([1, 2, 3]).length, 3);
assert.equal(array.flatten([1, [2, 3]]).length, 3);
assert.equal(array.flatten([1, [2, [3]]]).length, 3);
assert.equal(array.flatten([[1], [[2, [3]]]]).length, 3);
};
exports.testUnique = function(test) {
exports.testUnique = function(assert) {
var Class = function () {};
var A = {};
var B = new Class();
@ -73,23 +73,31 @@ exports.testUnique = function(test) {
var D = {};
var E = new Class();
compareArray(array.unique([1,2,3,1,2]), [1,2,3]);
compareArray(array.unique([1,1,1,4,9,5,5]), [1,4,9,5]);
compareArray(array.unique([A, A, A, B, B, D]), [A,B,D]);
compareArray(array.unique([A, D, A, E, E, D, A, A, C]), [A, D, E, C])
function compareArray (a, b) {
test.assertEqual(a.length, b.length);
for (let i = 0; i < a.length; i++) {
test.assertEqual(a[i], b[i]);
}
}
assert.deepEqual(array.unique([1,2,3,1,2]), [1,2,3]);
assert.deepEqual(array.unique([1,1,1,4,9,5,5]), [1,4,9,5]);
assert.deepEqual(array.unique([A, A, A, B, B, D]), [A,B,D]);
assert.deepEqual(array.unique([A, D, A, E, E, D, A, A, C]), [A, D, E, C])
};
exports.testFind = function(test) {
exports.testUnion = function(assert) {
var Class = function () {};
var A = {};
var B = new Class();
var C = [ 1, 2, 3 ];
var D = {};
var E = new Class();
assert.deepEqual(array.union([1, 2, 3],[7, 1, 2]), [1, 2, 3, 7]);
assert.deepEqual(array.union([1, 1, 1, 4, 9, 5, 5], [10, 1, 5]), [1, 4, 9, 5, 10]);
assert.deepEqual(array.union([A, B], [A, D]), [A, B, D]);
assert.deepEqual(array.union([A, D], [A, E], [E, D, A], [A, C]), [A, D, E, C]);
};
exports.testFind = function(assert) {
let isOdd = (x) => x % 2;
test.assertEqual(array.find([2, 4, 5, 7, 8, 9], isOdd), 5);
test.assertEqual(array.find([2, 4, 6, 8], isOdd), undefined);
test.assertEqual(array.find([2, 4, 6, 8], isOdd, null), null);
assert.equal(array.find([2, 4, 5, 7, 8, 9], isOdd), 5);
assert.equal(array.find([2, 4, 6, 8], isOdd), undefined);
assert.equal(array.find([2, 4, 6, 8], isOdd, null), null);
};
require('test').run(exports);

View File

@ -4,6 +4,12 @@
"use strict";
module.metadata = {
engines: {
"Firefox": "*"
}
};
const { Loader } = require("sdk/test/loader");
const { open, getMostRecentBrowserWindow, getOuterId } = require("sdk/window/utils");
const { setTimeout } = require("sdk/timers");

View File

@ -44,13 +44,17 @@ exports["test multiple tabs"] = function(assert, done) {
on(events, "data", function({type, target, timeStamp}) {
// ignore about:blank pages and *-document-global-created
// events that are not very consistent.
// ignore http:// requests, as Fennec's `about:home` page
// displays add-ons a user could install
if (target.URL !== "about:blank" &&
target.URL !== "about:home" &&
!target.URL.match(/^https?:\/\//i) &&
type !== "chrome-document-global-created" &&
type !== "content-document-global-created")
actual.push(type + " -> " + target.URL)
});
let window = getMostRecentBrowserWindow();
let window = getMostRecentBrowserWindow();
let firstTab = open("data:text/html,first-tab", window);
when("pageshow", firstTab).

View File

@ -429,8 +429,3 @@ if (isWindows) {
};
require('test').run(exports);
// Test disabled on OSX because of bug 891698
if (require("sdk/system/runtime").OS == "Darwin")
module.exports = {};

View File

@ -0,0 +1,292 @@
/* 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';
module.metadata = {
'engines': {
'Firefox': '*'
}
};
const { Cc, Ci } = require('chrome');
const { defer, all } = require('sdk/core/promise');
const { filter } = require('sdk/event/utils');
const { on, off } = require('sdk/event/core');
const { events } = require('sdk/places/events');
const { setTimeout } = require('sdk/timers');
const { before, after } = require('sdk/test/utils');
const {
search
} = require('sdk/places/history');
const {
invalidResolve, invalidReject, createTree, createBookmark,
compareWithHost, addVisits, resetPlaces, createBookmarkItem,
removeVisits
} = require('./places-helper');
const { save, MENU, UNSORTED } = require('sdk/places/bookmarks');
const { promisedEmitter } = require('sdk/places/utils');
exports['test bookmark-item-added'] = function (assert, done) {
function handler ({type, data}) {
if (type !== 'bookmark-item-added') return;
if (data.title !== 'bookmark-added-title') return;
assert.equal(type, 'bookmark-item-added', 'correct type in bookmark-added event');
assert.equal(data.type, 'bookmark', 'correct data in bookmark-added event');
assert.ok(data.id != null, 'correct data in bookmark-added event');
assert.ok(data.parentId != null, 'correct data in bookmark-added event');
assert.ok(data.index != null, 'correct data in bookmark-added event');
assert.equal(data.url, 'http://moz.com/', 'correct data in bookmark-added event');
assert.ok(data.dateAdded != null, 'correct data in bookmark-added event');
events.off('data', handler);
done();
}
events.on('data', handler);
createBookmark({ title: 'bookmark-added-title' });
};
exports['test bookmark-item-changed'] = function (assert, done) {
let id;
let complete = makeCompleted(done);
function handler ({type, data}) {
if (type !== 'bookmark-item-changed') return;
if (data.id !== id) return;
assert.equal(type, 'bookmark-item-changed',
'correct type in bookmark-item-changed event');
assert.equal(data.type, 'bookmark',
'correct data in bookmark-item-changed event');
assert.equal(data.property, 'title',
'correct property in bookmark-item-changed event');
assert.equal(data.value, 'bookmark-changed-title-2',
'correct value in bookmark-item-changed event');
assert.ok(data.id === id, 'correct id in bookmark-item-changed event');
assert.ok(data.parentId != null, 'correct data in bookmark-added event');
events.off('data', handler);
complete();
}
events.on('data', handler);
createBookmarkItem({ title: 'bookmark-changed-title' }).then(item => {
id = item.id;
item.title = 'bookmark-changed-title-2';
return saveP(item);
}).then(complete);
};
exports['test bookmark-item-moved'] = function (assert, done) {
let id;
let complete = makeCompleted(done);
function handler ({type, data}) {
if (type !== 'bookmark-item-moved') return;
if (data.id !== id) return;
assert.equal(type, 'bookmark-item-moved',
'correct type in bookmark-item-moved event');
assert.equal(data.type, 'bookmark',
'correct data in bookmark-item-moved event');
assert.ok(data.id === id, 'correct id in bookmark-item-moved event');
assert.equal(data.previousParentId, UNSORTED.id,
'correct previousParentId');
assert.equal(data.currentParentId, MENU.id,
'correct currentParentId');
assert.equal(data.previousIndex, 0, 'correct previousIndex');
assert.equal(data.currentIndex, 0, 'correct currentIndex');
events.off('data', handler);
complete();
}
events.on('data', handler);
createBookmarkItem({
title: 'bookmark-moved-title',
group: UNSORTED
}).then(item => {
id = item.id;
item.group = MENU;
return saveP(item);
}).then(complete);
};
exports['test bookmark-item-removed'] = function (assert, done) {
let id;
let complete = makeCompleted(done);
function handler ({type, data}) {
if (type !== 'bookmark-item-removed') return;
if (data.id !== id) return;
assert.equal(type, 'bookmark-item-removed',
'correct type in bookmark-item-removed event');
assert.equal(data.type, 'bookmark',
'correct data in bookmark-item-removed event');
assert.ok(data.id === id, 'correct id in bookmark-item-removed event');
assert.equal(data.parentId, UNSORTED.id,
'correct parentId in bookmark-item-removed');
assert.equal(data.url, 'http://moz.com/',
'correct url in bookmark-item-removed event');
assert.equal(data.index, 0,
'correct index in bookmark-item-removed event');
events.off('data', handler);
complete();
}
events.on('data', handler);
createBookmarkItem({
title: 'bookmark-item-remove-title',
group: UNSORTED
}).then(item => {
id = item.id;
item.remove = true;
return saveP(item);
}).then(complete);
};
exports['test bookmark-item-visited'] = function (assert, done) {
let id;
let complete = makeCompleted(done);
function handler ({type, data}) {
if (type !== 'bookmark-item-visited') return;
if (data.id !== id) return;
assert.equal(type, 'bookmark-item-visited',
'correct type in bookmark-item-visited event');
assert.ok(data.id === id, 'correct id in bookmark-item-visited event');
assert.equal(data.parentId, UNSORTED.id,
'correct parentId in bookmark-item-visited');
assert.ok(data.transitionType != null,
'has a transition type in bookmark-item-visited event');
assert.ok(data.time != null,
'has a time in bookmark-item-visited event');
assert.ok(data.visitId != null,
'has a visitId in bookmark-item-visited event');
assert.equal(data.url, 'http://bookmark-item-visited.com/',
'correct url in bookmark-item-visited event');
events.off('data', handler);
complete();
}
events.on('data', handler);
createBookmarkItem({
title: 'bookmark-item-visited',
url: 'http://bookmark-item-visited.com/'
}).then(item => {
id = item.id;
return addVisits('http://bookmark-item-visited.com/');
}).then(complete);
};
exports['test history-start-batch, history-end-batch, history-start-clear'] = function (assert, done) {
let complete = makeCompleted(done, 4);
let startEvent = filter(events, ({type}) => type === 'history-start-batch');
let endEvent = filter(events, ({type}) => type === 'history-end-batch');
let clearEvent = filter(events, ({type}) => type === 'history-start-clear');
function startHandler ({type, data}) {
assert.pass('history-start-batch called');
assert.equal(type, 'history-start-batch',
'history-start-batch has correct type');
off(startEvent, 'data', startHandler);
on(endEvent, 'data', endHandler);
complete();
}
function endHandler ({type, data}) {
assert.pass('history-end-batch called');
assert.equal(type, 'history-end-batch',
'history-end-batch has correct type');
off(endEvent, 'data', endHandler);
complete();
}
function clearHandler ({type, data}) {
assert.pass('history-start-clear called');
assert.equal(type, 'history-start-clear',
'history-start-clear has correct type');
off(clearEvent, 'data', clearHandler);
complete();
}
on(startEvent, 'data', startHandler);
on(clearEvent, 'data', clearHandler);
createBookmark().then(() => {
resetPlaces(complete);
})
};
exports['test history-visit, history-title-changed'] = function (assert, done) {
let complete = makeCompleted(() => {
off(titleEvents, 'data', titleHandler);
off(visitEvents, 'data', visitHandler);
done();
}, 6);
let visitEvents = filter(events, ({type}) => type === 'history-visit');
let titleEvents = filter(events, ({type}) => type === 'history-title-changed');
let urls = ['http://moz.com/', 'http://firefox.com/', 'http://mdn.com/'];
function visitHandler ({type, data}) {
assert.equal(type, 'history-visit', 'correct type in history-visit');
assert.ok(~urls.indexOf(data.url), 'history-visit has correct url');
assert.ok(data.visitId != null, 'history-visit has a visitId');
assert.ok(data.time != null, 'history-visit has a time');
assert.ok(data.sessionId != null, 'history-visit has a sessionId');
assert.ok(data.referringId != null, 'history-visit has a referringId');
assert.ok(data.transitionType != null, 'history-visit has a transitionType');
complete();
}
function titleHandler ({type, data}) {
assert.equal(type, 'history-title-changed',
'correct type in history-title-changed');
assert.ok(~urls.indexOf(data.url),
'history-title-changed has correct url');
assert.ok(data.title, 'history-title-changed has title');
complete();
}
on(titleEvents, 'data', titleHandler);
on(visitEvents, 'data', visitHandler);
addVisits(urls);
}
exports['test history-delete-url'] = function (assert, done) {
let complete = makeCompleted(() => {
events.off('data', handler);
done();
}, 3);
let urls = ['http://moz.com/', 'http://firefox.com/', 'http://mdn.com/'];
function handler({type, data}) {
if (type !== 'history-delete-url') return;
assert.equal(type, 'history-delete-url',
'history-delete-url has correct type');
assert.ok(~urls.indexOf(data.url), 'history-delete-url has correct url');
complete();
}
events.on('data', handler);
addVisits(urls).then(() => {
removeVisits(urls);
});
};
exports['test history-page-changed'] = function (assert) {
assert.pass('history-page-changed tested in test-places-favicons');
};
exports['test history-delete-visits'] = function (assert) {
assert.pass('TODO test history-delete-visits');
};
before(exports, (name, assert, done) => resetPlaces(done));
after(exports, (name, assert, done) => resetPlaces(done));
function saveP () {
return promisedEmitter(save.apply(null, Array.slice(arguments)));
}
function makeCompleted (done, countTo) {
let count = 0;
countTo = countTo || 2;
return function () {
if (++count === countTo) done();
};
}
require('sdk/test').run(exports);

View File

@ -35,7 +35,7 @@ const tagsrv = Cc['@mozilla.org/browser/tagging-service;1'].
exports.testBookmarksCreate = function (assert, done) {
let items = [{
title: 'my title',
url: 'http://moz.com',
url: 'http://test-places-host.com/testBookmarksCreate/',
tags: ['some', 'tags', 'yeah'],
type: 'bookmark'
}, {
@ -71,26 +71,26 @@ exports.testBookmarksCreateFail = function (assert, done) {
return send('sdk-places-bookmarks-create', item).then(null, function (reason) {
assert.ok(reason, 'bookmark create should fail');
});
})).then(function () {
done();
});
})).then(done);
};
exports.testBookmarkLastUpdated = function (assert, done) {
let timestamp;
let item;
createBookmark().then(function (data) {
createBookmark({
url: 'http://test-places-host.com/testBookmarkLastUpdated'
}).then(function (data) {
item = data;
timestamp = item.updated;
return send('sdk-places-bookmarks-last-updated', { id: item.id });
}).then(function (updated) {
let { resolve, promise } = defer();
assert.equal(timestamp, updated, 'should return last updated time');
item.title = 'updated mozilla';
return send('sdk-places-bookmarks-save', item).then(function (data) {
let deferred = defer();
setTimeout(function () deferred.resolve(data), 100);
return deferred.promise;
});
setTimeout(() => {
resolve(send('sdk-places-bookmarks-save', item));
}, 100);
return promise;
}).then(function (data) {
assert.ok(data.updated > timestamp, 'time has elapsed and updated the updated property');
done();
@ -99,7 +99,9 @@ exports.testBookmarkLastUpdated = function (assert, done) {
exports.testBookmarkRemove = function (assert, done) {
let id;
createBookmark().then(function (data) {
createBookmark({
url: 'http://test-places-host.com/testBookmarkRemove/'
}).then(function (data) {
id = data.id;
compareWithHost(assert, data); // ensure bookmark exists
bmsrv.getItemTitle(id); // does not throw an error
@ -114,7 +116,9 @@ exports.testBookmarkRemove = function (assert, done) {
exports.testBookmarkGet = function (assert, done) {
let bookmark;
createBookmark().then(function (data) {
createBookmark({
url: 'http://test-places-host.com/testBookmarkGet/'
}).then(function (data) {
bookmark = data;
return send('sdk-places-bookmarks-get', { id: data.id });
}).then(function (data) {
@ -136,7 +140,9 @@ exports.testBookmarkGet = function (assert, done) {
exports.testTagsTag = function (assert, done) {
let url;
createBookmark().then(function (data) {
createBookmark({
url: 'http://test-places-host.com/testTagsTag/',
}).then(function (data) {
url = data.url;
return send('sdk-places-tags-tag', {
url: data.url, tags: ['mozzerella', 'foxfire']
@ -153,7 +159,10 @@ exports.testTagsTag = function (assert, done) {
exports.testTagsUntag = function (assert, done) {
let item;
createBookmark({tags: ['tag1', 'tag2', 'tag3']}).then(function (data) {
createBookmark({
url: 'http://test-places-host.com/testTagsUntag/',
tags: ['tag1', 'tag2', 'tag3']
}).then(data => {
item = data;
return send('sdk-places-tags-untag', {
url: item.url,
@ -172,7 +181,9 @@ exports.testTagsUntag = function (assert, done) {
exports.testTagsGetURLsByTag = function (assert, done) {
let item;
createBookmark().then(function (data) {
createBookmark({
url: 'http://test-places-host.com/testTagsGetURLsByTag/'
}).then(function (data) {
item = data;
return send('sdk-places-tags-get-urls-by-tag', {
tag: 'firefox'
@ -186,7 +197,10 @@ exports.testTagsGetURLsByTag = function (assert, done) {
exports.testTagsGetTagsByURL = function (assert, done) {
let item;
createBookmark({ tags: ['firefox', 'mozilla', 'metal']}).then(function (data) {
createBookmark({
url: 'http://test-places-host.com/testTagsGetURLsByTag/',
tags: ['firefox', 'mozilla', 'metal']
}).then(function (data) {
item = data;
return send('sdk-places-tags-get-tags-by-url', {
url: data.url,
@ -202,9 +216,15 @@ exports.testTagsGetTagsByURL = function (assert, done) {
exports.testHostQuery = function (assert, done) {
all([
createBookmark({ url: 'http://firefox.com', tags: ['firefox', 'mozilla'] }),
createBookmark({ url: 'http://mozilla.com', tags: ['mozilla'] }),
createBookmark({ url: 'http://thunderbird.com' })
createBookmark({
url: 'http://firefox.com/testHostQuery/',
tags: ['firefox', 'mozilla']
}),
createBookmark({
url: 'http://mozilla.com/testHostQuery/',
tags: ['mozilla']
}),
createBookmark({ url: 'http://thunderbird.com/testHostQuery/' })
]).then(data => {
return send('sdk-places-query', {
queries: { tags: ['mozilla'] },
@ -212,34 +232,44 @@ exports.testHostQuery = function (assert, done) {
});
}).then(results => {
assert.equal(results.length, 2, 'should only return two');
assert.equal(results[0].url, 'http://mozilla.com/', 'is sorted by URI asc');
assert.equal(results[0].url,
'http://mozilla.com/testHostQuery/', 'is sorted by URI asc');
return send('sdk-places-query', {
queries: { tags: ['mozilla'] },
options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only
});
}).then(results => {
assert.equal(results.length, 2, 'should only return two');
assert.equal(results[0].url, 'http://firefox.com/', 'is sorted by URI desc');
assert.equal(results[0].url,
'http://firefox.com/testHostQuery/', 'is sorted by URI desc');
done();
});
};
exports.testHostMultiQuery = function (assert, done) {
all([
createBookmark({ url: 'http://firefox.com', tags: ['firefox', 'mozilla'] }),
createBookmark({ url: 'http://mozilla.com', tags: ['mozilla'] }),
createBookmark({ url: 'http://thunderbird.com' })
createBookmark({
url: 'http://firefox.com/testHostMultiQuery/',
tags: ['firefox', 'mozilla']
}),
createBookmark({
url: 'http://mozilla.com/testHostMultiQuery/',
tags: ['mozilla']
}),
createBookmark({ url: 'http://thunderbird.com/testHostMultiQuery/' })
]).then(data => {
return send('sdk-places-query', {
queries: [{ tags: ['firefox'] }, { uri: 'http://thunderbird.com/' }],
queries: [{ tags: ['firefox'] }, { uri: 'http://thunderbird.com/testHostMultiQuery/' }],
options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only
});
}).then(results => {
assert.equal(results.length, 2, 'should return 2 results ORing queries');
assert.equal(results[0].url, 'http://firefox.com/', 'should match URL or tag');
assert.equal(results[1].url, 'http://thunderbird.com/', 'should match URL or tag');
assert.equal(results[0].url,
'http://firefox.com/testHostMultiQuery/', 'should match URL or tag');
assert.equal(results[1].url,
'http://thunderbird.com/testHostMultiQuery/', 'should match URL or tag');
return send('sdk-places-query', {
queries: [{ tags: ['firefox'], url: 'http://mozilla.com/' }],
queries: [{ tags: ['firefox'], url: 'http://mozilla.com/testHostMultiQuery/' }],
options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only
});
}).then(results => {
@ -269,7 +299,6 @@ exports.testGetAllChildren = function (assert, done) {
});
};
before(exports, (name, assert, done) => resetPlaces(done));
after(exports, (name, assert, done) => resetPlaces(done));

View File

@ -457,3 +457,100 @@ exports.testTabReload = function(test) {
}
});
};
exports.testOnPageShowEvent = function (test) {
test.waitUntilDone();
let events = [];
let firstUrl = 'data:text/html;charset=utf-8,First';
let secondUrl = 'data:text/html;charset=utf-8,Second';
let counter = 0;
function onPageShow (tab, persisted) {
events.push('pageshow');
counter++;
if (counter === 1) {
test.assertEqual(persisted, false, 'page should not be cached on initial load');
tab.url = secondUrl;
}
else if (counter === 2) {
test.assertEqual(persisted, false, 'second test page should not be cached either');
tab.attach({
contentScript: 'setTimeout(function () { window.history.back(); }, 0)'
});
}
else {
test.assertEqual(persisted, true, 'when we get back to the fist page, it has to' +
'come from cache');
tabs.removeListener('pageshow', onPageShow);
tabs.removeListener('open', onOpen);
tabs.removeListener('ready', onReady);
tab.close(() => {
['open', 'ready', 'pageshow', 'ready',
'pageshow', 'pageshow'].map((type, i) => {
test.assertEqual(type, events[i], 'correct ordering of events');
});
test.done()
});
}
}
function onOpen () events.push('open');
function onReady () events.push('ready');
tabs.on('pageshow', onPageShow);
tabs.on('open', onOpen);
tabs.on('ready', onReady);
tabs.open({
url: firstUrl
});
};
exports.testOnPageShowEventDeclarative = function (test) {
test.waitUntilDone();
let events = [];
let firstUrl = 'data:text/html;charset=utf-8,First';
let secondUrl = 'data:text/html;charset=utf-8,Second';
let counter = 0;
function onPageShow (tab, persisted) {
events.push('pageshow');
counter++;
if (counter === 1) {
test.assertEqual(persisted, false, 'page should not be cached on initial load');
tab.url = secondUrl;
}
else if (counter === 2) {
test.assertEqual(persisted, false, 'second test page should not be cached either');
tab.attach({
contentScript: 'setTimeout(function () { window.history.back(); }, 0)'
});
}
else {
test.assertEqual(persisted, true, 'when we get back to the fist page, it has to' +
'come from cache');
tabs.removeListener('pageshow', onPageShow);
tabs.removeListener('open', onOpen);
tabs.removeListener('ready', onReady);
tab.close(() => {
['open', 'ready', 'pageshow', 'ready',
'pageshow', 'pageshow'].map((type, i) => {
test.assertEqual(type, events[i], 'correct ordering of events');
});
test.done()
});
}
}
function onOpen () events.push('open');
function onReady () events.push('ready');
tabs.open({
url: firstUrl,
onPageShow: onPageShow,
onOpen: onOpen,
onReady: onReady
});
};

View File

@ -3,7 +3,15 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const { URL, toFilename, fromFilename, isValidURI, getTLD, DataURL } = require('sdk/url');
const {
URL,
toFilename,
fromFilename,
isValidURI,
getTLD,
DataURL,
isLocalURL } = require('sdk/url');
const { pathFor } = require('sdk/system');
const file = require('sdk/io/file');
const tabs = require('sdk/tabs');
@ -63,11 +71,11 @@ exports.testParseHttpSearchAndHash = function (assert) {
var info = URL('https://www.moz.com/some/page.html');
assert.equal(info.hash, '');
assert.equal(info.search, '');
var hashOnly = URL('https://www.sub.moz.com/page.html#justhash');
assert.equal(hashOnly.search, '');
assert.equal(hashOnly.hash, '#justhash');
var queryOnly = URL('https://www.sub.moz.com/page.html?my=query');
assert.equal(queryOnly.search, '?my=query');
assert.equal(queryOnly.hash, '');
@ -75,11 +83,11 @@ exports.testParseHttpSearchAndHash = function (assert) {
var qMark = URL('http://www.moz.org?');
assert.equal(qMark.search, '');
assert.equal(qMark.hash, '');
var hash = URL('http://www.moz.org#');
assert.equal(hash.search, '');
assert.equal(hash.hash, '');
var empty = URL('http://www.moz.org?#');
assert.equal(hash.search, '');
assert.equal(hash.hash, '');
@ -347,6 +355,39 @@ exports.testWindowLocationMatch = function (assert, done) {
})
};
exports.testURLInRegExpTest = function(assert) {
let url = 'https://mozilla.org';
assert.equal((new RegExp(url).test(URL(url))), true, 'URL instances work in a RegExp test');
}
exports.testLocalURL = function(assert) {
[
'data:text/html;charset=utf-8,foo and bar',
'data:text/plain,foo and bar',
'resource://gre/modules/commonjs/',
'chrome://browser/content/browser.xul'
].forEach(aUri => {
assert.ok(isLocalURL(aUri), aUri + ' is a Local URL');
})
}
exports.testLocalURLwithRemoteURL = function(assert) {
validURIs().filter(url => !url.startsWith('data:')).forEach(aUri => {
assert.ok(!isLocalURL(aUri), aUri + ' is an invalid Local URL');
});
}
exports.testLocalURLwithInvalidURL = function(assert) {
invalidURIs().concat([
'data:foo and bar',
'resource:// must fail',
'chrome:// here too'
]).forEach(aUri => {
assert.ok(!isLocalURL(aUri), aUri + ' is an invalid Local URL');
});
}
function validURIs() {
return [
'http://foo.com/blah_blah',

View File

@ -181,7 +181,7 @@ var ContextUI = {
// Dismiss the navbar if visible.
dismissNavbar: function dismissNavbar() {
if (!StartUI.isVisible) {
if (!BrowserUI.isStartTabVisible) {
Elements.navbar.dismiss();
}
},
@ -230,17 +230,11 @@ var ContextUI = {
_onEdgeUIStarted: function(aEvent) {
this._hasEdgeSwipeStarted = true;
this._clearDelayedTimeout();
if (StartUI.hide()) {
this.dismiss();
return;
}
this.toggleNavUI();
},
_onEdgeUICanceled: function(aEvent) {
this._hasEdgeSwipeStarted = false;
StartUI.hide();
this.dismiss();
},
@ -251,10 +245,6 @@ var ContextUI = {
}
this._clearDelayedTimeout();
if (StartUI.hide()) {
this.dismiss();
return;
}
this.toggleNavUI();
},
@ -283,16 +273,21 @@ var ContextUI = {
this.dismissTabs();
break;
case "mousedown":
if (BrowserUI.isStartTabVisible)
break;
if (aEvent.button == 0 && this.isVisible)
this.dismiss();
break;
case "ToolPanelShown":
case "ToolPanelHidden":
case "touchstart":
case "AlertActive":
this.dismiss();
break;
case "touchstart":
if (!BrowserUI.isStartTabVisible) {
this.dismiss();
}
break;
}
},

View File

@ -3,10 +3,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
let prefs = Components.classes["@mozilla.org/preferences-service;1"].
getService(Components.interfaces.nsIPrefBranch);
Cu.import("resource://gre/modules/PageThumbs.jsm");
Cu.import("resource:///modules/colorUtils.jsm");
/**
* singleton to provide data-level functionality to the views
@ -163,290 +159,3 @@ let TopSites = {
}
};
function TopSitesView(aGrid, aMaxSites) {
this._set = aGrid;
this._set.controller = this;
this._topSitesMax = aMaxSites;
// clean up state when the appbar closes
window.addEventListener('MozAppbarDismissing', this, false);
let history = Cc["@mozilla.org/browser/nav-history-service;1"].
getService(Ci.nsINavHistoryService);
history.addObserver(this, false);
PageThumbs.addExpirationFilter(this);
Services.obs.addObserver(this, "Metro:RefreshTopsiteThumbnail", false);
Services.obs.addObserver(this, "metro_viewstate_changed", false);
NewTabUtils.allPages.register(this);
TopSites.prepareCache().then(function(){
this.populateGrid();
}.bind(this));
}
TopSitesView.prototype = Util.extend(Object.create(View.prototype), {
_set:null,
_topSitesMax: null,
// _lastSelectedSites used to temporarily store blocked/removed sites for undo/restore-ing
_lastSelectedSites: null,
// isUpdating used only for testing currently
isUpdating: false,
handleItemClick: function tabview_handleItemClick(aItem) {
let url = aItem.getAttribute("value");
BrowserUI.goToURI(url);
},
doActionOnSelectedTiles: function(aActionName, aEvent) {
let tileGroup = this._set;
let selectedTiles = tileGroup.selectedItems;
let sites = Array.map(selectedTiles, TopSites._linkFromNode);
let nextContextActions = new Set();
switch (aActionName){
case "delete":
for (let aNode of selectedTiles) {
// add some class to transition element before deletion?
aNode.contextActions.delete('delete');
// we need new context buttons to show (the tile node will go away though)
}
this._lastSelectedSites = (this._lastSelectedSites || []).concat(sites);
// stop the appbar from dismissing
aEvent.preventDefault();
nextContextActions.add('restore');
TopSites.hideSites(sites);
break;
case "restore":
// usually restore is an undo action, so we have to recreate the tiles and grid selection
if (this._lastSelectedSites) {
let selectedUrls = this._lastSelectedSites.map((site) => site.url);
// re-select the tiles once the tileGroup is done populating and arranging
tileGroup.addEventListener("arranged", function _onArranged(aEvent){
for (let url of selectedUrls) {
let tileNode = tileGroup.querySelector("richgriditem[value='"+url+"']");
if (tileNode) {
tileNode.setAttribute("selected", true);
}
}
tileGroup.removeEventListener("arranged", _onArranged, false);
// <sfoster> we can't just call selectItem n times on tileGroup as selecting means trigger the default action
// for seltype="single" grids.
// so we toggle the attributes and raise the selectionchange "manually"
let event = tileGroup.ownerDocument.createEvent("Events");
event.initEvent("selectionchange", true, true);
tileGroup.dispatchEvent(event);
}, false);
TopSites.restoreSites(this._lastSelectedSites);
// stop the appbar from dismissing,
// the selectionchange event will trigger re-population of the context appbar
aEvent.preventDefault();
}
break;
case "pin":
let pinIndices = [];
Array.forEach(selectedTiles, function(aNode) {
pinIndices.push( Array.indexOf(aNode.control.children, aNode) );
aNode.contextActions.delete('pin');
aNode.contextActions.add('unpin');
});
TopSites.pinSites(sites, pinIndices);
break;
case "unpin":
Array.forEach(selectedTiles, function(aNode) {
aNode.contextActions.delete('unpin');
aNode.contextActions.add('pin');
});
TopSites.unpinSites(sites);
break;
// default: no action
}
if (nextContextActions.size) {
// at next tick, re-populate the context appbar
setTimeout(function(){
// fire a MozContextActionsChange event to update the context appbar
let event = document.createEvent("Events");
event.actions = [...nextContextActions];
event.initEvent("MozContextActionsChange", true, false);
tileGroup.dispatchEvent(event);
},0);
}
},
handleEvent: function(aEvent) {
switch (aEvent.type){
case "MozAppbarDismissing":
// clean up when the context appbar is dismissed - we don't remember selections
this._lastSelectedSites = null;
}
},
update: function() {
// called by the NewTabUtils.allPages.update, notifying us of data-change in topsites
let grid = this._set,
dirtySites = TopSites.dirty();
if (dirtySites.size) {
// we can just do a partial update and refresh the node representing each dirty tile
for (let site of dirtySites) {
let tileNode = grid.querySelector("[value='"+site.url+"']");
if (tileNode) {
this.updateTile(tileNode, new Site(site));
}
}
} else {
// flush, recreate all
this.isUpdating = true;
// destroy and recreate all item nodes, skip calling arrangeItems
grid.clearAll(true);
this.populateGrid();
}
},
updateTile: function(aTileNode, aSite, aArrangeGrid) {
this._updateFavicon(aTileNode, Util.makeURI(aSite.url));
Task.spawn(function() {
let filepath = PageThumbsStorage.getFilePathForURL(aSite.url);
if (yield OS.File.exists(filepath)) {
aSite.backgroundImage = 'url("'+PageThumbs.getThumbnailURL(aSite.url)+'")';
aTileNode.setAttribute("customImage", aSite.backgroundImage);
if (aTileNode.refresh) {
aTileNode.refresh()
}
}
});
aSite.applyToTileNode(aTileNode);
if (aArrangeGrid) {
this._set.arrangeItems();
}
},
populateGrid: function populateGrid() {
this.isUpdating = true;
let sites = TopSites.getSites();
let length = Math.min(sites.length, this._topSitesMax || Infinity);
let tileset = this._set;
// if we're updating with a collection that is smaller than previous
// remove any extra tiles
while (tileset.children.length > length) {
tileset.removeChild(tileset.children[tileset.children.length -1]);
}
for (let idx=0; idx < length; idx++) {
let isNew = !tileset.children[idx],
site = sites[idx];
let item = isNew ? tileset.createItemElement(site.title, site.url) : tileset.children[idx];
this.updateTile(item, site);
if (isNew) {
tileset.appendChild(item);
}
}
tileset.arrangeItems();
this.isUpdating = false;
},
forceReloadOfThumbnail: function forceReloadOfThumbnail(url) {
let nodes = this._set.querySelectorAll('richgriditem[value="'+url+'"]');
for (let item of nodes) {
if ("isBound" in item && item.isBound) {
item.refreshBackgroundImage();
}
}
},
filterForThumbnailExpiration: function filterForThumbnailExpiration(aCallback) {
aCallback([item.getAttribute("value") for (item of this._set.children)]);
},
isFirstRun: function isFirstRun() {
return prefs.getBoolPref("browser.firstrun.show.localepicker");
},
destruct: function destruct() {
Services.obs.removeObserver(this, "Metro:RefreshTopsiteThumbnail");
Services.obs.removeObserver(this, "metro_viewstate_changed");
PageThumbs.removeExpirationFilter(this);
window.removeEventListener('MozAppbarDismissing', this, false);
},
// nsIObservers
observe: function (aSubject, aTopic, aState) {
switch(aTopic) {
case "Metro:RefreshTopsiteThumbnail":
this.forceReloadOfThumbnail(aState);
break;
case "metro_viewstate_changed":
this.onViewStateChange(aState);
for (let item of this._set.children) {
if (aState == "snapped") {
item.removeAttribute("tiletype");
} else {
item.setAttribute("tiletype", "thumbnail");
}
}
break;
}
},
// nsINavHistoryObserver
onBeginUpdateBatch: function() {
},
onEndUpdateBatch: function() {
},
onVisit: function(aURI, aVisitID, aTime, aSessionID,
aReferringID, aTransitionType) {
},
onTitleChanged: function(aURI, aPageTitle) {
},
onDeleteURI: function(aURI) {
},
onClearHistory: function() {
this._set.clearAll();
},
onPageChanged: function(aURI, aWhat, aValue) {
},
onDeleteVisits: function (aURI, aVisitTime, aGUID, aReason, aTransitionType) {
},
QueryInterface: function(iid) {
if (iid.equals(Components.interfaces.nsINavHistoryObserver) ||
iid.equals(Components.interfaces.nsISupports)) {
return this;
}
throw Cr.NS_ERROR_NO_INTERFACE;
}
});
let TopSitesStartView = {
_view: null,
get _grid() { return document.getElementById("start-topsites-grid"); },
init: function init() {
this._view = new TopSitesView(this._grid, 8);
if (this._view.isFirstRun()) {
let topsitesVbox = document.getElementById("start-topsites");
topsitesVbox.setAttribute("hidden", "true");
}
},
uninit: function uninit() {
this._view.destruct();
},
show: function show() {
this._grid.arrangeItems();
}
};

View File

@ -31,6 +31,14 @@ var Appbar = {
switch (aEvent.type) {
case 'URLChanged':
case 'TabSelect':
this.update();
// Switching away from or loading a site into a startui tab that has actions
// pending, we consider this confirmation that the user wants to flush changes.
if (this.activeTileset && aEvent.lastTab && aEvent.lastTab.browser.currentURI.spec == kStartURI) {
ContextUI.dismiss();
}
break;
case 'MozAppbarShowing':
this.update();
break;
@ -102,7 +110,7 @@ var Appbar = {
onMenuButton: function(aEvent) {
let typesArray = [];
if (!StartUI.isVisible)
if (!BrowserUI.isStartTabVisible)
typesArray.push("find-in-page");
if (ConsolePanelView.enabled)
typesArray.push("open-error-console");

View File

@ -77,32 +77,21 @@ var APZCObserver = {
let resolution = frameMetrics.resolution;
let compositedRect = frameMetrics.compositedRect;
if (StartUI.isStartPageVisible) {
let windowUtils = Browser.windowUtils;
Browser.selectedBrowser.contentWindow.scrollTo(scrollTo.x, scrollTo.y);
windowUtils.setResolution(resolution, resolution);
windowUtils.setDisplayPortForElement(displayPort.x * resolution,
displayPort.y * resolution,
displayPort.width * resolution,
displayPort.height * resolution,
Elements.startUI);
} else {
let windowUtils = Browser.selectedBrowser.contentWindow.
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils);
windowUtils.setScrollPositionClampingScrollPortSize(compositedRect.width,
compositedRect.height);
Browser.selectedBrowser.messageManager.sendAsyncMessage("Content:SetCacheViewport", {
scrollX: scrollTo.x,
scrollY: scrollTo.y,
x: displayPort.x + scrollTo.x,
y: displayPort.y + scrollTo.y,
w: displayPort.width,
h: displayPort.height,
scale: resolution,
id: scrollId
});
}
let windowUtils = Browser.selectedBrowser.contentWindow.
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils);
windowUtils.setScrollPositionClampingScrollPortSize(compositedRect.width,
compositedRect.height);
Browser.selectedBrowser.messageManager.sendAsyncMessage("Content:SetCacheViewport", {
scrollX: scrollTo.x,
scrollY: scrollTo.y,
x: displayPort.x + scrollTo.x,
y: displayPort.y + scrollTo.y,
w: displayPort.width,
h: displayPort.height,
scale: resolution,
id: scrollId
});
Util.dumpLn("APZC scrollId: " + scrollId);
Util.dumpLn("APZC scrollTo.x: " + scrollTo.x + ", scrollTo.y: " + scrollTo.y);

View File

@ -15,16 +15,15 @@
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="documenttab">
<content observes="bcast_urlbarState">
<content>
<xul:stack class="documenttab-container">
<html:canvas anonid="thumbnail-canvas" class="documenttab-thumbnail" />
<xul:image anonid="favicon" class="documenttab-favicon"
observes="bcast_urlbarState" width="26" height="26"/>
<xul:image anonid="favicon" class="documenttab-favicon" width="26" height="26"/>
<xul:label anonid="title" class="documenttab-title" bottom="0" start="0" end="0" crop="end"/>
<xul:box anonid="selection" class="documenttab-crop"/>
<xul:box anonid="selection" class="documenttab-selection"/>
<xul:button anonid="close" class="documenttab-close" observes="bcast_urlbarState" end="0" top="0"
<xul:button anonid="close" class="documenttab-close" end="0" top="0"
onclick="event.stopPropagation(); document.getBindingParent(this)._onClose()"
label="&closetab.label;"/>
</xul:stack>

View File

@ -272,7 +272,7 @@
<property name="isEditing" readonly="true">
<getter>
<![CDATA[
return Elements.urlbarState.getAttribute("mode") == "edit";
return Elements.urlbarState.hasAttribute("editing");
]]>
</getter>
</property>
@ -284,7 +284,7 @@
if (this.isEditing)
return;
Elements.urlbarState.setAttribute("mode", "edit");
Elements.urlbarState.setAttribute("editing", true);
this._lastKnownGoodURL = this.value;
if (!this.focused)
@ -306,7 +306,7 @@
if (!this.isEditing)
return;
Elements.urlbarState.setAttribute("mode", "view");
Elements.urlbarState.removeAttribute("editing");
this.closePopup();
this.formatValue();

View File

@ -65,372 +65,3 @@ var Bookmarks = {
});
}
};
/**
* Wraps a list/grid control implementing nsIDOMXULSelectControlElement and
* fills it with the user's bookmarks.
*
* @param aSet Control implementing nsIDOMXULSelectControlElement.
* @param {Number} aLimit Maximum number of items to show in the view.
* @param aRoot Bookmark root to show in the view.
*/
function BookmarksView(aSet, aLimit, aRoot, aFilterUnpinned) {
this._set = aSet;
this._set.controller = this;
this._inBatch = false; // batch up grid updates to avoid redundant arrangeItems calls
this._limit = aLimit;
this._filterUnpinned = aFilterUnpinned;
this._bookmarkService = PlacesUtils.bookmarks;
this._navHistoryService = gHistSvc;
this._changes = new BookmarkChangeListener(this);
this._pinHelper = new ItemPinHelper("metro.bookmarks.unpinned");
this._bookmarkService.addObserver(this._changes, false);
Services.obs.addObserver(this, "metro_viewstate_changed", false);
window.addEventListener('MozAppbarDismissing', this, false);
window.addEventListener('BookmarksNeedsRefresh', this, false);
this.root = aRoot;
}
BookmarksView.prototype = Util.extend(Object.create(View.prototype), {
_limit: null,
_set: null,
_changes: null,
_root: null,
_sort: 0, // Natural bookmark order.
_toRemove: null,
get sort() {
return this._sort;
},
set sort(aSort) {
this._sort = aSort;
this.clearBookmarks();
this.getBookmarks();
},
get root() {
return this._root;
},
set root(aRoot) {
this._root = aRoot;
},
handleItemClick: function bv_handleItemClick(aItem) {
let url = aItem.getAttribute("value");
BrowserUI.goToURI(url);
},
_getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) {
return this._set.querySelector("richgriditem[bookmarkId='" + aBookmarkId + "']");
},
_getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) {
return +aItem.getAttribute("bookmarkId");
},
_updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) {
for (let name in aAttrs)
anItem.setAttribute(name, aAttrs[name]);
},
getBookmarks: function bv_getBookmarks(aRefresh) {
let options = this._navHistoryService.getNewQueryOptions();
options.queryType = options.QUERY_TYPE_BOOKMARKS;
options.excludeQueries = true; // Don't include "smart folders"
options.sortingMode = this._sort;
let limit = this._limit || Infinity;
let query = this._navHistoryService.getNewQuery();
query.setFolders([Bookmarks.metroRoot], 1);
let result = this._navHistoryService.executeQuery(query, options);
let rootNode = result.root;
rootNode.containerOpen = true;
let childCount = rootNode.childCount;
this._inBatch = true; // batch up grid updates to avoid redundant arrangeItems calls
for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) {
let node = rootNode.getChild(i);
// Ignore folders, separators, undefined item types, etc.
if (node.type != node.RESULT_TYPE_URI)
continue;
// If item is marked for deletion, skip it.
if (this._toRemove && this._toRemove.indexOf(node.itemId) !== -1)
continue;
let item = this._getItemForBookmarkId(node.itemId);
// Item has been unpinned.
if (this._filterUnpinned && !this._pinHelper.isPinned(node.itemId)) {
if (item)
this.removeBookmark(node.itemId);
continue;
}
if (!aRefresh || !item) {
// If we're not refreshing or the item is not in the grid, add it.
this.addBookmark(node.itemId, addedCount);
} else if (aRefresh && item) {
// Update context action in case it changed in another view.
this._setContextActions(item);
}
addedCount++;
}
// Remove extra items in case a refresh added more than the limit.
// This can happen when undoing a delete.
if (aRefresh) {
while (this._set.itemCount > limit)
this._set.removeItemAt(this._set.itemCount - 1, true);
}
this._set.arrangeItems();
this._inBatch = false;
rootNode.containerOpen = false;
},
inCurrentView: function bv_inCurrentView(aParentId, aItemId) {
if (this._root && aParentId != this._root)
return false;
return !!this._getItemForBookmarkId(aItemId);
},
clearBookmarks: function bv_clearBookmarks() {
this._set.clearAll();
},
addBookmark: function bv_addBookmark(aBookmarkId, aPos) {
let index = this._bookmarkService.getItemIndex(aBookmarkId);
let uri = this._bookmarkService.getBookmarkURI(aBookmarkId);
let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec;
let item = this._set.insertItemAt(aPos || index, title, uri.spec, this._inBatch);
item.setAttribute("bookmarkId", aBookmarkId);
this._setContextActions(item);
this._updateFavicon(item, uri);
},
_setContextActions: function bv__setContextActions(aItem) {
let itemId = this._getBookmarkIdForItem(aItem);
aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(itemId) ? "unpin" : "pin"));
if (aItem.refresh) aItem.refresh();
},
_sendNeedsRefresh: function bv__sendNeedsRefresh(){
// Event sent when all view instances need to refresh.
let event = document.createEvent("Events");
event.initEvent("BookmarksNeedsRefresh", true, false);
window.dispatchEvent(event);
},
updateBookmark: function bv_updateBookmark(aBookmarkId) {
let item = this._getItemForBookmarkId(aBookmarkId);
if (!item)
return;
let oldIndex = this._set.getIndexOfItem(item);
let index = this._bookmarkService.getItemIndex(aBookmarkId);
if (oldIndex != index) {
this.removeBookmark(aBookmarkId);
this.addBookmark(aBookmarkId);
return;
}
let uri = this._bookmarkService.getBookmarkURI(aBookmarkId);
let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec;
item.setAttribute("value", uri.spec);
item.setAttribute("label", title);
this._updateFavicon(item, uri);
},
removeBookmark: function bv_removeBookmark(aBookmarkId) {
let item = this._getItemForBookmarkId(aBookmarkId);
let index = this._set.getIndexOfItem(item);
this._set.removeItemAt(index, this._inBatch);
},
destruct: function bv_destruct() {
this._bookmarkService.removeObserver(this._changes);
Services.obs.removeObserver(this, "metro_viewstate_changed");
window.removeEventListener('MozAppbarDismissing', this, false);
window.removeEventListener('BookmarksNeedsRefresh', this, false);
},
doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) {
let tileGroup = this._set;
let selectedTiles = tileGroup.selectedItems;
switch (aActionName){
case "delete":
Array.forEach(selectedTiles, function(aNode) {
if (!this._toRemove) {
this._toRemove = [];
}
let itemId = this._getBookmarkIdForItem(aNode);
this._toRemove.push(itemId);
this.removeBookmark(itemId);
}, this);
// stop the appbar from dismissing
aEvent.preventDefault();
// at next tick, re-populate the context appbar.
setTimeout(function(){
// fire a MozContextActionsChange event to update the context appbar
let event = document.createEvent("Events");
// we need the restore button to show (the tile node will go away though)
event.actions = ["restore"];
event.initEvent("MozContextActionsChange", true, false);
tileGroup.dispatchEvent(event);
}, 0);
break;
case "restore":
// clear toRemove and let _sendNeedsRefresh update the items.
this._toRemove = null;
break;
case "unpin":
Array.forEach(selectedTiles, function(aNode) {
let itemId = this._getBookmarkIdForItem(aNode);
if (this._filterUnpinned)
this.removeBookmark(itemId);
this._pinHelper.setUnpinned(itemId);
}, this);
break;
case "pin":
Array.forEach(selectedTiles, function(aNode) {
let itemId = this._getBookmarkIdForItem(aNode);
this._pinHelper.setPinned(itemId);
}, this);
break;
default:
return;
}
// Send refresh event so all view are in sync.
this._sendNeedsRefresh();
},
// nsIObservers
observe: function (aSubject, aTopic, aState) {
switch(aTopic) {
case "metro_viewstate_changed":
this.onViewStateChange(aState);
break;
}
},
handleEvent: function bv_handleEvent(aEvent) {
switch (aEvent.type){
case "MozAppbarDismissing":
// If undo wasn't pressed, time to do definitive actions.
if (this._toRemove) {
for (let bookmarkId of this._toRemove) {
this._bookmarkService.removeItem(bookmarkId);
}
this._toRemove = null;
}
break;
case "BookmarksNeedsRefresh":
this.getBookmarks(true);
break;
}
}
});
var BookmarksStartView = {
_view: null,
get _grid() { return document.getElementById("start-bookmarks-grid"); },
init: function init() {
this._view = new BookmarksView(this._grid, StartUI.maxResultsPerSection, Bookmarks.metroRoot, true);
this._view.getBookmarks();
},
uninit: function uninit() {
this._view.destruct();
},
show: function show() {
this._grid.arrangeItems();
}
};
/**
* Observes bookmark changes and keeps a linked BookmarksView updated.
*
* @param aView An instance of BookmarksView.
*/
function BookmarkChangeListener(aView) {
this._view = aView;
}
BookmarkChangeListener.prototype = {
//////////////////////////////////////////////////////////////////////////////
//// nsINavBookmarkObserver
onBeginUpdateBatch: function () { },
onEndUpdateBatch: function () { },
onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) {
this._view.getBookmarks(true);
},
onItemChanged: function bCL_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID) {
let itemIndex = PlacesUtils.bookmarks.getItemIndex(aItemId);
if (!this._view.inCurrentView(aParentId, aItemId))
return;
this._view.updateBookmark(aItemId);
},
onItemMoved: function bCL_onItemMoved(aItemId, aOldParentId, aOldIndex, aNewParentId, aNewIndex, aItemType, aGUID, aOldParentGUID, aNewParentGUID) {
let wasInView = this._view.inCurrentView(aOldParentId, aItemId);
let nowInView = this._view.inCurrentView(aNewParentId, aItemId);
if (!wasInView && nowInView)
this._view.addBookmark(aItemId);
if (wasInView && !nowInView)
this._view.removeBookmark(aItemId);
this._view.getBookmarks(true);
},
onBeforeItemRemoved: function (aItemId, aItemType, aParentId, aGUID, aParentGUID) { },
onItemRemoved: function bCL_onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) {
if (!this._view.inCurrentView(aParentId, aItemId))
return;
this._view.removeBookmark(aItemId);
this._view.getBookmarks(true);
},
onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { },
//////////////////////////////////////////////////////////////////////////////
//// nsISupports
QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
};

View File

@ -34,13 +34,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Promise",
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CrossSlide",
"resource:///modules/CrossSlide.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "View",
"resource:///modules/View.jsm");
/*
* Services
@ -123,13 +118,8 @@ let ScriptContexts = {};
["Bookmarks", "chrome://browser/content/bookmarks.js"],
["Downloads", "chrome://browser/content/downloads.js"],
["ConsolePanelView", "chrome://browser/content/console.js"],
["BookmarksStartView", "chrome://browser/content/bookmarks.js"],
["HistoryView", "chrome://browser/content/history.js"],
["HistoryStartView", "chrome://browser/content/history.js"],
["Site", "chrome://browser/content/Site.js"],
["TopSites", "chrome://browser/content/TopSites.js"],
["TopSitesView", "chrome://browser/content/TopSites.js"],
["TopSitesStartView", "chrome://browser/content/TopSites.js"],
["Sanitizer", "chrome://browser/content/sanitize.js"],
["SanitizeUI", "chrome://browser/content/sanitizeUI.js"],
["SSLExceptions", "chrome://browser/content/exceptions.js"],
@ -137,10 +127,6 @@ let ScriptContexts = {};
["NavButtonSlider", "chrome://browser/content/NavButtonSlider.js"],
["ContextUI", "chrome://browser/content/ContextUI.js"],
["FlyoutPanelsUI", "chrome://browser/content/flyoutpanels/FlyoutPanelsUI.js"],
#ifdef MOZ_SERVICES_SYNC
["RemoteTabsView", "chrome://browser/content/RemoteTabs.js"],
["RemoteTabsStartView", "chrome://browser/content/RemoteTabs.js"],
#endif
].forEach(function (aScript) {
let [name, script] = aScript;
XPCOMUtils.defineLazyGetter(window, name, function() {

View File

@ -11,9 +11,6 @@ Cu.import("resource://gre/modules/devtools/dbg-server.jsm")
* Constants
*/
// Page for which the start UI is shown
const kStartOverlayURI = "about:start";
// Devtools Messages
const debugServerStateChanged = "devtools.debugger.remote-enabled";
const debugServerPortChanged = "devtools.debugger.remote-port";
@ -33,6 +30,7 @@ let Elements = {};
[
["contentShowing", "bcast_contentShowing"],
["urlbarState", "bcast_urlbarState"],
["loadingState", "bcast_loadingState"],
["windowState", "bcast_windowState"],
["mainKeyset", "mainKeyset"],
["stack", "stack"],
@ -40,7 +38,6 @@ let Elements = {};
["tabs", "tabs-container"],
["controls", "browser-controls"],
["panelUI", "panel-container"],
["startUI", "start-container"],
["tray", "tray"],
["toolbar", "toolbar"],
["browsers", "browsers"],
@ -109,7 +106,6 @@ var BrowserUI = {
// Init core UI modules
ContextUI.init();
StartUI.init();
PanelUI.init();
FlyoutPanelsUI.init();
PageThumbs.init();
@ -184,7 +180,6 @@ var BrowserUI = {
messageManager.removeMessageListener("Browser:MozApplicationManifest", OfflineApps);
PanelUI.uninit();
StartUI.uninit();
Downloads.uninit();
SettingsCharm.uninit();
messageManager.removeMessageListener("Content:StateChange", this);
@ -231,7 +226,7 @@ var BrowserUI = {
},
showContent: function showContent(aURI) {
StartUI.update(aURI);
this.updateStartURIAttributes(aURI);
ContextUI.dismissTabs();
ContextUI.dismissContextAppbar();
FlyoutPanelsUI.hide();
@ -329,7 +324,7 @@ var BrowserUI = {
let flags = aFlags || 0;
if (!(flags & this.NO_STARTUI_VISIBILITY)) {
let uri = this.getDisplayURI(Browser.selectedBrowser);
StartUI.update(uri);
this.updateStartURIAttributes(uri);
}
this._updateButtons();
this._updateToolbar();
@ -342,6 +337,25 @@ var BrowserUI = {
this._edit.value = cleanURI;
},
get isStartTabVisible() {
return this.isStartURI();
},
isStartURI: function isStartURI(aURI) {
aURI = aURI || Browser.selectedBrowser.currentURI.spec;
return aURI == kStartURI;
},
updateStartURIAttributes: function (aURI) {
aURI = aURI || Browser.selectedBrowser.currentURI.spec;
if (this.isStartURI(aURI)) {
ContextUI.displayNavbar();
Elements.windowState.setAttribute("startpage", "true");
} else if (aURI != "about:blank") { // about:blank is loaded briefly for new tabs; ignore it
Elements.windowState.removeAttribute("startpage");
}
},
getDisplayURI: function(browser) {
let uri = browser.currentURI;
let spec = uri.spec;
@ -422,7 +436,7 @@ var BrowserUI = {
*/
newTab: function newTab(aURI, aOwner, aPeekTabs) {
aURI = aURI || kStartOverlayURI;
aURI = aURI || kStartURI;
if (aPeekTabs) {
ContextUI.peekTabs(kNewTabAnimationDelayMsec);
}
@ -690,13 +704,11 @@ var BrowserUI = {
},
_updateToolbar: function _updateToolbar() {
let mode = Elements.urlbarState.getAttribute("mode");
let isLoading = Browser.selectedTab.isLoading();
if (isLoading && mode != "loading")
Elements.urlbarState.setAttribute("mode", "loading");
else if (!isLoading && mode != "edit")
Elements.urlbarState.setAttribute("mode", "view");
if (Browser.selectedTab.isLoading()) {
Elements.loadingState.setAttribute("loading", true);
} else {
Elements.loadingState.removeAttribute("loading");
}
},
_closeOrQuit: function _closeOrQuit() {
@ -781,12 +793,6 @@ var BrowserUI = {
return;
}
if (StartUI.hide()) {
// When escaping from the start screen, hide the toolbar too.
ContextUI.dismiss();
return;
}
if (Browser.selectedTab.isLoading()) {
Browser.selectedBrowser.stop();
return;
@ -863,7 +869,6 @@ var BrowserUI = {
let referrerURI = null;
if (json.referrer)
referrerURI = Services.io.newURI(json.referrer, null, null);
//Browser.addTab(json.uri, json.bringFront, Browser.selectedTab, { referrerURI: referrerURI });
this.goToURI(json.uri);
break;
case "Content:StateChange":
@ -1113,139 +1118,6 @@ var BrowserUI = {
}
};
var StartUI = {
get isVisible() { return this.isStartPageVisible; },
get isStartPageVisible() { return Elements.windowState.hasAttribute("startpage"); },
get maxResultsPerSection() {
return Services.prefs.getIntPref("browser.display.startUI.maxresults");
},
sections: [
"TopSitesStartView",
"BookmarksStartView",
"HistoryStartView",
"RemoteTabsStartView"
],
init: function init() {
Elements.startUI.addEventListener("contextmenu", this, false);
Elements.startUI.addEventListener("click", this, false);
Elements.startUI.addEventListener("MozMousePixelScroll", this, false);
this.sections.forEach(function (sectionName) {
let section = window[sectionName];
if (section.init)
section.init();
});
},
uninit: function() {
this.sections.forEach(function (sectionName) {
let section = window[sectionName];
if (section.uninit)
section.uninit();
});
},
/** Show the Firefox start page / "new tab" page */
show: function show() {
if (this.isStartPageVisible)
return false;
ContextUI.displayNavbar();
Elements.contentShowing.setAttribute("disabled", "true");
Elements.windowState.setAttribute("startpage", "true");
this.sections.forEach(function (sectionName) {
let section = window[sectionName];
if (section.show)
section.show();
});
return true;
},
/** Hide the Firefox start page */
hide: function hide(aURI) {
aURI = aURI || Browser.selectedBrowser.currentURI.spec;
if (!this.isStartPageVisible || this.isStartURI(aURI))
return false;
Elements.contentShowing.removeAttribute("disabled");
Elements.windowState.removeAttribute("startpage");
return true;
},
/** Is the current tab supposed to show the Firefox start page? */
isStartURI: function isStartURI(aURI) {
aURI = aURI || Browser.selectedBrowser.currentURI.spec;
return aURI == kStartOverlayURI || aURI == "about:home";
},
/** Call this to show or hide the start page when switching tabs or pages */
update: function update(aURI) {
aURI = aURI || Browser.selectedBrowser.currentURI.spec;
if (this.isStartURI(aURI)) {
this.show();
} else if (aURI != "about:blank") { // about:blank is loaded briefly for new tabs; ignore it
this.hide(aURI);
}
},
onClick: function onClick(aEvent) {
// If someone clicks / taps in empty grid space, take away
// focus from the nav bar edit so the soft keyboard will hide.
if (BrowserUI.blurNavBar()) {
// Advanced notice to CAO, so we can shuffle the nav bar in advance
// of the keyboard transition.
ContentAreaObserver.navBarWillBlur();
}
if (aEvent.button == 0)
ContextUI.dismissTabs();
},
onNarrowTitleClick: function onNarrowTitleClick(sectionId) {
let section = document.getElementById(sectionId);
if (section.hasAttribute("expanded"))
return;
for (let expandedSection of Elements.startUI.querySelectorAll(".meta-section[expanded]"))
expandedSection.removeAttribute("expanded")
section.setAttribute("expanded", "true");
},
handleEvent: function handleEvent(aEvent) {
switch (aEvent.type) {
case "contextmenu":
let event = document.createEvent("Events");
event.initEvent("MozEdgeUICompleted", true, false);
window.dispatchEvent(event);
break;
case "click":
this.onClick(aEvent);
break;
case "MozMousePixelScroll":
let startBox = document.getElementById("start-scrollbox");
let [, scrollInterface] = ScrollUtils.getScrollboxFromElement(startBox);
if (Elements.windowState.getAttribute("viewstate") == "snapped") {
scrollInterface.scrollBy(0, aEvent.detail);
} else {
scrollInterface.scrollBy(aEvent.detail, 0);
}
aEvent.preventDefault();
aEvent.stopPropagation();
break;
}
}
};
var PanelUI = {
get _panels() { return document.getElementById("panel-items"); },

View File

@ -10,6 +10,9 @@ let Cr = Components.results;
Cu.import("resource://gre/modules/PageThumbs.jsm");
// Page for which the start UI is shown
const kStartURI = "about:start";
const kBrowserViewZoomLevelPrecision = 10000;
// allow panning after this timeout on pages with registered touch listeners
@ -22,6 +25,9 @@ const kTabThumbnailDelayCapture = 500;
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// See grid.xml, we use this to cache style info across loads of the startui.
var _richgridTileSizes = {};
// Override sizeToContent in the main window. It breaks things (bug 565887)
window.sizeToContent = function() {
Cu.reportError("window.sizeToContent is not allowed in this window");
@ -175,13 +181,10 @@ var Browser = {
let self = this;
function loadStartupURI() {
let uri = activationURI || commandURL || Browser.getHomePage();
if (StartUI.isStartURI(uri)) {
self.addTab(uri, true);
StartUI.show(); // This makes about:start load a lot faster
} else if (activationURI) {
self.addTab(uri, true, null, { flags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP });
if (activationURI) {
self.addTab(activationURI, true, null, { flags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP });
} else {
let uri = commandURL || Browser.getHomePage();
self.addTab(uri, true);
}
}
@ -191,9 +194,9 @@ var Browser = {
if (ss.shouldRestore() || Services.prefs.getBoolPref("browser.startup.sessionRestore")) {
let bringFront = false;
// First open any commandline URLs, except the homepage
if (activationURI && !StartUI.isStartURI(activationURI)) {
if (activationURI && activationURI != kStartURI) {
this.addTab(activationURI, true, null, { flags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP });
} else if (commandURL && !StartUI.isStartURI(commandURL)) {
} else if (commandURL && commandURL != kStartURI) {
this.addTab(commandURL, true);
} else {
bringFront = true;
@ -290,7 +293,7 @@ var Browser = {
getHomePage: function getHomePage(aOptions) {
aOptions = aOptions || { useDefault: false };
let url = "about:start";
let url = kStartURI;
try {
let prefs = aOptions.useDefault ? Services.prefs.getDefaultBranch(null) : Services.prefs;
url = prefs.getComplexValue("browser.startup.homepage", Ci.nsIPrefLocalizedString).data;
@ -560,9 +563,16 @@ var Browser = {
item.owner = null;
});
// tray tab
let event = document.createEvent("Events");
event.initEvent("TabClose", true, false);
aTab.chromeTab.dispatchEvent(event);
// tab window
event = document.createEvent("Events");
event.initEvent("TabClose", true, false);
aTab.browser.contentWindow.dispatchEvent(event);
aTab.browser.messageManager.sendAsyncMessage("Browser:TabClose");
let container = aTab.chromeTab.parentNode;

View File

@ -1,4 +1,4 @@
<?xml version="1.0"?>
<?xml version="1.0" encoding="Windows-1252" ?>
<!-- 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
@ -51,9 +51,10 @@
<script type="application/javascript" src="chrome://browser/content/apzc.js"/>
<broadcasterset id="broadcasterset">
<broadcaster id="bcast_contentShowing" disabled="false"/>
<broadcaster id="bcast_urlbarState" mode="view"/>
<broadcaster id="bcast_urlbarState" mode="editing"/>
<broadcaster id="bcast_preciseInput" input="precise"/>
<broadcaster id="bcast_windowState" viewstate=""/>
<broadcaster id="bcast_loadingState" loading="false"/>
</broadcasterset>
<observerset id="observerset">
@ -63,7 +64,7 @@
<commandset id="mainCommandSet">
<!-- basic navigation -->
<command id="cmd_back" disabled="true" oncommand="CommandUpdater.doCommand(this.id);"/>
<command id="cmd_forward" disabled="true" oncommand="CommandUpdater.doCommand(this.id);" observes="bcast_urlbarState"/>
<command id="cmd_forward" disabled="true" oncommand="CommandUpdater.doCommand(this.id);"/>
<command id="cmd_handleBackspace" oncommand="BrowserUI.handleBackspace();" />
<command id="cmd_handleShiftBackspace" oncommand="BrowserUI.handleShiftBackspace();" />
<command id="cmd_reload" oncommand="CommandUpdater.doCommand(this.id);"/>
@ -190,46 +191,6 @@
<toolbarbutton id="newtab-button" command="cmd_newTab" label="&newtab.label;"/>
</vbox>
</hbox>
<!-- Start UI -->
<hbox id="start-container" flex="1" observes="bcast_windowState" class="meta content-height content-width">
<!-- portrait/landscape/filled view -->
<scrollbox id="start-scrollbox" observes="bcast_preciseInput" flex="1">
<vbox id="start-topsites" class="meta-section" expanded="true">
<label class="meta-section-title wide-title" value="&topSitesHeader.label;"/>
<html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-topsites')">
&narrowTopSitesHeader.label;
</html:div>
<richgrid id="start-topsites-grid" set-name="topSites" rows="3" columns="3" tiletype="thumbnail" seltype="multiple" flex="1"/>
</vbox>
<vbox id="start-bookmarks" class="meta-section">
<label class="meta-section-title wide-title" value="&bookmarksHeader.label;"/>
<html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-bookmarks')">
&narrowBookmarksHeader.label;
</html:div>
<richgrid id="start-bookmarks-grid" set-name="bookmarks" seltype="multiple" flex="1"/>
</vbox>
<vbox id="start-history" class="meta-section">
<label class="meta-section-title wide-title" value="&recentHistoryHeader.label;"/>
<html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-history')">
&narrowRecentHistoryHeader.label;
</html:div>
<richgrid id="start-history-grid" set-name="recentHistory" seltype="multiple" flex="1"/>
</vbox>
<vbox id="start-remotetabs" class="meta-section">
<label class="meta-section-title wide-title" value="&remoteTabsHeader.label;"/>
<html:div id="snappedRemoteTabsLabel" class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-remotetabs')">
&narrowRemoteTabsHeader.label;
</html:div>
<richgrid id="start-remotetabs-grid" set-name="remoteTabs" seltype="multiple" flex="1"/>
</vbox>
</scrollbox>
</hbox>
</vbox> <!-- end tray -->
<!-- Content viewport -->
@ -267,7 +228,9 @@
<toolbarbutton id="forward-button" class="appbar-primary"
command="cmd_forward"/>
<hbox id="urlbar" flex="1" observes="bcast_urlbarState">
<hbox id="urlbar" flex="1">
<observes element="bcast_urlbarState" attribute="*"/>
<observes element="bcast_loadingState" attribute="*"/>
<box id="identity-box" align="center" role="button">
<image id="identity-icon"/>
</box>
@ -291,7 +254,6 @@
<stack id="toolbar-contextual">
<observes element="bcast_windowState" attribute="*"/>
<observes element="bcast_urlbarState" attribute="*"/>
<hbox id="toolbar-context-page" pack="end">
<circularprogressindicator id="download-progress"
oncommand="Appbar.onDownloadButton()"/>

View File

@ -107,7 +107,7 @@ var FindHelperUI = {
},
show: function findHelperShow() {
if (StartUI.isVisible || this._open)
if (BrowserUI.isStartTabVisible || this._open)
return;
// Hide any menus

View File

@ -239,7 +239,6 @@ var TouchModule = {
// Don't allow kinetic panning if APZC is enabled and the pan element is the deck
let deck = document.getElementById("browsers");
if (Services.prefs.getBoolPref(kAsyncPanZoomEnabled) &&
!StartUI.isStartPageVisible &&
this._targetScrollbox == deck) {
return;
}
@ -364,11 +363,10 @@ var TouchModule = {
if (Date.now() - this._dragStartTime > kStopKineticPanOnDragTimeout)
this._kinetic._velocity.set(0, 0);
// Start kinetic pan if we i) aren't using async pan zoom or ii) if we
// are on the start page, iii) If the scroll element is not browsers
// Start kinetic pan if we aren't using async pan zoom or the scroll
// element is not browsers.
let deck = document.getElementById("browsers");
if (!Services.prefs.getBoolPref(kAsyncPanZoomEnabled) ||
StartUI.isStartPageVisible ||
this._targetScrollbox != deck) {
this._kinetic.start();
}
@ -449,8 +447,10 @@ var ScrollUtils = {
getScrollboxFromElement: function getScrollboxFromElement(elem) {
let scrollbox = null;
let qinterface = null;
// if element is content, get the browser scroll interface
if (elem.ownerDocument == Browser.selectedBrowser.contentDocument) {
// if element is content (but not the startui page), get the browser scroll interface
if (!BrowserUI.isStartTabVisible &&
elem.ownerDocument == Browser.selectedBrowser.contentDocument) {
elem = Browser.selectedBrowser;
}
for (; elem; elem = elem.parentNode) {

View File

@ -0,0 +1,385 @@
/* 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";
/**
* Wraps a list/grid control implementing nsIDOMXULSelectControlElement and
* fills it with the user's bookmarks.
*
* @param aSet Control implementing nsIDOMXULSelectControlElement.
* @param {Number} aLimit Maximum number of items to show in the view.
* @param aRoot Bookmark root to show in the view.
*/
function BookmarksView(aSet, aLimit, aRoot, aFilterUnpinned) {
this._set = aSet;
this._set.controller = this;
this._inBatch = false; // batch up grid updates to avoid redundant arrangeItems calls
this._limit = aLimit;
this._filterUnpinned = aFilterUnpinned;
this._bookmarkService = PlacesUtils.bookmarks;
this._navHistoryService = gHistSvc;
this._changes = new BookmarkChangeListener(this);
this._pinHelper = new ItemPinHelper("metro.bookmarks.unpinned");
this._bookmarkService.addObserver(this._changes, false);
Services.obs.addObserver(this, "metro_viewstate_changed", false);
StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false);
StartUI.chromeWin.addEventListener('BookmarksNeedsRefresh', this, false);
window.addEventListener("TabClose", this, true);
this.root = aRoot;
}
BookmarksView.prototype = Util.extend(Object.create(View.prototype), {
_limit: null,
_set: null,
_changes: null,
_root: null,
_sort: 0, // Natural bookmark order.
_toRemove: null,
get sort() {
return this._sort;
},
set sort(aSort) {
this._sort = aSort;
this.clearBookmarks();
this.getBookmarks();
},
get root() {
return this._root;
},
set root(aRoot) {
this._root = aRoot;
},
destruct: function bv_destruct() {
this._bookmarkService.removeObserver(this._changes);
Services.obs.removeObserver(this, "metro_viewstate_changed");
if (StartUI.chromeWin) {
StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false);
StartUI.chromeWin.removeEventListener('BookmarksNeedsRefresh', this, false);
}
},
handleItemClick: function bv_handleItemClick(aItem) {
let url = aItem.getAttribute("value");
StartUI.goToURI(url);
},
_getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) {
return this._set.querySelector("richgriditem[bookmarkId='" + aBookmarkId + "']");
},
_getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) {
return +aItem.getAttribute("bookmarkId");
},
_updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) {
for (let name in aAttrs)
anItem.setAttribute(name, aAttrs[name]);
},
getBookmarks: function bv_getBookmarks(aRefresh) {
let options = this._navHistoryService.getNewQueryOptions();
options.queryType = options.QUERY_TYPE_BOOKMARKS;
options.excludeQueries = true; // Don't include "smart folders"
options.sortingMode = this._sort;
let limit = this._limit || Infinity;
let query = this._navHistoryService.getNewQuery();
query.setFolders([Bookmarks.metroRoot], 1);
let result = this._navHistoryService.executeQuery(query, options);
let rootNode = result.root;
rootNode.containerOpen = true;
let childCount = rootNode.childCount;
this._inBatch = true; // batch up grid updates to avoid redundant arrangeItems calls
for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) {
let node = rootNode.getChild(i);
// Ignore folders, separators, undefined item types, etc.
if (node.type != node.RESULT_TYPE_URI)
continue;
// If item is marked for deletion, skip it.
if (this._toRemove && this._toRemove.indexOf(node.itemId) !== -1)
continue;
let item = this._getItemForBookmarkId(node.itemId);
// Item has been unpinned.
if (this._filterUnpinned && !this._pinHelper.isPinned(node.itemId)) {
if (item)
this.removeBookmark(node.itemId);
continue;
}
if (!aRefresh || !item) {
// If we're not refreshing or the item is not in the grid, add it.
this.addBookmark(node.itemId, addedCount);
} else if (aRefresh && item) {
// Update context action in case it changed in another view.
this._setContextActions(item);
}
addedCount++;
}
// Remove extra items in case a refresh added more than the limit.
// This can happen when undoing a delete.
if (aRefresh) {
while (this._set.itemCount > limit)
this._set.removeItemAt(this._set.itemCount - 1, true);
}
this._set.arrangeItems();
this._inBatch = false;
rootNode.containerOpen = false;
},
inCurrentView: function bv_inCurrentView(aParentId, aItemId) {
if (this._root && aParentId != this._root)
return false;
return !!this._getItemForBookmarkId(aItemId);
},
clearBookmarks: function bv_clearBookmarks() {
this._set.clearAll();
},
addBookmark: function bv_addBookmark(aBookmarkId, aPos) {
let index = this._bookmarkService.getItemIndex(aBookmarkId);
let uri = this._bookmarkService.getBookmarkURI(aBookmarkId);
let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec;
let item = this._set.insertItemAt(aPos || index, title, uri.spec, this._inBatch);
item.setAttribute("bookmarkId", aBookmarkId);
this._setContextActions(item);
this._updateFavicon(item, uri);
},
_setContextActions: function bv__setContextActions(aItem) {
let itemId = this._getBookmarkIdForItem(aItem);
aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(itemId) ? "unpin" : "pin"));
if (aItem.refresh) aItem.refresh();
},
_sendNeedsRefresh: function bv__sendNeedsRefresh(){
// Event sent when all view instances need to refresh.
let event = document.createEvent("Events");
event.initEvent("BookmarksNeedsRefresh", true, false);
window.dispatchEvent(event);
},
updateBookmark: function bv_updateBookmark(aBookmarkId) {
let item = this._getItemForBookmarkId(aBookmarkId);
if (!item)
return;
let oldIndex = this._set.getIndexOfItem(item);
let index = this._bookmarkService.getItemIndex(aBookmarkId);
if (oldIndex != index) {
this.removeBookmark(aBookmarkId);
this.addBookmark(aBookmarkId);
return;
}
let uri = this._bookmarkService.getBookmarkURI(aBookmarkId);
let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec;
item.setAttribute("value", uri.spec);
item.setAttribute("label", title);
this._updateFavicon(item, uri);
},
removeBookmark: function bv_removeBookmark(aBookmarkId) {
let item = this._getItemForBookmarkId(aBookmarkId);
let index = this._set.getIndexOfItem(item);
this._set.removeItemAt(index, this._inBatch);
},
doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) {
let tileGroup = this._set;
let selectedTiles = tileGroup.selectedItems;
switch (aActionName){
case "delete":
Array.forEach(selectedTiles, function(aNode) {
if (!this._toRemove) {
this._toRemove = [];
}
let itemId = this._getBookmarkIdForItem(aNode);
this._toRemove.push(itemId);
this.removeBookmark(itemId);
}, this);
// stop the appbar from dismissing
aEvent.preventDefault();
// at next tick, re-populate the context appbar.
setTimeout(function(){
// fire a MozContextActionsChange event to update the context appbar
let event = document.createEvent("Events");
// we need the restore button to show (the tile node will go away though)
event.actions = ["restore"];
event.initEvent("MozContextActionsChange", true, false);
tileGroup.dispatchEvent(event);
}, 0);
break;
case "restore":
// clear toRemove and let _sendNeedsRefresh update the items.
this._toRemove = null;
break;
case "unpin":
Array.forEach(selectedTiles, function(aNode) {
let itemId = this._getBookmarkIdForItem(aNode);
if (this._filterUnpinned)
this.removeBookmark(itemId);
this._pinHelper.setUnpinned(itemId);
}, this);
break;
case "pin":
Array.forEach(selectedTiles, function(aNode) {
let itemId = this._getBookmarkIdForItem(aNode);
this._pinHelper.setPinned(itemId);
}, this);
break;
default:
return;
}
// Send refresh event so all view are in sync.
this._sendNeedsRefresh();
},
// nsIObservers
observe: function (aSubject, aTopic, aState) {
switch(aTopic) {
case "metro_viewstate_changed":
this.onViewStateChange(aState);
break;
}
},
handleEvent: function bv_handleEvent(aEvent) {
switch (aEvent.type){
case "MozAppbarDismissing":
// If undo wasn't pressed, time to do definitive actions.
if (this._toRemove) {
for (let bookmarkId of this._toRemove) {
this._bookmarkService.removeItem(bookmarkId);
}
this._toRemove = null;
}
break;
case "BookmarksNeedsRefresh":
this.getBookmarks(true);
break;
case "TabClose":
// Flush any pending actions - appbar will call us back
// before this returns with 'MozAppbarDismissing' above.
StartUI.chromeWin.ContextUI.dismiss();
break;
}
}
});
let BookmarksStartView = {
_view: null,
get _grid() { return document.getElementById("start-bookmarks-grid"); },
init: function init() {
this._view = new BookmarksView(this._grid, StartUI.maxResultsPerSection, Bookmarks.metroRoot, true);
this._view.getBookmarks();
},
uninit: function uninit() {
if (this._view) {
this._view.destruct();
}
},
show: function show() {
this._grid.arrangeItems();
}
};
/**
* Observes bookmark changes and keeps a linked BookmarksView updated.
*
* @param aView An instance of BookmarksView.
*/
function BookmarkChangeListener(aView) {
this._view = aView;
}
BookmarkChangeListener.prototype = {
//////////////////////////////////////////////////////////////////////////////
//// nsINavBookmarkObserver
onBeginUpdateBatch: function () { },
onEndUpdateBatch: function () { },
onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) {
this._view.getBookmarks(true);
},
onItemChanged: function bCL_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID) {
let itemIndex = PlacesUtils.bookmarks.getItemIndex(aItemId);
if (!this._view.inCurrentView(aParentId, aItemId))
return;
this._view.updateBookmark(aItemId);
},
onItemMoved: function bCL_onItemMoved(aItemId, aOldParentId, aOldIndex, aNewParentId, aNewIndex, aItemType, aGUID, aOldParentGUID, aNewParentGUID) {
let wasInView = this._view.inCurrentView(aOldParentId, aItemId);
let nowInView = this._view.inCurrentView(aNewParentId, aItemId);
if (!wasInView && nowInView)
this._view.addBookmark(aItemId);
if (wasInView && !nowInView)
this._view.removeBookmark(aItemId);
this._view.getBookmarks(true);
},
onBeforeItemRemoved: function (aItemId, aItemType, aParentId, aGUID, aParentGUID) { },
onItemRemoved: function bCL_onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) {
if (!this._view.inCurrentView(aParentId, aItemId))
return;
this._view.removeBookmark(aItemId);
this._view.getBookmarks(true);
},
onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { },
//////////////////////////////////////////////////////////////////////////////
//// nsISupports
QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
};

View File

@ -17,17 +17,27 @@ function HistoryView(aSet, aLimit, aFilterUnpinned) {
this._pinHelper = new ItemPinHelper("metro.history.unpinned");
this._historyService.addObserver(this, false);
Services.obs.addObserver(this, "metro_viewstate_changed", false);
window.addEventListener('MozAppbarDismissing', this, false);
window.addEventListener('HistoryNeedsRefresh', this, false);
StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false);
StartUI.chromeWin.addEventListener('HistoryNeedsRefresh', this, false);
window.addEventListener("TabClose", this, true);
}
HistoryView.prototype = Util.extend(Object.create(View.prototype), {
_set: null,
_toRemove: null,
destruct: function destruct() {
this._historyService.removeObserver(this);
Services.obs.removeObserver(this, "metro_viewstate_changed");
if (StartUI.chromeWin) {
StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false);
StartUI.chromeWin.removeEventListener('HistoryNeedsRefresh', this, false);
}
},
handleItemClick: function tabview_handleItemClick(aItem) {
let url = aItem.getAttribute("value");
BrowserUI.goToURI(url);
StartUI.goToURI(url);
},
populateGrid: function populateGrid(aRefresh) {
@ -90,13 +100,6 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), {
this._inBatch--;
},
destruct: function destruct() {
this._historyService.removeObserver(this);
Services.obs.removeObserver(this, "metro_viewstate_changed");
window.removeEventListener('MozAppbarDismissing', this, false);
window.removeEventListener('HistoryNeedsRefresh', this, false);
},
addItemToSet: function addItemToSet(aURI, aTitle, aIcon, aPos) {
let item = this._set.insertItemAt(aPos || 0, aTitle, aURI, this._inBatch);
this._setContextActions(item);
@ -208,6 +211,12 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), {
case "HistoryNeedsRefresh":
this.populateGrid(true);
break;
case "TabClose":
// Flush any pending actions - appbar will call us back
// before this returns with 'MozAppbarDismissing' above.
StartUI.chromeWin.ContextUI.dismiss();
break;
}
},
@ -298,6 +307,8 @@ let HistoryStartView = {
},
uninit: function uninit() {
this._view.destruct();
if (this._view) {
this._view.destruct();
}
}
};

View File

@ -45,7 +45,7 @@ RemoteTabsView.prototype = Util.extend(Object.create(View.prototype), {
handleItemClick: function tabview_handleItemClick(aItem) {
let url = aItem.getAttribute("value");
BrowserUI.goToURI(url);
StartUI.goToURI(url);
},
observe: function(subject, topic, data) {
@ -122,7 +122,9 @@ let RemoteTabsStartView = {
},
uninit: function uninit() {
this._view.destruct();
if (this._view) {
this._view.destruct();
}
},
show: function show() {

View File

@ -0,0 +1,78 @@
<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<?xml-stylesheet href="chrome://browser/skin/platform.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/tiles.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
%brandDTD;
#ifdef MOZ_SERVICES_SYNC
<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
%syncBrandDTD;
<!ENTITY % syncDTD SYSTEM "chrome://browser/locale/sync.dtd">
%syncDTD;
#endif
]>
<page id="startui-page"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
onload="StartUI.init();"
onunload="StartUI.uninit();">
<script type="application/javascript" src="chrome://browser/content/startui-scripts.js"/>
<!-- mimic broadcasts in browser.xul. note browser.xul broadcasters do not propagate down here! -->
<broadcasterset id="broadcasterset">
<broadcaster id="bcast_preciseInput" input="precise"/>
<broadcaster id="bcast_windowState" viewstate=""/>
</broadcasterset>
<hbox id="start-container" flex="1" observes="bcast_windowState" class="meta content-height content-width">
<scrollbox id="start-scrollbox" observes="bcast_preciseInput" flex="1">
<vbox id="start-topsites" class="meta-section" expanded="true">
<label class="meta-section-title wide-title" value="&topSitesHeader.label;"/>
<html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-topsites')">
&narrowTopSitesHeader.label;
</html:div>
<richgrid id="start-topsites-grid" set-name="topSites" rows="3" columns="3" tiletype="thumbnail" seltype="multiple" flex="1"/>
</vbox>
<vbox id="start-bookmarks" class="meta-section">
<label class="meta-section-title wide-title" value="&bookmarksHeader.label;"/>
<html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-bookmarks')">
&narrowBookmarksHeader.label;
</html:div>
<richgrid id="start-bookmarks-grid" set-name="bookmarks" seltype="multiple" flex="1"/>
</vbox>
<vbox id="start-history" class="meta-section">
<label class="meta-section-title wide-title" value="&recentHistoryHeader.label;"/>
<html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-history')">
&narrowRecentHistoryHeader.label;
</html:div>
<richgrid id="start-history-grid" set-name="recentHistory" seltype="multiple" flex="1"/>
</vbox>
#ifdef MOZ_SERVICES_SYNC
<vbox id="start-remotetabs" class="meta-section">
<label class="meta-section-title wide-title" value="&remoteTabsHeader.label;"/>
<html:div id="snappedRemoteTabsLabel" class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-remotetabs')">
&narrowRemoteTabsHeader.label;
</html:div>
<richgrid id="start-remotetabs-grid" set-name="remoteTabs" seltype="multiple" flex="1"/>
</vbox>
#endif
</scrollbox>
</hbox>
</page>

View File

@ -0,0 +1,151 @@
/* 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";
Cu.import("resource://gre/modules/Services.jsm");
var StartUI = {
get startUI() { return document.getElementById("start-container"); },
get maxResultsPerSection() {
return Services.prefs.getIntPref("browser.display.startUI.maxresults");
},
get chromeWin() {
// XXX Not e10s friendly. We use this in a few places.
return Services.wm.getMostRecentWindow('navigator:browser');
},
init: function init() {
this.startUI.addEventListener("click", this, false);
this.startUI.addEventListener("MozMousePixelScroll", this, false);
// Update the input type on our local broadcaster
document.getElementById("bcast_preciseInput").setAttribute("input",
this.chromeWin.InputSourceHelper.isPrecise ? "precise" : "imprecise");
TopSitesStartView.init();
BookmarksStartView.init();
HistoryStartView.init();
RemoteTabsStartView.init();
TopSitesStartView.show();
BookmarksStartView.show();
HistoryStartView.show();
RemoteTabsStartView.show();
this.chromeWin.addEventListener("MozPrecisePointer", this, true);
this.chromeWin.addEventListener("MozImprecisePointer", this, true);
Services.obs.addObserver(this, "metro_viewstate_changed", false);
},
uninit: function() {
if (TopSitesStartView)
TopSitesStartView.uninit();
if (BookmarksStartView)
BookmarksStartView.uninit();
if (HistoryStartView)
HistoryStartView.uninit();
if (RemoteTabsStartView)
RemoteTabsStartView.uninit();
if (this.chromeWin) {
this.chromeWin.removeEventListener("MozPrecisePointer", this, true);
this.chromeWin.removeEventListener("MozImprecisePointer", this, true);
}
Services.obs.removeObserver(this, "metro_viewstate_changed");
},
goToURI: function (aURI) {
this.chromeWin.BrowserUI.goToURI(aURI);
},
onClick: function onClick(aEvent) {
// If someone clicks / taps in empty grid space, take away
// focus from the nav bar edit so the soft keyboard will hide.
if (this.chromeWin.BrowserUI.blurNavBar()) {
// Advanced notice to CAO, so we can shuffle the nav bar in advance
// of the keyboard transition.
this.chromeWin.ContentAreaObserver.navBarWillBlur();
}
if (aEvent.button == 0) {
this.chromeWin.ContextUI.dismissTabs();
}
},
onNarrowTitleClick: function onNarrowTitleClick(sectionId) {
let section = document.getElementById(sectionId);
if (section.hasAttribute("expanded"))
return;
for (let expandedSection of this.startUI.querySelectorAll(".meta-section[expanded]"))
expandedSection.removeAttribute("expanded")
section.setAttribute("expanded", "true");
},
getScrollBoxObject: function () {
let startBox = document.getElementById("start-scrollbox");
if (!startBox._cachedSBO) {
startBox._cachedSBO = startBox.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
}
return startBox._cachedSBO;
},
handleEvent: function handleEvent(aEvent) {
switch (aEvent.type) {
case "MozPrecisePointer":
document.getElementById("bcast_preciseInput").setAttribute("input", "precise");
break;
case "MozImprecisePointer":
document.getElementById("bcast_preciseInput").setAttribute("input", "imprecise");
break;
case "click":
this.onClick(aEvent);
break;
case "MozMousePixelScroll":
let scroller = this.getScrollBoxObject();
if (this.startUI.getAttribute("viewstate") == "snapped") {
scroller.scrollBy(0, aEvent.detail);
} else {
scroller.scrollBy(aEvent.detail, 0);
}
aEvent.preventDefault();
aEvent.stopPropagation();
break;
}
},
_adjustDOMforViewState: function() {
if (this.chromeWin.MetroUtils.immersive) {
let currViewState = "";
switch (this.chromeWin.MetroUtils.snappedState) {
case Ci.nsIWinMetroUtils.fullScreenLandscape:
currViewState = "landscape";
break;
case Ci.nsIWinMetroUtils.fullScreenPortrait:
currViewState = "portrait";
break;
case Ci.nsIWinMetroUtils.filled:
currViewState = "filled";
break;
case Ci.nsIWinMetroUtils.snapped:
currViewState = "snapped";
break;
}
document.getElementById("bcast_windowState").setAttribute("viewstate", currViewState);
}
},
observe: function (aSubject, aTopic, aData) {
switch (aTopic) {
case "metro_viewstate_changed":
this._adjustDOMforViewState();
break;
}
}
};

View File

@ -0,0 +1,304 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict"
let prefs = Components.classes["@mozilla.org/preferences-service;1"].
getService(Components.interfaces.nsIPrefBranch);
Cu.import("resource://gre/modules/PageThumbs.jsm");
Cu.import("resource:///modules/colorUtils.jsm");
function TopSitesView(aGrid, aMaxSites) {
this._set = aGrid;
this._set.controller = this;
this._topSitesMax = aMaxSites;
// clean up state when the appbar closes
StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false);
let history = Cc["@mozilla.org/browser/nav-history-service;1"].
getService(Ci.nsINavHistoryService);
history.addObserver(this, false);
PageThumbs.addExpirationFilter(this);
Services.obs.addObserver(this, "Metro:RefreshTopsiteThumbnail", false);
Services.obs.addObserver(this, "metro_viewstate_changed", false);
NewTabUtils.allPages.register(this);
TopSites.prepareCache().then(function(){
this.populateGrid();
}.bind(this));
}
TopSitesView.prototype = Util.extend(Object.create(View.prototype), {
_set:null,
_topSitesMax: null,
// _lastSelectedSites used to temporarily store blocked/removed sites for undo/restore-ing
_lastSelectedSites: null,
// isUpdating used only for testing currently
isUpdating: false,
destruct: function destruct() {
Services.obs.removeObserver(this, "Metro:RefreshTopsiteThumbnail");
Services.obs.removeObserver(this, "metro_viewstate_changed");
PageThumbs.removeExpirationFilter(this);
NewTabUtils.allPages.unregister(this);
if (StartUI.chromeWin) {
StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false);
}
},
handleItemClick: function tabview_handleItemClick(aItem) {
let url = aItem.getAttribute("value");
StartUI.goToURI(url);
},
doActionOnSelectedTiles: function(aActionName, aEvent) {
let tileGroup = this._set;
let selectedTiles = tileGroup.selectedItems;
let sites = Array.map(selectedTiles, TopSites._linkFromNode);
let nextContextActions = new Set();
switch (aActionName){
case "delete":
for (let aNode of selectedTiles) {
// add some class to transition element before deletion?
aNode.contextActions.delete('delete');
// we need new context buttons to show (the tile node will go away though)
}
this._lastSelectedSites = (this._lastSelectedSites || []).concat(sites);
// stop the appbar from dismissing
aEvent.preventDefault();
nextContextActions.add('restore');
TopSites.hideSites(sites);
break;
case "restore":
// usually restore is an undo action, so we have to recreate the tiles and grid selection
if (this._lastSelectedSites) {
let selectedUrls = this._lastSelectedSites.map((site) => site.url);
// re-select the tiles once the tileGroup is done populating and arranging
tileGroup.addEventListener("arranged", function _onArranged(aEvent){
for (let url of selectedUrls) {
let tileNode = tileGroup.querySelector("richgriditem[value='"+url+"']");
if (tileNode) {
tileNode.setAttribute("selected", true);
}
}
tileGroup.removeEventListener("arranged", _onArranged, false);
// <sfoster> we can't just call selectItem n times on tileGroup as selecting means trigger the default action
// for seltype="single" grids.
// so we toggle the attributes and raise the selectionchange "manually"
let event = tileGroup.ownerDocument.createEvent("Events");
event.initEvent("selectionchange", true, true);
tileGroup.dispatchEvent(event);
}, false);
TopSites.restoreSites(this._lastSelectedSites);
// stop the appbar from dismissing,
// the selectionchange event will trigger re-population of the context appbar
aEvent.preventDefault();
}
break;
case "pin":
let pinIndices = [];
Array.forEach(selectedTiles, function(aNode) {
pinIndices.push( Array.indexOf(aNode.control.children, aNode) );
aNode.contextActions.delete('pin');
aNode.contextActions.add('unpin');
});
TopSites.pinSites(sites, pinIndices);
break;
case "unpin":
Array.forEach(selectedTiles, function(aNode) {
aNode.contextActions.delete('unpin');
aNode.contextActions.add('pin');
});
TopSites.unpinSites(sites);
break;
// default: no action
}
if (nextContextActions.size) {
// at next tick, re-populate the context appbar
setTimeout(function(){
// fire a MozContextActionsChange event to update the context appbar
let event = document.createEvent("Events");
event.actions = [...nextContextActions];
event.initEvent("MozContextActionsChange", true, false);
tileGroup.dispatchEvent(event);
},0);
}
},
handleEvent: function(aEvent) {
switch (aEvent.type){
case "MozAppbarDismissing":
// clean up when the context appbar is dismissed - we don't remember selections
this._lastSelectedSites = null;
}
},
update: function() {
// called by the NewTabUtils.allPages.update, notifying us of data-change in topsites
let grid = this._set,
dirtySites = TopSites.dirty();
if (dirtySites.size) {
// we can just do a partial update and refresh the node representing each dirty tile
for (let site of dirtySites) {
let tileNode = grid.querySelector("[value='"+site.url+"']");
if (tileNode) {
this.updateTile(tileNode, new Site(site));
}
}
} else {
// flush, recreate all
this.isUpdating = true;
// destroy and recreate all item nodes, skip calling arrangeItems
grid.clearAll(true);
this.populateGrid();
}
},
updateTile: function(aTileNode, aSite, aArrangeGrid) {
this._updateFavicon(aTileNode, Util.makeURI(aSite.url));
Task.spawn(function() {
let filepath = PageThumbsStorage.getFilePathForURL(aSite.url);
if (yield OS.File.exists(filepath)) {
aSite.backgroundImage = 'url("'+PageThumbs.getThumbnailURL(aSite.url)+'")';
aTileNode.setAttribute("customImage", aSite.backgroundImage);
if (aTileNode.refresh) {
aTileNode.refresh()
}
}
});
aSite.applyToTileNode(aTileNode);
if (aArrangeGrid) {
this._set.arrangeItems();
}
},
populateGrid: function populateGrid() {
this.isUpdating = true;
let sites = TopSites.getSites();
let length = Math.min(sites.length, this._topSitesMax || Infinity);
let tileset = this._set;
// if we're updating with a collection that is smaller than previous
// remove any extra tiles
while (tileset.children.length > length) {
tileset.removeChild(tileset.children[tileset.children.length -1]);
}
for (let idx=0; idx < length; idx++) {
let isNew = !tileset.children[idx],
site = sites[idx];
let item = isNew ? tileset.createItemElement(site.title, site.url) : tileset.children[idx];
this.updateTile(item, site);
if (isNew) {
tileset.appendChild(item);
}
}
tileset.arrangeItems();
this.isUpdating = false;
},
forceReloadOfThumbnail: function forceReloadOfThumbnail(url) {
let nodes = this._set.querySelectorAll('richgriditem[value="'+url+'"]');
for (let item of nodes) {
if ("isBound" in item && item.isBound) {
item.refreshBackgroundImage();
}
}
},
filterForThumbnailExpiration: function filterForThumbnailExpiration(aCallback) {
aCallback([item.getAttribute("value") for (item of this._set.children)]);
},
isFirstRun: function isFirstRun() {
return prefs.getBoolPref("browser.firstrun.show.localepicker");
},
// nsIObservers
observe: function (aSubject, aTopic, aState) {
switch(aTopic) {
case "Metro:RefreshTopsiteThumbnail":
this.forceReloadOfThumbnail(aState);
break;
case "metro_viewstate_changed":
this.onViewStateChange(aState);
for (let item of this._set.children) {
if (aState == "snapped") {
item.removeAttribute("tiletype");
} else {
item.setAttribute("tiletype", "thumbnail");
}
}
break;
}
},
// nsINavHistoryObserver
onBeginUpdateBatch: function() {
},
onEndUpdateBatch: function() {
},
onVisit: function(aURI, aVisitID, aTime, aSessionID,
aReferringID, aTransitionType) {
},
onTitleChanged: function(aURI, aPageTitle) {
},
onDeleteURI: function(aURI) {
},
onClearHistory: function() {
this._set.clearAll();
},
onPageChanged: function(aURI, aWhat, aValue) {
},
onDeleteVisits: function (aURI, aVisitTime, aGUID, aReason, aTransitionType) {
},
QueryInterface: function(iid) {
if (iid.equals(Components.interfaces.nsINavHistoryObserver) ||
iid.equals(Components.interfaces.nsISupports)) {
return this;
}
throw Cr.NS_ERROR_NO_INTERFACE;
}
});
let TopSitesStartView = {
_view: null,
get _grid() { return document.getElementById("start-topsites-grid"); },
init: function init() {
this._view = new TopSitesView(this._grid, 8);
if (this._view.isFirstRun()) {
let topsitesVbox = document.getElementById("start-topsites");
topsitesVbox.setAttribute("hidden", "true");
}
},
uninit: function uninit() {
if (this._view) {
this._view.destruct();
}
},
show: function show() {
this._grid.arrangeItems();
}
};

View File

@ -0,0 +1,83 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
let Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/commonjs/sdk/core/promise.js");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CrossSlide",
"resource:///modules/CrossSlide.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "View",
"resource:///modules/View.jsm");
XPCOMUtils.defineLazyServiceGetter(window, "gHistSvc",
"@mozilla.org/browser/nav-history-service;1",
"nsINavHistoryService",
"nsIBrowserHistory");
let ScriptContexts = {};
[
["Util", "chrome://browser/content/Util.js"],
["Site", "chrome://browser/content/Site.js"],
["StartUI", "chrome://browser/content/StartUI.js"],
["Bookmarks", "chrome://browser/content/bookmarks.js"],
["BookmarksView", "chrome://browser/content/BookmarksView.js"],
["HistoryView", "chrome://browser/content/HistoryView.js"],
["TopSitesView", "chrome://browser/content/TopSitesView.js"],
["RemoteTabsView", "chrome://browser/content/RemoteTabsView.js"],
["BookmarksStartView", "chrome://browser/content/BookmarksView.js"],
["HistoryStartView", "chrome://browser/content/HistoryView.js"],
["TopSitesStartView", "chrome://browser/content/TopSitesView.js"],
["RemoteTabsStartView", "chrome://browser/content/RemoteTabsView.js"],
["ItemPinHelper", "chrome://browser/content/helperui/ItemPinHelper.js"],
].forEach(function (aScript) {
let [name, script] = aScript;
XPCOMUtils.defineLazyGetter(window, name, function() {
let sandbox;
if (script in ScriptContexts) {
sandbox = ScriptContexts[script];
} else {
sandbox = ScriptContexts[script] = {};
Services.scriptloader.loadSubScript(script, sandbox);
}
return sandbox[name];
});
});
// singleton
XPCOMUtils.defineLazyGetter(this, "TopSites", function() {
return StartUI.chromeWin.TopSites;
});
#ifdef MOZ_SERVICES_SYNC
XPCOMUtils.defineLazyGetter(this, "Weave", function() {
Components.utils.import("resource://services-sync/main.js");
return Weave;
});
#endif

View File

@ -78,7 +78,6 @@ chrome.jar:
content/bookmarks.js (content/bookmarks.js)
content/exceptions.js (content/exceptions.js)
content/downloads.js (content/downloads.js)
content/history.js (content/history.js)
content/Site.js (content/Site.js)
content/TopSites.js (content/TopSites.js)
content/console.js (content/console.js)
@ -86,12 +85,21 @@ chrome.jar:
content/dbg-metro-actors.js (content/dbg-metro-actors.js)
#ifdef MOZ_SERVICES_SYNC
content/flyoutpanels/SyncFlyoutPanel.js (content/flyoutpanels/SyncFlyoutPanel.js)
content/RemoteTabs.js (content/RemoteTabs.js)
#endif
content/NavButtonSlider.js (content/NavButtonSlider.js)
content/ContextUI.js (content/ContextUI.js)
content/apzc.js (content/apzc.js)
* content/Start.xul (content/startui/Start.xul)
* content/startui-scripts.js (content/startui/startui-scripts.js)
content/StartUI.js (content/startui/StartUI.js)
content/BookmarksView.js (content/startui/BookmarksView.js)
content/HistoryView.js (content/startui/HistoryView.js)
content/TopSitesView.js (content/startui/TopSitesView.js)
#ifdef MOZ_SERVICES_SYNC
content/RemoteTabsView.js (content/startui/RemoteTabsView.js)
#endif
% override chrome://global/content/config.xul chrome://browser/content/config.xul
% override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
% override chrome://mozapps/content/extensions/extensions.xul chrome://browser/content/aboutAddons.xhtml

View File

@ -5,7 +5,7 @@
"use strict";
let gStartView = BookmarksStartView._view;
let gStartView = null;
function test() {
runTests();
@ -13,16 +13,17 @@ function test() {
function setup() {
PanelUI.hide();
if (!BrowserUI.isStartTabVisible) {
let tab = yield addTab("about:start");
gStartView = tab.browser.contentWindow.BookmarksStartView._view;
yield waitForCondition(() => BrowserUI.isStartTabVisible);
yield hideContextUI();
}
BookmarksTestHelper.setup();
yield hideContextUI();
if (StartUI.isStartPageVisible)
return;
yield addTab("about:start");
yield waitForCondition(() => StartUI.isStartPageVisible);
}
function tearDown() {

View File

@ -15,10 +15,10 @@ gTests.push({
yield addTab("about:start");
yield waitForCondition(function () {
return StartUI.isStartPageVisible;
return BrowserUI.isStartTabVisible;
});
is(StartUI.isVisible, true, "Start UI is displayed on about:start");
is(BrowserUI.isStartTabVisible, true, "Start UI is displayed on about:start");
is(ContextUI.navbarVisible, true, "Navbar is displayed on about:start");
is(ContextUI.tabbarVisible, false, "Tabbar is not displayed initially");
is(ContextUI.contextAppbarVisible, false, "Appbar is not displayed initially");
@ -41,7 +41,7 @@ gTests.push({
is(ContextUI.tabbarVisible, true, "Tabbar is visible after third swipe");
is(ContextUI.contextAppbarVisible, false, "Appbar is hidden after third swipe");
is(StartUI.isVisible, true, "Start UI is still visible");
is(BrowserUI.isStartTabVisible, true, "Start UI is still visible");
}
});
@ -50,7 +50,7 @@ gTests.push({
run: function testAbout() {
yield addTab("about:");
ContextUI.dismiss();
is(StartUI.isVisible, false, "Start UI is not visible on about:");
is(BrowserUI.isStartTabVisible, false, "Start UI is not visible on about:");
is(ContextUI.navbarVisible, false, "Navbar is not initially visible on about:");
is(ContextUI.tabbarVisible, false, "Tabbar is not initially visible on about:");
@ -62,7 +62,7 @@ gTests.push({
is(ContextUI.navbarVisible, false, "Navbar is not visible after second swipe");
is(ContextUI.tabbarVisible, false, "Tabbar is not visible after second swipe");
is(StartUI.isVisible, false, "Start UI is still not visible");
is(BrowserUI.isStartTabVisible, false, "Start UI is still not visible");
}
});

View File

@ -5,31 +5,31 @@
"use strict";
let gStartView = HistoryStartView._view;
let gStartView = null;
function test() {
runTests();
}
function scrollToEnd() {
let startBox = document.getElementById("start-scrollbox");
let [, scrollInterface] = ScrollUtils.getScrollboxFromElement(startBox);
scrollInterface.scrollBy(50000, 0);
let scroller = getBrowser().contentWindow.StartUI.getScrollBoxObject();
scroller.scrollBy(50000, 0);
}
function setup() {
PanelUI.hide();
HistoryTestHelper.setup();
if (!StartUI.isStartPageVisible) {
yield addTab("about:start");
if (!BrowserUI.isStartTabVisible) {
let tab = yield addTab("about:start");
gStartView = tab.browser.contentWindow.HistoryStartView._view;
yield waitForCondition(() => StartUI.isStartPageVisible);
yield waitForCondition(() => BrowserUI.isStartTabVisible);
yield hideContextUI();
}
HistoryTestHelper.setup();
// Scroll to make sure all tiles are visible.
scrollToEnd();
}

View File

@ -18,25 +18,25 @@ function test() {
gTests.push({
desc: "Test sync tabs from other devices UI",
run: function run() {
if (StartUI.isStartPageVisible)
if (BrowserUI.isStartTabVisible)
return;
yield addTab("about:start");
yield waitForCondition(() => StartUI.isStartPageVisible);
yield waitForCondition(() => BrowserUI.isStartTabVisible);
yield hideContextUI();
is(Weave.Status.checkSetup(), Weave.CLIENT_NOT_CONFIGURED, "Sync should be disabled on start");
let vbox = document.getElementById("start-remotetabs");
let vbox = Browser.selectedBrowser.contentDocument.getElementById("start-remotetabs");
ok(vbox.hidden, "remote tabs in the start page should be hidden when sync is not enabled");
RemoteTabsStartView._view.setUIAccessVisible(true);
Browser.selectedBrowser.contentWindow.RemoteTabsStartView._view.setUIAccessVisible(true);
// start page grid should be visible
ok(vbox, "remote tabs grid is present on start page");
is(vbox.hidden, false, "remote tabs should be visible in start page when sync is enabled");
RemoteTabsStartView._view.setUIAccessVisible(false);
Browser.selectedBrowser.contentWindow.RemoteTabsStartView._view.setUIAccessVisible(false);
ok(vbox.hidden, "remote tabs in the start page should be hidden when sync is not enabled");
}

View File

@ -38,7 +38,7 @@ gTests.push({
yield addTab(chromeRoot + "browser_selection_basic.html");
yield waitForCondition(function () {
return !StartUI.isStartPageVisible;
return !BrowserUI.isStartTabVisible;
}, 10000, 100);
yield hideContextUI();

View File

@ -31,7 +31,7 @@ gTests.push({
yield addTab(chromeRoot + "browser_selection_caretfocus.html");
yield waitForCondition(function () {
return !StartUI.isStartPageVisible;
return !BrowserUI.isStartTabVisible;
});
yield hideContextUI();

View File

@ -20,7 +20,7 @@ gTests.push({
info(chromeRoot + "browser_selection_contenteditable.html");
yield addTab(chromeRoot + "browser_selection_contenteditable.html");
yield waitForCondition(function () {
return !StartUI.isStartPageVisible;
return !BrowserUI.isStartTabVisible;
}, kCommonWaitMs, kCommonPollMs);
yield hideContextUI();

View File

@ -38,7 +38,7 @@ gTests.push({
yield addTab(chromeRoot + "browser_selection_frame_content.html");
yield waitForCondition(function () {
return !StartUI.isStartPageVisible;
return !BrowserUI.isStartTabVisible;
}, 10000, 100);
yield hideContextUI();

View File

@ -41,7 +41,7 @@ gTests.push({
yield addTab(chromeRoot + "browser_selection_frame_inputs.html");
yield waitForCondition(function () {
return !StartUI.isStartPageVisible;
return !BrowserUI.isStartTabVisible;
}, 10000, 100);
yield hideContextUI();

View File

@ -41,7 +41,7 @@ gTests.push({
yield addTab(chromeRoot + "browser_selection_frame_textarea.html");
yield waitForCondition(function () {
return !StartUI.isStartPageVisible;
return !BrowserUI.isStartTabVisible;
}, 10000, 100);
yield hideContextUI();

View File

@ -44,7 +44,7 @@ gTests.push({
yield addTab(chromeRoot + "browser_selection_inputs.html");
yield waitForCondition(function () {
return !StartUI.isStartPageVisible;
return !BrowserUI.isStartTabVisible;
});
yield hideContextUI();

View File

@ -38,7 +38,7 @@ gTests.push({
yield addTab(chromeRoot + "browser_selection_textarea.html");
yield waitForCondition(function () {
return !StartUI.isStartPageVisible;
return !BrowserUI.isStartTabVisible;
}, 10000, 100);
yield hideContextUI();

View File

@ -23,7 +23,7 @@ gTests.push({
yield addTab(chromeRoot + "res/textblock01.html");
yield waitForCondition(function () {
return !StartUI.isStartPageVisible;
return !BrowserUI.isStartTabVisible;
});
yield hideContextUI();

View File

@ -9,14 +9,20 @@
// Test helpers
let TopSitesTestHelper = {
get grid() {
return Browser.selectedBrowser.contentDocument.getElementById("start-topsites-grid");
},
get document() {
return Browser.selectedBrowser.contentDocument;
},
setup: function() {
return Task.spawn(function(){
if (StartUI.isStartPageVisible)
if (BrowserUI.isStartTabVisible)
return;
yield addTab("about:start");
yield waitForCondition(() => StartUI.isStartPageVisible);
yield waitForCondition(() => BrowserUI.isStartTabVisible);
});
},
mockLinks: function th_mockLinks(aLinks) {
@ -176,18 +182,18 @@ gTests.push({
desc: "load and display top sites",
setUp: function() {
yield TopSitesTestHelper.setup();
let grid = document.getElementById("start-topsites-grid");
let grid = TopSitesTestHelper.grid;
// setup - set history to known state
yield TopSitesTestHelper.setLinks("brian,dougal,dylan,ermintrude,florence,moose,sgtsam,train,zebedee,zeebad");
let arrangedPromise = waitForEvent(grid, "arranged");
yield TopSitesTestHelper.updatePagesAndWait();
yield arrangedPromise;
// pause until the update has fired and the view is finishd updating
yield arrangedPromise;
},
run: function() {
let grid = document.getElementById("start-topsites-grid");
let grid = TopSitesTestHelper.grid;
let items = grid.children;
is(items.length, 8, "should be 8 topsites"); // i.e. not 10
if(items.length) {
@ -217,17 +223,17 @@ gTests.push({
this.pins
);
// pause until the update has fired and the view is finishd updating
let arrangedPromise = waitForEvent(document.getElementById("start-topsites-grid"), "arranged");
let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged");
yield TopSitesTestHelper.updatePagesAndWait();
yield arrangedPromise;
},
run: function() {
// test that pinned state of each site as rendered matches our expectations
let pins = this.pins.split(",");
let items = document.getElementById("start-topsites-grid").children;
let items = TopSitesTestHelper.grid.children;
is(items.length, 8, "should be 8 topsites in the grid");
is(document.querySelectorAll("#start-topsites-grid > [pinned]").length, 3, "should be 3 children with 'pinned' attribute");
is(TopSitesTestHelper.document.querySelectorAll("#start-topsites-grid > [pinned]").length, 3, "should be 3 children with 'pinned' attribute");
try {
Array.forEach(items, function(aItem, aIndex){
// pinned state should agree with the pins array
@ -258,7 +264,7 @@ gTests.push({
// setup - set history to known state
yield TopSitesTestHelper.setLinks("sgtsam,train,zebedee,zeebad", []); // nothing initially pinned
// pause until the update has fired and the view is finishd updating
let arrangedPromise = waitForEvent(document.getElementById("start-topsites-grid"), "arranged");
let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged");
yield TopSitesTestHelper.updatePagesAndWait();
yield arrangedPromise;
},
@ -266,7 +272,7 @@ gTests.push({
// pin a site
// test that site is pinned as expected
// and that sites fill positions around it
let grid = document.getElementById("start-topsites-grid"),
let grid = TopSitesTestHelper.grid,
items = grid.children;
is(items.length, 4, this.desc + ": should be 4 topsites");
@ -315,14 +321,14 @@ gTests.push({
this.pins
);
// pause until the update has fired and the view is finishd updating
let arrangedPromise = waitForEvent(document.getElementById("start-topsites-grid"), "arranged");
let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged");
yield TopSitesTestHelper.updatePagesAndWait();
yield arrangedPromise;
},
run: function() {
// unpin a pinned site
// test that sites are unpinned as expected
let grid = document.getElementById("start-topsites-grid"),
let grid = TopSitesTestHelper.grid,
items = grid.children;
is(items.length, 8, this.desc + ": should be 8 topsites");
let site = {
@ -356,7 +362,7 @@ gTests.push({
",dougal"
);
// pause until the update has fired and the view is finishd updating
let arrangedPromise = waitForEvent(document.getElementById("start-topsites-grid"), "arranged");
let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged");
yield TopSitesTestHelper.updatePagesAndWait();
yield arrangedPromise;
},
@ -364,7 +370,7 @@ gTests.push({
try {
// block a site
// test that sites are removed from the grid as expected
let grid = document.getElementById("start-topsites-grid"),
let grid = TopSitesTestHelper.grid,
items = grid.children;
is(items.length, 8, this.desc + ": should be 8 topsites");
@ -444,14 +450,14 @@ gTests.push({
this.pins
);
// pause until the update has fired and the view is finishd updating
let arrangedPromise = waitForEvent(document.getElementById("start-topsites-grid"), "arranged");
let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged");
yield TopSitesTestHelper.updatePagesAndWait();
yield arrangedPromise;
},
run: function() {
// delete a both pinned and unpinned sites
// test that sites are removed from the grid
let grid = document.getElementById("start-topsites-grid"),
let grid = TopSitesTestHelper.grid,
items = grid.children;
is(items.length, 4, this.desc + ": should be 4 topsites");

View File

@ -91,19 +91,16 @@ function removeMockSearchDefault(aTimeoutMs) {
=============================================================================*/
function test() {
runTests();
waitForExplicitFinish();
Task.spawn(function(){
yield addTab("about:blank");
}).then(runTests);
}
function setUp() {
if (!gEdit)
gEdit = document.getElementById("urlbar-edit");
yield addTab("about:start");
yield showNavBar();
yield waitForCondition(function () {
return StartUI.isStartPageVisible;
});
}
function tearDown() {
@ -164,6 +161,8 @@ gTests.push({
run: function testSearchKeyboard() {
yield addMockSearchDefault();
yield waitForCondition(() => !Browser.selectedTab.isLoading());
sendElementTap(window, gEdit);
ok(gEdit.isEditing, "focus urlbar: in editing mode");
ok(!gEdit.popup.popupOpen, "focus urlbar: popup not open yet");
@ -209,6 +208,8 @@ gTests.push({
run: function testUrlbarSearchesTouch() {
yield addMockSearchDefault();
yield waitForCondition(() => !Browser.selectedTab.isLoading());
sendElementTap(window, gEdit);
ok(gEdit.isEditing, "focus urlbar: in editing mode");
ok(!gEdit.popup.popupOpen, "focus urlbar: popup not open yet");

View File

@ -780,8 +780,9 @@ function runTests() {
let badTabs = [];
Browser.tabs.forEach(function(item, index, array) {
let location = item.browser.currentURI.spec;
if (index == 0 && location == "about:blank")
if (index == 0 && location == "about:blank" || location == "about:start") {
return;
}
ok(false, "Left over tab after test: '" + location + "'");
badTabs.push(item);
});

View File

@ -8,8 +8,8 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
let modules = {
start: {
uri: "about:blank",
privileged: false
uri: "chrome://browser/content/Start.xul",
privileged: true
},
// about:blank has some bad loading behavior we can avoid, if we use an alias
empty: {
@ -36,9 +36,8 @@ let modules = {
uri: "chrome://browser/content/aboutCertError.xhtml",
privileged: true
},
// an alias for about:start
home: {
uri: "about:blank",
uri: "about:start",
privileged: true
}
}

View File

@ -188,11 +188,6 @@ documenttab[selected] .documenttab-selection {
/* Start UI ----------------------------------------------------------------- */
#start-container {
display: none;
}
#start-container[startpage],
#start-container[filtering] {
display: -moz-box;
}
@ -200,12 +195,6 @@ documenttab[selected] .documenttab-selection {
overflow-x: scroll;
}
/* if autocomplete is set, hide both start pages,
* else hide the autocomplete screen */
#start-container[filtering] > .start-page,
#start-container:not([filtering]) > #start-autocomplete {
visibility: collapse;
}
/* startUI sections, grids */
#start-scrollbox > .meta-section {
/* allot space for at least a single column */
@ -213,13 +202,16 @@ documenttab[selected] .documenttab-selection {
/* leave margin for horizontal scollbar */
margin-bottom: 30px;
}
#start-scrollbox[input="precise"] > .meta-section {
margin-bottom: 5px;
}
#start-topsites {
/* allot space for 3 tile columns for the topsites grid */
min-width: calc(3 * @grid_double_column_width@);
}
#start-scrollbox {
-moz-box-orient: horizontal;
/* Move scrollbar above toolbar,
@ -558,7 +550,7 @@ documenttab[selected] .documenttab-selection {
overflow: hidden;
}
#urlbar[mode="edit"] {
#urlbar[editing] {
border-color: @metro_orange@;
}
@ -683,9 +675,19 @@ documenttab[selected] .documenttab-selection {
}
}
#urlbar:-moz-any([mode="loading"], [mode="view"]) > #go-button,
#urlbar:-moz-any([mode="edit"], [mode="loading"]) > #reload-button,
#urlbar:-moz-any([mode="edit"], [mode="view"]) > #stop-button,
/* navbar edit button: one button out of three - when editing: go, when !editing,
loading: stop, when !editing, !loading: reload */
#go-button, #reload-button, #stop-button {
visibility: collapse;
}
#urlbar[editing] > #go-button,
#urlbar:not([editing])[loading] > #stop-button,
#urlbar:not([editing]):not([loading]) > #reload-button {
visibility: visible;
}
#toolbar[viewstate="snapped"] > #urlbar ~ toolbarbutton {
visibility: collapse;
}

View File

@ -17,8 +17,9 @@
<org.mozilla.gecko.FlowLayout android:id="@+id/suggestion_layout"
android:layout_toRightOf="@id/suggestion_icon"
android:layout_width="wrap_content"
android:duplicateParentState="true"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:duplicateParentState="true">
<include layout="@layout/suggestion_item"
android:id="@+id/suggestion_user_entered"/>

View File

@ -479,5 +479,181 @@ this.DownloadIntegration = {
*/
_getDirectory: function DI_getDirectory(aName) {
return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile);
},
/**
* Register the downloads interruption observers.
*
* @param aList
* The public or private downloads list.
* @param aIsPrivate
* True if the list is private, false otherwise.
*
* @return {Promise}
* @resolves When the views and observers are added.
*/
addListObservers: function DI_addListObservers(aList, aIsPrivate) {
DownloadObserver.registerView(aList, aIsPrivate);
if (!DownloadObserver.observersAdded) {
DownloadObserver.observersAdded = true;
Services.obs.addObserver(DownloadObserver, "quit-application-requested", true);
Services.obs.addObserver(DownloadObserver, "offline-requested", true);
Services.obs.addObserver(DownloadObserver, "last-pb-context-exiting", true);
}
return Promise.resolve();
}
};
let DownloadObserver = {
/**
* Flag to determine if the observers have been added previously.
*/
observersAdded: false,
/**
* Set that contains the in progress publics downloads.
* It's keep updated when a public download is added, removed or change its
* properties.
*/
_publicInProgressDownloads: new Set(),
/**
* Set that contains the in progress private downloads.
* It's keep updated when a private download is added, removed or change its
* properties.
*/
_privateInProgressDownloads: new Set(),
/**
* Registers a view that updates the corresponding downloads state set, based
* on the aIsPrivate argument. The set is updated when a download is added,
* removed or changed its properties.
*
* @param aList
* The public or private downloads list.
* @param aIsPrivate
* True if the list is private, false otherwise.
*/
registerView: function DO_registerView(aList, aIsPrivate) {
let downloadsSet = aIsPrivate ? this._privateInProgressDownloads
: this._publicInProgressDownloads;
let downloadsView = {
onDownloadAdded: function DO_V_onDownloadAdded(aDownload) {
if (!aDownload.stopped) {
downloadsSet.add(aDownload);
}
},
onDownloadChanged: function DO_V_onDownloadChanged(aDownload) {
if (aDownload.stopped) {
downloadsSet.delete(aDownload);
} else {
downloadsSet.add(aDownload);
}
},
onDownloadRemoved: function DO_V_onDownloadRemoved(aDownload) {
downloadsSet.delete(aDownload);
}
};
aList.addView(downloadsView);
},
/**
* Shows the confirm cancel downloads dialog.
*
* @param aCancel
* The observer notification subject.
* @param aDownloadsCount
* The current downloads count.
* @param aIdTitle
* The string bundle id for the dialog title.
* @param aIdMessageSingle
* The string bundle id for the single download message.
* @param aIdMessageMultiple
* The string bundle id for the multiple downloads message.
* @param aIdButton
* The string bundle id for the don't cancel button text.
*/
_confirmCancelDownloads: function DO_confirmCancelDownload(
aCancel, aDownloadsCount, aIdTitle, aIdMessageSingle, aIdMessageMultiple, aIdButton) {
// If user has already dismissed the request, then do nothing.
if ((aCancel instanceof Ci.nsISupportsPRBool) && aCancel.data) {
return;
}
// If there are no active downloads, then do nothing.
if (aDownloadsCount <= 0) {
return;
}
let win = Services.wm.getMostRecentWindow("navigator:browser");
let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
(Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1);
let title = gStringBundle.GetStringFromName(aIdTitle);
let dontQuitButton = gStringBundle.GetStringFromName(aIdButton);
let quitButton;
let message;
if (aDownloadsCount > 1) {
message = gStringBundle.formatStringFromName(aIdMessageMultiple,
[aDownloadsCount], 1);
quitButton = gStringBundle.formatStringFromName("cancelDownloadsOKTextMultiple",
[aDownloadsCount], 1);
} else {
message = gStringBundle.GetStringFromName(aIdMessageSingle);
quitButton = gStringBundle.GetStringFromName("cancelDownloadsOKText");
}
let rv = Services.prompt.confirmEx(win, title, message, buttonFlags,
quitButton, dontQuitButton, null, null, {});
aCancel.data = (rv == 1);
},
////////////////////////////////////////////////////////////////////////////
//// nsIObserver
observe: function DO_observe(aSubject, aTopic, aData) {
let downloadsCount;
switch (aTopic) {
case "quit-application-requested":
downloadsCount = this._publicInProgressDownloads.size +
this._privateInProgressDownloads.size;
#ifndef XP_MACOSX
this._confirmCancelDownloads(aSubject, downloadsCount,
"quitCancelDownloadsAlertTitle",
"quitCancelDownloadsAlertMsg",
"quitCancelDownloadsAlertMsgMultiple",
"dontQuitButtonWin");
#else
this._confirmCancelDownloads(aSubject, downloadsCount,
"quitCancelDownloadsAlertTitle",
"quitCancelDownloadsAlertMsgMac",
"quitCancelDownloadsAlertMsgMacMultiple",
"dontQuitButtonMac");
#endif
break;
case "offline-requested":
downloadsCount = this._publicInProgressDownloads.size +
this._privateInProgressDownloads.size;
this._confirmCancelDownloads(aSubject, downloadsCount,
"offlineCancelDownloadsAlertTitle",
"offlineCancelDownloadsAlertMsg",
"offlineCancelDownloadsAlertMsgMultiple",
"dontGoOfflineButton");
break;
case "last-pb-context-exiting":
this._confirmCancelDownloads(aSubject,
this._privateInProgressDownloads.size,
"leavePrivateBrowsingCancelDownloadsAlertTitle",
"leavePrivateBrowsingWindowsCancelDownloadsAlertMsg",
"leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple",
"dontLeavePrivateBrowsingButton");
break;
}
},
////////////////////////////////////////////////////////////////////////////
//// nsISupports
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference])
};

View File

@ -147,6 +147,7 @@ this.Downloads = {
function task_D_getPublicDownloadList() {
let list = new DownloadList(true);
try {
yield DownloadIntegration.addListObservers(list, false);
yield DownloadIntegration.loadPersistent(list);
} catch (ex) {
Cu.reportError(ex);
@ -176,12 +177,27 @@ this.Downloads = {
*/
getPrivateDownloadList: function D_getPrivateDownloadList()
{
if (!this._privateDownloadList) {
this._privateDownloadList = new DownloadList(false);
if (!this._promisePrivateDownloadList) {
this._promisePrivateDownloadList = Task.spawn(
function task_D_getPublicDownloadList() {
let list = new DownloadList(false);
try {
yield DownloadIntegration.addListObservers(list, true);
} catch (ex) {
Cu.reportError(ex);
}
throw new Task.Result(list);
});
}
return Promise.resolve(this._privateDownloadList);
return this._promisePrivateDownloadList;
},
_privateDownloadList: null,
/**
* This promise is resolved with a reference to a DownloadList object that
* represents private downloads. This property is null before the list of
* downloads is requested for the first time.
*/
_promisePrivateDownloadList: null,
/**
* Returns the system downloads directory asynchronously.

View File

@ -389,7 +389,7 @@ function promiseNewDownloadList() {
*/
function promiseNewPrivateDownloadList() {
// Force the creation of a new public download list.
Downloads._privateDownloadList = null;
Downloads._promisePrivateDownloadList = null;
return Downloads.getPrivateDownloadList();
}

View File

@ -23,6 +23,7 @@ this.EXPORTED_SYMBOLS = ["OS"];
let SharedAll = {};
Components.utils.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
Components.utils.import("resource://gre/modules/Deprecated.jsm", this);
// Boilerplate, to simplify the transition to require()
let OS = SharedAll.OS;
@ -725,7 +726,14 @@ File.writeAtomic = function writeAtomic(path, buffer, options = {}) {
* @constructor
*/
File.Info = function Info(value) {
return value;
// Note that we can't just do this[k] = value[k] because our
// prototype defines getters for all of these fields.
for (let k in value) {
if (k != "creationDate") {
Object.defineProperty(this, k, {value: value[k]});
}
}
Object.defineProperty(this, "_deprecatedCreationDate", {value: value["creationDate"]});
};
if (OS.Constants.Win) {
File.Info.prototype = Object.create(OS.Shared.Win.AbstractInfo.prototype);
@ -735,6 +743,14 @@ if (OS.Constants.Win) {
throw new Error("I am neither under Windows nor under a Posix system");
}
// Deprecated
Object.defineProperty(File.Info.prototype, "creationDate", {
get: function creationDate() {
Deprecated.warning("Field 'creationDate' is deprecated.", "https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info#Cross-platform_Attributes");
return this._deprecatedCreationDate;
}
});
File.Info.fromMsg = function fromMsg(value) {
return new File.Info(value);
};

View File

@ -0,0 +1,39 @@
"use strict";
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
function run_test() {
do_test_pending();
run_next_test();
}
/**
* Test to ensure that deprecation warning is issued on use
* of creationDate.
*/
add_task(function test_deprecatedCreationDate () {
let deferred = Promise.defer();
let consoleListener = {
observe: function (aMessage) {
if(aMessage.message.indexOf("Field 'creationDate' is deprecated.") > -1) {
do_print("Deprecation message printed");
do_check_true(true);
Services.console.unregisterListener(consoleListener);
deferred.resolve();
}
}
};
let currentDir = yield OS.File.getCurrentDirectory();
let path = OS.Path.join(currentDir, "test_creationDate.js");
Services.console.registerListener(consoleListener);
(yield OS.File.stat(path)).creationDate;
});
add_task(do_test_finished);

View File

@ -7,5 +7,6 @@ tail =
[test_osfile_async.js]
[test_profiledir.js]
[test_logging.js]
[test_creationDate.js]
# bug 845190 - thread pool wasn't shutdown assertions
skip-if = (os == "win" || "linux") && debug

View File

@ -0,0 +1,286 @@
"use strict";
this.EXPORTED_SYMBOLS = ["TelemetryFile"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
let imports = {};
Cu.import("resource://gre/modules/Services.jsm", imports);
Cu.import("resource://gre/modules/Deprecated.jsm", imports);
Cu.import("resource://gre/modules/NetUtil.jsm", imports);
let {Services, Deprecated, NetUtil} = imports;
// Constants from prio.h for nsIFileOutputStream.init
const PR_WRONLY = 0x2;
const PR_CREATE_FILE = 0x8;
const PR_TRUNCATE = 0x20;
const PR_EXCL = 0x80;
const RW_OWNER = parseInt("0600", 8);
const RWX_OWNER = parseInt("0700", 8);
// Delete ping files that have been lying around for longer than this.
const MAX_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week
// The number of outstanding saved pings that we have issued loading
// requests for.
let pingsLoaded = 0;
// The number of those requests that have actually completed.
let pingLoadsCompleted = 0;
// If |true|, send notifications "telemetry-test-save-complete"
// and "telemetry-test-load-complete" once save/load is complete.
let shouldNotifyUponSave = false;
// Data that has neither been saved nor sent by ping
let pendingPings = [];
this.TelemetryFile = {
/**
* Save a single ping to a file.
*
* @param {object} ping The content of the ping to save.
* @param {nsIFile} file The destination file.
* @param {bool} sync If |true|, write synchronously. Deprecated.
* This argument should be |false|.
* @param {bool} overwrite If |true|, the file will be overwritten
* if it exists.
*/
savePingToFile: function(ping, file, sync, overwrite) {
let pingString = JSON.stringify(ping);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let ostream = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
let initFlags = PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE;
if (!overwrite) {
initFlags |= PR_EXCL;
}
try {
ostream.init(file, initFlags, RW_OWNER, 0);
} catch (e) {
// Probably due to PR_EXCL.
return;
}
if (sync) {
let utf8String = converter.ConvertFromUnicode(pingString);
utf8String += converter.Finish();
let success = false;
try {
let amount = ostream.write(utf8String, utf8String.length);
success = amount == utf8String.length;
} catch (e) {
}
finishTelemetrySave(success, ostream);
} else {
let istream = converter.convertToInputStream(pingString);
let self = this;
NetUtil.asyncCopy(istream, ostream,
function(result) {
finishTelemetrySave(Components.isSuccessCode(result),
ostream);
});
}
},
/**
* Save a ping to its file, synchronously.
*
* @param {object} ping The content of the ping to save.
* @param {bool} overwrite If |true|, the file will be overwritten
* if it exists.
*/
savePing: function(ping, overwrite) {
this.savePingToFile(ping,
getSaveFileForPing(ping), true, overwrite);
},
/**
* Save all pending pings, synchronously.
*
* @param {object} sessionPing The additional session ping.
*/
savePendingPings: function(sessionPing) {
this.savePing(sessionPing, true);
pendingPings.forEach(function sppcb(e, i, a) {
this.savePing(e, false);
}, this);
pendingPings = [];
},
/**
* Remove the file for a ping
*
* @param {object} ping The ping.
*/
cleanupPingFile: function(ping) {
// FIXME: We shouldn't create the directory just to remove the file.
let file = getSaveFileForPing(ping);
try {
file.remove(true); // FIXME: Should be |false|, isn't it?
} catch(e) {
}
},
/**
* Load all saved pings.
*
* Once loaded, the saved pings can be accessed (destructively only)
* through |popPendingPings|.
*
* @param {bool} sync If |true|, loading takes place synchronously.
* @param {function*} onLoad A function called upon loading of each
* ping. It is passed |true| in case of success, |false| in case of
* format error.
*/
loadSavedPings: function(sync, onLoad = null) {
let directory = ensurePingDirectory();
let entries = directory.directoryEntries
.QueryInterface(Ci.nsIDirectoryEnumerator);
pingsLoaded = 0;
pingLoadsCompleted = 0;
try {
while (entries.hasMoreElements()) {
this.loadHistograms(entries.nextFile, sync, onLoad);
}
} finally {
entries.close();
}
},
/**
* Load the histograms from a file.
*
* Once loaded, the saved pings can be accessed (destructively only)
* through |popPendingPings|.
*
* @param {nsIFile} file The file to load.
* @param {bool} sync If |true|, loading takes place synchronously.
* @param {function*} onLoad A function called upon loading of the
* ping. It is passed |true| in case of success, |false| in case of
* format error.
*/
loadHistograms: function loadHistograms(file, sync, onLoad = null) {
let now = new Date();
if (now - file.lastModifiedTime > MAX_PING_FILE_AGE) {
// We haven't had much luck in sending this file; delete it.
file.remove(true);
return;
}
pingsLoaded++;
if (sync) {
let stream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
stream.init(file, -1, -1, 0);
addToPendingPings(file, stream, onLoad);
} else {
let channel = NetUtil.newChannel(file);
channel.contentType = "application/json";
NetUtil.asyncFetch(channel, (function(stream, result) {
if (!Components.isSuccessCode(result)) {
return;
}
addToPendingPings(file, stream, onLoad);
}).bind(this));
}
},
/**
* The number of pings loaded since the beginning of time.
*/
get pingsLoaded() {
return pingsLoaded;
},
/**
* Iterate destructively through the pending pings.
*
* @return {iterator}
*/
popPendingPings: function(reason) {
while (pendingPings.length > 0) {
let data = pendingPings.pop();
// Send persisted pings to the test URL too.
if (reason == "test-ping") {
data.reason = reason;
}
yield data;
}
},
set shouldNotifyUponSave(value) {
shouldNotifyUponSave = value;
},
testLoadHistograms: function(file, sync, onLoad) {
pingsLoaded = 0;
pingLoadsCompleted = 0;
this.loadHistograms(file, sync, onLoad);
}
};
///// Utility functions
function getSaveFileForPing(ping) {
let file = ensurePingDirectory();
file.append(ping.slug);
return file;
};
function ensurePingDirectory() {
let directory = Services.dirsvc.get("ProfD", Ci.nsILocalFile).clone();
directory.append("saved-telemetry-pings");
try {
directory.create(Ci.nsIFile.DIRECTORY_TYPE, RWX_OWNER);
} catch (e) {
// Already exists, just ignore this.
}
return directory;
};
function addToPendingPings(file, stream, onLoad) {
let success = false;
try {
let string = NetUtil.readInputStreamToString(stream, stream.available(),
{ charset: "UTF-8" });
stream.close();
let ping = JSON.parse(string);
// The ping's payload used to be stringified JSON. Deal with that.
if (typeof(ping.payload) == "string") {
ping.payload = JSON.parse(ping.payload);
}
pingLoadsCompleted++;
pendingPings.push(ping);
if (shouldNotifyUponSave &&
pingLoadsCompleted == pingsLoaded) {
Services.obs.notifyObservers(null, "telemetry-test-load-complete", null);
}
success = true;
} catch (e) {
// An error reading the file, or an error parsing the contents.
stream.close(); // close is idempotent.
file.remove(true); // FIXME: Should be false, isn't it?
}
if (onLoad) {
onLoad(success);
}
};
function finishTelemetrySave(ok, stream) {
stream.close();
if (shouldNotifyUponSave && ok) {
Services.obs.notifyObservers(null, "telemetry-test-save-complete", null);
}
};

View File

@ -10,12 +10,12 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
#ifndef MOZ_WIDGET_GONK
Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
#endif
Cu.import("resource://gre/modules/ctypes.jsm");
Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm");
Cu.import("resource://gre/modules/TelemetryFile.jsm");
// When modifying the payload in incompatible ways, please bump this version number
const PAYLOAD_VERSION = 1;
@ -37,15 +37,6 @@ const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID";
const TELEMETRY_INTERVAL = 60000;
// Delay before intializing telemetry (ms)
const TELEMETRY_DELAY = 60000;
// Delete ping files that have been lying around for longer than this.
const MAX_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week
// Constants from prio.h for nsIFileOutputStream.init
const PR_WRONLY = 0x2;
const PR_CREATE_FILE = 0x8;
const PR_TRUNCATE = 0x20;
const PR_EXCL = 0x80;
const RW_OWNER = 0600;
const RWX_OWNER = 0700;
// MEM_HISTOGRAMS lists the memory reporters we turn into histograms.
//
@ -172,15 +163,7 @@ TelemetryPing.prototype = {
_prevSession: null,
_hasWindowRestoredObserver: false,
_hasXulWindowVisibleObserver: false,
_pendingPings: [],
_doLoadSaveNotifications: false,
_startupIO : {},
// The number of outstanding saved pings that we have issued loading
// requests for.
_pingsLoaded: 0,
// The number of those requests that have actually completed.
_pingLoadsCompleted: 0,
_savedProfileDirectory: null,
// The previous build ID, if this is the first run with a new build.
// Undefined if this is not the first run, or the previous build ID is unknown.
_previousBuildID: undefined,
@ -258,7 +241,7 @@ TelemetryPing.prototype = {
} catch(e) {
}
if (!forSavedSession || hasPingBeenSent) {
ret.savedPings = this._pingsLoaded;
ret.savedPings = TelemetryFile.pingsLoaded;
}
return ret;
@ -593,16 +576,11 @@ TelemetryPing.prototype = {
return this.assemblePing(this.getSessionPayload(reason), reason);
},
getPayloads: function getPayloads(reason) {
popPayloads: function popPayloads(reason) {
function payloadIter() {
yield this.getSessionPayloadAndSlug(reason);
while (this._pendingPings.length > 0) {
let data = this._pendingPings.pop();
// Send persisted pings to the test URL too.
if (reason == "test-ping") {
data.reason = reason;
}
let iterator = TelemetryFile.popPendingPings(reason);
for (let data of iterator) {
yield data;
}
}
@ -618,7 +596,7 @@ TelemetryPing.prototype = {
// populate histograms one last time
this.gatherMemory();
this.sendPingsFromIterator(server, reason,
Iterator(this.getPayloads(reason)));
Iterator(this.popPayloads(reason)));
},
/**
@ -651,7 +629,7 @@ TelemetryPing.prototype = {
this.sendPingsFromIterator(server, reason, i);
}
function onError() {
this.savePing(data, true);
TelemetryFile.savePing(data, true);
// Notify that testing is complete, even if we didn't send everything.
finishPings(reason);
}
@ -667,11 +645,7 @@ TelemetryPing.prototype = {
hping.add(new Date() - startTime);
if (success) {
let file = this.saveFileForPing(ping);
try {
file.remove(true);
} catch(e) {
}
TelemetryFile.cleanupPingFile(ping);
}
},
@ -820,7 +794,11 @@ TelemetryPing.prototype = {
this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
function timerCallback() {
this._initialized = true;
this.loadSavedPings(false);
TelemetryFile.loadSavedPings(false, (success =>
{
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
success_histogram.add(success);
}));
this.attachObservers();
this.gatherMemory();
@ -832,128 +810,12 @@ TelemetryPing.prototype = {
Ci.nsITimer.TYPE_ONE_SHOT);
},
addToPendingPings: function addToPendingPings(file, stream) {
let success = false;
try {
let string = NetUtil.readInputStreamToString(stream, stream.available(), { charset: "UTF-8" });
stream.close();
let ping = JSON.parse(string);
// The ping's payload used to be stringified JSON. Deal with that.
if (typeof(ping.payload) == "string") {
ping.payload = JSON.parse(ping.payload);
}
this._pingLoadsCompleted++;
this._pendingPings.push(ping);
if (this._doLoadSaveNotifications &&
this._pingLoadsCompleted == this._pingsLoaded) {
Services.obs.notifyObservers(null, "telemetry-test-load-complete", null);
}
success = true;
} catch (e) {
// An error reading the file, or an error parsing the contents.
stream.close(); // close is idempotent.
file.remove(true);
}
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
success_histogram.add(success);
},
loadHistograms: function loadHistograms(file, sync) {
let now = new Date();
if (now - file.lastModifiedTime > MAX_PING_FILE_AGE) {
// We haven't had much luck in sending this file; delete it.
file.remove(true);
return;
}
this._pingsLoaded++;
if (sync) {
let stream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
stream.init(file, -1, -1, 0);
this.addToPendingPings(file, stream);
} else {
let channel = NetUtil.newChannel(file);
channel.contentType = "application/json"
NetUtil.asyncFetch(channel, (function(stream, result) {
if (!Components.isSuccessCode(result)) {
return;
}
this.addToPendingPings(file, stream);
}).bind(this));
}
},
testLoadHistograms: function testLoadHistograms(file, sync) {
this._pingsLoaded = 0;
this._pingLoadsCompleted = 0;
this.loadHistograms(file, sync);
},
loadSavedPings: function loadSavedPings(sync) {
let directory = this.ensurePingDirectory();
let entries = directory.directoryEntries
.QueryInterface(Ci.nsIDirectoryEnumerator);
this._pingsLoaded = 0;
this._pingLoadsCompleted = 0;
try {
while (entries.hasMoreElements()) {
this.loadHistograms(entries.nextFile, sync);
}
}
finally {
entries.close();
}
},
finishTelemetrySave: function finishTelemetrySave(ok, stream) {
stream.close();
if (this._doLoadSaveNotifications && ok) {
Services.obs.notifyObservers(null, "telemetry-test-save-complete", null);
}
},
savePingToFile: function savePingToFile(ping, file, sync, overwrite) {
let pingString = JSON.stringify(ping);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let ostream = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
let initFlags = PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE;
if (!overwrite) {
initFlags |= PR_EXCL;
}
try {
ostream.init(file, initFlags, RW_OWNER, 0);
} catch (e) {
// Probably due to PR_EXCL.
return;
}
if (sync) {
let utf8String = converter.ConvertFromUnicode(pingString);
utf8String += converter.Finish();
let success = false;
try {
let amount = ostream.write(utf8String, utf8String.length);
success = amount == utf8String.length;
} catch (e) {
}
this.finishTelemetrySave(success, ostream);
} else {
let istream = converter.convertToInputStream(pingString)
let self = this;
NetUtil.asyncCopy(istream, ostream,
function(result) {
self.finishTelemetrySave(Components.isSuccessCode(result),
ostream);
});
}
TelemetryFile.testLoadHistograms(file, sync, (success =>
{
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
success_histogram.add(success);
}));
},
getFlashVersion: function getFlashVersion() {
@ -968,39 +830,15 @@ TelemetryPing.prototype = {
return null;
},
ensurePingDirectory: function ensurePingDirectory() {
let directory = this._savedProfileDirectory.clone();
directory.append("saved-telemetry-pings");
try {
directory.create(Ci.nsIFile.DIRECTORY_TYPE, RWX_OWNER);
} catch (e) {
// Already exists, just ignore this.
}
return directory;
},
saveFileForPing: function saveFileForPing(ping) {
let file = this.ensurePingDirectory();
file.append(ping.slug);
return file;
},
savePing: function savePing(ping, overwrite) {
this.savePingToFile(ping, this.saveFileForPing(ping), true, overwrite);
},
savePendingPings: function savePendingPings() {
let sessionPing = this.getSessionPayloadAndSlug("saved-session");
this.savePing(sessionPing, true);
this._pendingPings.forEach(function sppcb(e, i, a) {
this.savePing(e, false);
}, this);
this._pendingPings = [];
TelemetryFile.savePendingPings(sessionPing);
},
saveHistograms: function saveHistograms(file, sync) {
this.savePingToFile(this.getSessionPayloadAndSlug("saved-session"),
file, sync, true);
TelemetryFile.savePingToFile(
this.getSessionPayloadAndSlug("saved-session"),
file, sync, true);
},
/**
@ -1045,7 +883,7 @@ TelemetryPing.prototype = {
},
enableLoadSaveNotifications: function enableLoadSaveNotifications() {
this._doLoadSaveNotifications = true;
TelemetryFile.shouldNotifyUponSave = true;
},
setAddOns: function setAddOns(aAddOns) {
@ -1069,7 +907,8 @@ TelemetryPing.prototype = {
},
cacheProfileDirectory: function cacheProfileDirectory() {
this._savedProfileDirectory = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
// This method doesn't do anything anymore
return;
},
/**
@ -1079,7 +918,6 @@ TelemetryPing.prototype = {
switch (aTopic) {
case "profile-after-change":
this.setup();
this.cacheProfileDirectory();
break;
case "cycle-collector-begin":
let now = new Date();
@ -1146,7 +984,7 @@ TelemetryPing.prototype = {
case "application-background":
if (Telemetry.canSend) {
let ping = this.getSessionPayloadAndSlug("saved-session");
this.savePing(ping, true);
TelemetryFile.savePing(ping, true);
}
break;
#endif

View File

@ -32,6 +32,7 @@ EXTRA_PP_COMPONENTS += [
]
EXTRA_JS_MODULES += [
'TelemetryFile.jsm',
'TelemetryStopwatch.jsm',
'ThirdPartyCookieProbe.jsm',
]

View File

@ -62,6 +62,7 @@ toolkit.jar:
content/global/bindings/expander.xml (widgets/expander.xml)
* content/global/bindings/filefield.xml (widgets/filefield.xml)
*+ content/global/bindings/findbar.xml (widgets/findbar.xml)
content/global/bindings/findbar.css (widgets/findbar.css)
content/global/bindings/general.xml (widgets/general.xml)
content/global/bindings/groupbox.xml (widgets/groupbox.xml)
*+ content/global/bindings/listbox.xml (widgets/listbox.xml)

View File

@ -0,0 +1,41 @@
/* 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/. */
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
findbar {
transition-property: transform, opacity, visibility;
transition-duration: 120ms, 120ms, 0s;
transition-timing-function: ease-in-out, ease-in-out, linear;
/* The following positioning properties only take an effect during findbar
* transitions. The findbar binding sets position:absolute during that time
* on the findbar.
*/
left: 0;
right: 0;
bottom: 0;
}
findbar[position="top"] {
top: 0;
bottom: auto;
}
findbar > hbox {
width: 100%;
}
findbar[hidden] {
/* Override display:none to make the transition work. */
display: -moz-box;
visibility: collapse;
opacity: 0;
transition-delay: 0s, 0s, 120ms;
transform: translateY(2em);
}
findbar[position="top"][hidden] {
transform: translateY(-2em);
}

View File

@ -173,6 +173,7 @@
<binding id="findbar"
extends="chrome://global/content/bindings/toolbar.xml#toolbar">
<resources>
<stylesheet src="chrome://global/content/bindings/findbar.css"/>
<stylesheet src="chrome://global/skin/findBar.css"/>
</resources>
@ -241,6 +242,8 @@
<field name="_flashFindBar">0</field>
<field name="_initialFlashFindBarCount">6</field>
<field name="_contentScrollOffset">0</field>
<property name="_foundLink"
onget="return this._foundLinkRef.get();"
onset="this._foundLinkRef = Components.utils.getWeakReference(val); return val;"/>
@ -1134,8 +1137,30 @@
this._updateFindUI();
if (this.hidden) {
// Use position:absolute during the transition.
this.style.position = "absolute";
this.parentNode.style.position = "relative";
// Apparently a flush is necessary after setting position:relative
// on our parentNode, otherwise setting hidden to false won't
// animate the transform change.
this.getBoundingClientRect();
this.hidden = false;
// Set a height on the findbar that's at least as much as the
// current height, but guaranteed to be an integer number of
// screen pixels.
// This way, reapplying position:static on the findbar after the
// fade in animation won't cause the browser contents to wiggle.
let [chromeOffset, contentScrollOffset] = this._findOffsets();
this.style.height = chromeOffset + "px";
this._contentScrollOffset = contentScrollOffset;
// Wait for the findbar appearance animation to end before
// changing the browser size.
this.addEventListener("transitionend", this);
this._updateStatusUI(this.nsITypeAheadFind.FIND_FOUND);
let event = document.createEvent("Events");
@ -1178,6 +1203,16 @@
}
this.hidden = true;
this.addEventListener("transitionend", this);
// Revert browser scroll shift + findbar static positioning.
if (this.getAttribute("position") == "top" &&
this.style.position != "absolute") {
this._browser.contentWindow.scrollBy(0, -this._contentScrollOffset);
}
this.style.position = "absolute";
var fastFind = this.browser.fastFind;
fastFind.setSelectionModeAndRepaint
(this.nsISelectionController.SELECTION_ON);
@ -1479,10 +1514,58 @@
case "keypress":
this._onBrowserKeypress(aEvent);
break;
case "transitionend":
if (aEvent.target == this &&
aEvent.propertyName == "transform") {
this.removeEventListener("transitionend", this);
// Change the browser size in such a way that the region that's
// overlapped by the findbar can be scrolled to, but try to
// avoid a visual shift of the browser contents.
this.style.removeProperty("position");
if (this.getAttribute("position") == "top" &&
!this.hidden) {
this._browser.contentWindow.scrollBy(0, this._contentScrollOffset);
}
// We'd like to remove position:relative from this.parentNode,
// but that unfortunately causes unnecessary repainting.
}
break;
}
]]></body>
</method>
<method name="_screenPixelsPerCSSPixel">
<parameter name="aWindow"/>
<body><![CDATA[
return aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils)
.screenPixelsPerCSSPixel;
]]></body>
</method>
<!--
- Find two numbers, one in chrome CSS pixels and one in integer
- content CSS pixels, that are about the same as (but not less than)
- the height of the findbar. These two numbers hopefully map to the
- same number of integer screen pixels.
- We want to avoid shifting of the page even when chrome and content
- have different zoom factors, and scrollBy() only accepts integers.
-->
<method name="_findOffsets">
<body><![CDATA[
let chromeFactor = this._screenPixelsPerCSSPixel(window);
let contentFactor = this._screenPixelsPerCSSPixel(this._browser.contentWindow);
let findbarHeightScreen = this.getBoundingClientRect().height * chromeFactor;
let contentScrollOffset = Math.ceil(findbarHeightScreen / contentFactor);
let estimatedScrollOffsetInScreenPixels = Math.round(contentScrollOffset * contentFactor);
let chromeOffset = estimatedScrollOffsetInScreenPixels / chromeFactor;
return [chromeOffset, contentScrollOffset];
]]></body>
</method>
<method name="_enableFindButtons">
<parameter name="aEnable"/>
<body><![CDATA[

View File

@ -51,6 +51,7 @@ var BuiltinProvider = {
"devtools": "resource:///modules/devtools",
"devtools/server": "resource://gre/modules/devtools/server",
"devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole",
"devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic",
// Allow access to xpcshell test items from the loader.
"xpcshell-test": "resource://test"
@ -85,6 +86,8 @@ var SrcdirProvider = {
let toolkitURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools"));
let serverURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "server"));
let webconsoleURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "webconsole"));
let cssLogicURI = this.fileURI(OS.Path.join(toolkitURI, "styleinspector", "css-logic"));
let mainURI = this.fileURI(OS.Path.join(srcdir, "browser", "devtools", "main.js"));
this.loader = new loader.Loader({
modules: {
@ -95,6 +98,7 @@ var SrcdirProvider = {
"devtools/server": serverURI,
"devtools/toolkit/webconsole": webconsoleURI,
"devtools": devtoolsURI,
"devtools/styleinspector/css-logic": cssLogicURI,
"main": mainURI
},
globals: loaderGlobals

View File

@ -10,5 +10,6 @@ PARALLEL_DIRS += [
'gcli',
'sourcemap',
'webconsole',
'apps'
'apps',
'styleinspector'
]

View File

@ -716,6 +716,15 @@ function DebuggerServerConnection(aPrefix, aTransport)
this._actorPool = new ActorPool(this);
this._extraPools = [];
// Responses to a given actor must be returned the the client
// in the same order as the requests that they're replying to, but
// Implementations might finish serving requests in a different
// order. To keep things in order we generate a promise for each
// request, chained to the promise for the request before it.
// This map stores the latest request promise in the chain, keyed
// by an actor ID string.
this._actorResponses = new Map;
/*
* We can forward packets to other servers, if the actors on that server
* all use a distinct prefix on their names. This is a map from prefixes
@ -942,19 +951,23 @@ DebuggerServerConnection.prototype = {
return;
}
resolve(ret)
.then(function (aResponse) {
if (!aResponse.from) {
aResponse.from = aPacket.to;
}
return aResponse;
})
.then(this.transport.send.bind(this.transport))
.then(null, (e) => {
return this._unknownError(
"error occurred while processing '" + aPacket.type,
e);
});
let pendingResponse = this._actorResponses.get(actor.actorID) || resolve(null);
let response = pendingResponse.then(() => {
return ret;
}).then(aResponse => {
if (!aResponse.from) {
aResponse.from = aPacket.to;
}
this.transport.send(aResponse);
}).then(null, (e) => {
let errorPacket = this._unknownError(
"error occurred while processing '" + aPacket.type,
e);
errorPacket.from = aPacket.to;
this.transport.send(errorPacket);
});
this._actorResponses.set(actor.actorID, response);
},
/**

View File

@ -824,6 +824,12 @@ let Actor = Class({
message: err.toString()
});
},
_queueResponse: function(create) {
let pending = this._pendingResponse || promise.resolve(null);
let response = create(pending);
this._pendingResponse = response;
}
});
exports.Actor = Actor;
@ -915,14 +921,16 @@ let actorProto = function(actorProto) {
conn.send(response);
};
if (ret && ret.then) {
ret.then(sendReturn).then(null, this.writeError.bind(this));
} else {
sendReturn(ret);
}
this._queueResponse(p => {
return p
.then(() => ret)
.then(sendReturn)
.then(null, this.writeError.bind(this));
})
} catch(e) {
this.writeError(e);
this._queueResponse(p => {
return p.then(() => this.writeError(e));
});
}
};

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