mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-10 11:55:49 +00:00
Merge m-c to inbound.
This commit is contained in:
commit
2ae0ffbbc8
@ -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 ###
|
||||
|
@ -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`.
|
@ -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.
|
@ -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 don’t 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 won’t 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 Bugzilla’s 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!
|
@ -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.
|
@ -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.
|
@ -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">
|
||||
|
@ -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 ##
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -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}
|
||||
|
||||
|
450
addon-sdk/source/doc/module-source/sdk/places/bookmarks.md
Normal file
450
addon-sdk/source/doc/module-source/sdk/places/bookmarks.md
Normal 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>
|
110
addon-sdk/source/doc/module-source/sdk/places/history.md
Normal file
110
addon-sdk/source/doc/module-source/sdk/places/history.md
Normal 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>
|
@ -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
|
||||
|
@ -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 ###
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
121
addon-sdk/source/lib/sdk/places/events.js
Normal file
121
addon-sdk/source/lib/sdk/places/events.js
Normal 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;
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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 = [];
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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']
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 = {}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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).
|
||||
|
@ -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 = {};
|
||||
|
||||
|
292
addon-sdk/source/test/test-places-events.js
Normal file
292
addon-sdk/source/test/test-places-events.js
Normal 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);
|
@ -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));
|
||||
|
||||
|
@ -36,15 +36,15 @@ exports.testPlainTextConsole = function(test) {
|
||||
test.pass("PlainTextConsole instantiates");
|
||||
|
||||
con.log('testing', 1, [2, 3, 4]);
|
||||
test.assertEqual(lastPrint(), "console.log: " + name + ": testing, 1, Array [2,3,4]\n",
|
||||
test.assertEqual(lastPrint(), "console.log: " + name + ": testing 1 Array [2,3,4]\n",
|
||||
"PlainTextConsole.log() must work.");
|
||||
|
||||
con.info('testing', 1, [2, 3, 4]);
|
||||
test.assertEqual(lastPrint(), "console.info: " + name + ": testing, 1, Array [2,3,4]\n",
|
||||
test.assertEqual(lastPrint(), "console.info: " + name + ": testing 1 Array [2,3,4]\n",
|
||||
"PlainTextConsole.info() must work.");
|
||||
|
||||
con.warn('testing', 1, [2, 3, 4]);
|
||||
test.assertEqual(lastPrint(), "console.warn: " + name + ": testing, 1, Array [2,3,4]\n",
|
||||
test.assertEqual(lastPrint(), "console.warn: " + name + ": testing 1 Array [2,3,4]\n",
|
||||
"PlainTextConsole.warn() must work.");
|
||||
|
||||
con.error('testing', 1, [2, 3, 4]);
|
||||
@ -64,20 +64,20 @@ exports.testPlainTextConsole = function(test) {
|
||||
prints = [];
|
||||
|
||||
con.log('testing', undefined);
|
||||
test.assertEqual(lastPrint(), "console.log: " + name + ": testing, undefined\n",
|
||||
test.assertEqual(lastPrint(), "console.log: " + name + ": testing undefined\n",
|
||||
"PlainTextConsole.log() must stringify undefined.");
|
||||
|
||||
con.log('testing', null);
|
||||
test.assertEqual(lastPrint(), "console.log: " + name + ": testing, null\n",
|
||||
test.assertEqual(lastPrint(), "console.log: " + name + ": testing null\n",
|
||||
"PlainTextConsole.log() must stringify null.");
|
||||
|
||||
// TODO: Fix console.jsm to detect custom toString.
|
||||
con.log("testing", { toString: function() "obj.toString()" });
|
||||
test.assertEqual(lastPrint(), "console.log: " + name + ": testing, {}\n",
|
||||
test.assertEqual(lastPrint(), "console.log: " + name + ": testing {}\n",
|
||||
"PlainTextConsole.log() doesn't printify custom toString.");
|
||||
|
||||
con.log("testing", { toString: function() { throw "fail!"; } });
|
||||
test.assertEqual(lastPrint(), "console.log: " + name + ": testing, {}\n",
|
||||
test.assertEqual(lastPrint(), "console.log: " + name + ": testing {}\n",
|
||||
"PlainTextConsole.log() must stringify custom bad toString.");
|
||||
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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',
|
||||
|
@ -17,6 +17,7 @@ html xul|scrollbar {
|
||||
background-image: none !important;
|
||||
border: 0px solid transparent !important;
|
||||
z-index: 2147483647;
|
||||
pointer-events: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
@ -791,6 +791,7 @@ var AlertsHelper = {
|
||||
|
||||
if (!manifestUrl || !manifestUrl.length) {
|
||||
send(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a manifest URL, get the icon and title from the manifest
|
||||
|
@ -40,7 +40,8 @@ category app-startup ProcessGlobal service,@mozilla.org/b2g-process-global;1
|
||||
|
||||
# ContentHandler.js
|
||||
component {d18d0216-d50c-11e1-ba54-efb18d0ef0ac} ContentHandler.js
|
||||
contract @mozilla.org/uriloader/content-handler;1?type=application/pdf {d18d0216-d50c-11e1-ba54-efb18d0ef0ac}
|
||||
contract @mozilla.org/b2g/activities-content-handler;1 {d18d0216-d50c-11e1-ba54-efb18d0ef0ac}
|
||||
category app-startup ContentHandler service,@mozilla.org/b2g/activities-content-handler;1
|
||||
|
||||
# PaymentGlue.js
|
||||
component {8b83eabc-7929-47f4-8b48-4dea8d887e4b} PaymentGlue.js
|
||||
|
@ -18,22 +18,28 @@ XPCOMUtils.defineLazyGetter(this, "cpmm", function() {
|
||||
.getService(Ci.nsIMessageSender);
|
||||
});
|
||||
|
||||
function log(aMsg) {
|
||||
let msg = "ContentHandler.js: " + (aMsg.join ? aMsg.join("") : aMsg);
|
||||
Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService)
|
||||
.logStringMessage(msg);
|
||||
dump(msg + "\n");
|
||||
function debug(aMsg) {
|
||||
//dump("--*-- ContentHandler: " + aMsg + "\n");
|
||||
}
|
||||
|
||||
const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001;
|
||||
function ContentHandler() {
|
||||
|
||||
let ActivityContentFactory = {
|
||||
createInstance: function createInstance(outer, iid) {
|
||||
if (outer != null) {
|
||||
throw Cr.NS_ERROR_NO_AGGREGATION;
|
||||
}
|
||||
return new ActivityContentHandler().QueryInterface(iid);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory])
|
||||
}
|
||||
|
||||
ContentHandler.prototype = {
|
||||
handleContent: function handleContent(aMimetype, aContext, aRequest) {
|
||||
if (aMimetype != PDF_CONTENT_TYPE)
|
||||
throw NS_ERROR_WONT_HANDLE_CONTENT;
|
||||
function ActivityContentHandler() {
|
||||
}
|
||||
|
||||
ActivityContentHandler.prototype = {
|
||||
handleContent: function handleContent(aMimetype, aContext, aRequest) {
|
||||
if (!(aRequest instanceof Ci.nsIChannel))
|
||||
throw NS_ERROR_WONT_HANDLE_CONTENT;
|
||||
|
||||
@ -46,8 +52,96 @@ ContentHandler.prototype = {
|
||||
aRequest.cancel(Cr.NS_BINDING_ABORTED);
|
||||
},
|
||||
|
||||
classID: Components.ID("{d18d0216-d50c-11e1-ba54-efb18d0ef0ac}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler])
|
||||
}
|
||||
|
||||
function ContentHandler() {
|
||||
this.classIdMap = {};
|
||||
}
|
||||
|
||||
ContentHandler.prototype = {
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
if (aTopic == "app-startup") {
|
||||
// We only want to register these from content processes.
|
||||
let appInfo = Cc["@mozilla.org/xre/app-info;1"];
|
||||
if (appInfo.getService(Ci.nsIXULRuntime)
|
||||
.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cpmm.addMessageListener("Activities:RegisterContentTypes", this);
|
||||
cpmm.addMessageListener("Activities:UnregisterContentTypes", this);
|
||||
cpmm.sendAsyncMessage("Activities:GetContentTypes", { });
|
||||
},
|
||||
|
||||
/**
|
||||
* Do the component registration for a content type.
|
||||
* We only need to register one component per content type, even if several
|
||||
* apps provide it, so we keep track of the number of providers for each
|
||||
* content type.
|
||||
*/
|
||||
registerContentHandler: function registerContentHandler(aContentType) {
|
||||
debug("Registering " + aContentType);
|
||||
|
||||
// We already have a provider for this content type, just increase the
|
||||
// tracking count.
|
||||
if (this.classIdMap[aContentType]) {
|
||||
this.classIdMap[aContentType].count++;
|
||||
return;
|
||||
}
|
||||
|
||||
let contractID = "@mozilla.org/uriloader/content-handler;1?type=" +
|
||||
aContentType;
|
||||
let uuidGen = Cc["@mozilla.org/uuid-generator;1"]
|
||||
.getService(Ci.nsIUUIDGenerator);
|
||||
let id = Components.ID(uuidGen.generateUUID().toString());
|
||||
this.classIdMap[aContentType] = { count: 1, id: id };
|
||||
let cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
cr.registerFactory(Components.ID(id), "Activity Content Handler", contractID,
|
||||
ActivityContentFactory);
|
||||
},
|
||||
|
||||
/**
|
||||
* Do the component unregistration for a content type.
|
||||
*/
|
||||
unregisterContentHandler: function registerContentHandler(aContentType) {
|
||||
debug("Unregistering " + aContentType);
|
||||
|
||||
let record = this.classIdMap[aContentType];
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail out if we still have providers left for this content type.
|
||||
if (--record.count > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let contractID = "@mozilla.org/uriloader/content-handler;1?type=" +
|
||||
aContentType;
|
||||
let cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
cr.unregisterFactory(record.id, ActivityContentFactory);
|
||||
delete this.classIdMap[aContentType]
|
||||
},
|
||||
|
||||
receiveMessage: function(aMessage) {
|
||||
let data = aMessage.data;
|
||||
|
||||
switch (aMessage.name) {
|
||||
case "Activities:RegisterContentTypes":
|
||||
data.contentTypes.forEach(this.registerContentHandler, this);
|
||||
break;
|
||||
case "Activities:UnregisterContentTypes":
|
||||
data.contentTypes.forEach(this.unregisterContentHandler, this);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
classID: Components.ID("{d18d0216-d50c-11e1-ba54-efb18d0ef0ac}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler,
|
||||
Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference])
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentHandler]);
|
||||
|
@ -188,7 +188,9 @@ ContentPermissionPrompt.prototype = {
|
||||
this.sendToBrowserWindow("permission-prompt", request, requestId, function(type, remember) {
|
||||
if (type == "permission-allow") {
|
||||
rememberPermission(request.type, principal, !remember);
|
||||
callback();
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
request.allow();
|
||||
return;
|
||||
}
|
||||
@ -202,7 +204,9 @@ ContentPermissionPrompt.prototype = {
|
||||
Ci.nsIPermissionManager.EXPIRE_SESSION, 0);
|
||||
}
|
||||
|
||||
callback();
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
request.cancel();
|
||||
});
|
||||
},
|
||||
|
@ -106,7 +106,7 @@ let Keyboard = {
|
||||
|
||||
switch (msg.name) {
|
||||
case 'Forms:Input':
|
||||
this.forwardEvent('Keyboard:FocusChange', msg);
|
||||
this.handleFocusChange(msg);
|
||||
break;
|
||||
case 'Forms:SelectionChange':
|
||||
case 'Forms:GetText:Result:OK':
|
||||
@ -163,6 +163,17 @@ let Keyboard = {
|
||||
ppmm.broadcastAsyncMessage(newEventName, msg.data);
|
||||
},
|
||||
|
||||
handleFocusChange: function keyboardHandleFocusChange(msg) {
|
||||
this.forwardEvent('Keyboard:FocusChange', msg);
|
||||
|
||||
let browser = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
|
||||
browser.shell.sendChromeEvent({
|
||||
type: 'inputmethod-contextchange',
|
||||
inputType: msg.data.type
|
||||
});
|
||||
},
|
||||
|
||||
setSelectedOption: function keyboardSetSelectedOption(msg) {
|
||||
this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
|
||||
},
|
||||
@ -191,14 +202,14 @@ let Keyboard = {
|
||||
showInputMethodPicker: function keyboardShowInputMethodPicker() {
|
||||
let browser = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
browser.shell.sendChromeEvent({
|
||||
type: "input-method-show-picker"
|
||||
type: "inputmethod-showall"
|
||||
});
|
||||
},
|
||||
|
||||
switchToNextInputMethod: function keyboardSwitchToNextInputMethod() {
|
||||
let browser = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
browser.shell.sendChromeEvent({
|
||||
type: "input-method-switch-to-next"
|
||||
type: "inputmethod-next"
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"revision": "fd03fbd18a09517bc5eb4e2af62314421ae7124a",
|
||||
"revision": "0e3e30e489ff38cceb8b9cf9ee5caea5fb072457",
|
||||
"repo_path": "/integration/gaia-central"
|
||||
}
|
||||
|
@ -762,7 +762,6 @@ var gBrowserInit = {
|
||||
|
||||
// initialize observers and listeners
|
||||
// and give C++ access to gBrowser
|
||||
gBrowser.init();
|
||||
XULBrowserWindow.init();
|
||||
window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(nsIWebNavigation)
|
||||
|
@ -2956,7 +2956,17 @@
|
||||
filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL);
|
||||
this.mTabListeners[0] = tabListener;
|
||||
this.mTabFilters[0] = filter;
|
||||
this.init();
|
||||
|
||||
try {
|
||||
// We assume this can only fail because mCurrentBrowser's docShell
|
||||
// hasn't been created, yet. This may be caused by code accessing
|
||||
// gBrowser before the window has finished loading.
|
||||
this._addProgressListenerForInitialTab();
|
||||
} catch (e) {
|
||||
// The binding was constructed too early, wait until the initial
|
||||
// tab's document is ready, then add the progress listener.
|
||||
this._waitForInitialContentDocument();
|
||||
}
|
||||
|
||||
this.style.backgroundColor =
|
||||
Services.prefs.getBoolPref("browser.display.use_system_colors") ?
|
||||
@ -2970,17 +2980,28 @@
|
||||
]]>
|
||||
</constructor>
|
||||
|
||||
<method name="init">
|
||||
<method name="_addProgressListenerForInitialTab">
|
||||
<body><![CDATA[
|
||||
if (!this._initialProgressListenerAdded) {
|
||||
this._initialProgressListenerAdded = true;
|
||||
try {
|
||||
this.webProgress.addProgressListener(this.mTabFilters[0], Components.interfaces.nsIWebProgress.NOTIFY_ALL);
|
||||
} catch (e) {
|
||||
// The binding was constructed too early, need to try this again later. See bug 463384.
|
||||
this._initialProgressListenerAdded = false;
|
||||
this.webProgress.addProgressListener(this.mTabFilters[0], Ci.nsIWebProgress.NOTIFY_ALL);
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="_waitForInitialContentDocument">
|
||||
<body><![CDATA[
|
||||
let obs = (subject, topic) => {
|
||||
if (this.browsers[0].contentWindow == subject) {
|
||||
Services.obs.removeObserver(obs, topic);
|
||||
this._addProgressListenerForInitialTab();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// We use content-document-global-created as an approximation for
|
||||
// "docShell is initialized". We can do this because in the
|
||||
// mTabProgressListener we care most about the STATE_STOP notification
|
||||
// that will reset mBlank. That means it's important to at least add
|
||||
// the progress listener before the initial about:blank load stops
|
||||
// if we can't do it before the load starts.
|
||||
Services.obs.addObserver(obs, "content-document-global-created", false);
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
|
@ -184,6 +184,7 @@ MOCHITEST_BROWSER_FILES = \
|
||||
browser_bug822367.js \
|
||||
browser_bug832435.js \
|
||||
browser_bug839103.js \
|
||||
browser_bug880101.js \
|
||||
browser_bug882977.js \
|
||||
browser_bug887515.js \
|
||||
browser_canonizeURL.js \
|
||||
|
50
browser/base/content/test/browser_bug880101.js
Normal file
50
browser/base/content/test/browser_bug880101.js
Normal file
@ -0,0 +1,50 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const URL = "about:robots";
|
||||
|
||||
function test() {
|
||||
let win;
|
||||
|
||||
let listener = {
|
||||
onLocationChange: (webProgress, request, uri, flags) => {
|
||||
ok(webProgress.isTopLevel, "Received onLocationChange from top frame");
|
||||
is(uri.spec, URL, "Received onLocationChange for correct URL");
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
// Remove the listener and window when we're done.
|
||||
registerCleanupFunction(() => {
|
||||
win.gBrowser.removeProgressListener(listener);
|
||||
win.close();
|
||||
});
|
||||
|
||||
// Wait for the newly opened window.
|
||||
whenNewWindowOpened(w => win = w);
|
||||
|
||||
// Open a link in a new window.
|
||||
openLinkIn(URL, "window", {});
|
||||
|
||||
// On the next tick, but before the window has finished loading, access the
|
||||
// window's gBrowser property to force the tabbrowser constructor early.
|
||||
(function tryAddProgressListener() {
|
||||
executeSoon(() => {
|
||||
try {
|
||||
win.gBrowser.addProgressListener(listener);
|
||||
} catch (e) {
|
||||
// win.gBrowser wasn't ready, yet. Try again in a tick.
|
||||
tryAddProgressListener();
|
||||
}
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
function whenNewWindowOpened(cb) {
|
||||
Services.obs.addObserver(function obs(win) {
|
||||
Services.obs.removeObserver(obs, "domwindowopened");
|
||||
cb(win);
|
||||
}, "domwindowopened", false);
|
||||
}
|
@ -311,9 +311,6 @@ let SessionStoreInternal = {
|
||||
// states for all recently closed windows
|
||||
_closedWindows: [],
|
||||
|
||||
// not-"dirty" windows usually don't need to have their data updated
|
||||
_dirtyWindows: {},
|
||||
|
||||
// collection of session states yet to be restored
|
||||
_statesToRestore: {},
|
||||
|
||||
@ -488,8 +485,6 @@ let SessionStoreInternal = {
|
||||
this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
|
||||
this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
|
||||
|
||||
this._initEncoding();
|
||||
|
||||
this._performUpgradeBackup();
|
||||
|
||||
this._sessionInitialized = true;
|
||||
@ -524,13 +519,6 @@ let SessionStoreInternal = {
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_initEncoding : function ssi_initEncoding() {
|
||||
// The (UTF-8) encoder used to write to files.
|
||||
XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function () {
|
||||
return new TextEncoder();
|
||||
});
|
||||
},
|
||||
|
||||
_initPrefs : function() {
|
||||
this._prefBranch = Services.prefs.getBranch("browser.");
|
||||
|
||||
@ -1000,7 +988,7 @@ let SessionStoreInternal = {
|
||||
var activeWindow = this._getMostRecentBrowserWindow();
|
||||
if (activeWindow)
|
||||
this.activeWindowSSiCache = activeWindow.__SSi || "";
|
||||
this._dirtyWindows = [];
|
||||
DirtyWindows.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -2544,14 +2532,14 @@ let SessionStoreInternal = {
|
||||
this._forEachBrowserWindow(function(aWindow) {
|
||||
if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore
|
||||
return;
|
||||
if (aUpdateAll || this._dirtyWindows[aWindow.__SSi] || aWindow == activeWindow) {
|
||||
if (aUpdateAll || DirtyWindows.has(aWindow) || aWindow == activeWindow) {
|
||||
this._collectWindowData(aWindow);
|
||||
}
|
||||
else { // always update the window features (whose change alone never triggers a save operation)
|
||||
this._updateWindowFeatures(aWindow);
|
||||
}
|
||||
});
|
||||
this._dirtyWindows = [];
|
||||
DirtyWindows.clear();
|
||||
}
|
||||
|
||||
// collect the data for all windows
|
||||
@ -2682,7 +2670,7 @@ let SessionStoreInternal = {
|
||||
this._windows[aWindow.__SSi].__lastSessionWindowID =
|
||||
aWindow.__SS_lastSessionWindowID;
|
||||
|
||||
this._dirtyWindows[aWindow.__SSi] = false;
|
||||
DirtyWindows.remove(aWindow);
|
||||
},
|
||||
|
||||
/* ........ Restoring Functionality .............. */
|
||||
@ -3010,7 +2998,7 @@ let SessionStoreInternal = {
|
||||
|
||||
// It's important to set the window state to dirty so that
|
||||
// we collect their data for the first time when saving state.
|
||||
this._dirtyWindows[aWindow.__SSi] = true;
|
||||
DirtyWindows.add(aWindow);
|
||||
}
|
||||
|
||||
if (aTabs.length == 0) {
|
||||
@ -3701,7 +3689,7 @@ let SessionStoreInternal = {
|
||||
*/
|
||||
saveStateDelayed: function ssi_saveStateDelayed(aWindow = null, aDelay = 2000) {
|
||||
if (aWindow) {
|
||||
this._dirtyWindows[aWindow.__SSi] = true;
|
||||
DirtyWindows.add(aWindow);
|
||||
}
|
||||
|
||||
if (!this._saveTimer) {
|
||||
@ -4689,6 +4677,28 @@ let DyingWindowCache = {
|
||||
}
|
||||
};
|
||||
|
||||
// A weak set of dirty windows. We use it to determine which windows we need to
|
||||
// recollect data for when _getCurrentState() is called.
|
||||
let DirtyWindows = {
|
||||
_data: new WeakMap(),
|
||||
|
||||
has: function (window) {
|
||||
return this._data.has(window);
|
||||
},
|
||||
|
||||
add: function (window) {
|
||||
return this._data.set(window, true);
|
||||
},
|
||||
|
||||
remove: function (window) {
|
||||
this._data.delete(window);
|
||||
},
|
||||
|
||||
clear: function (window) {
|
||||
this._data.clear();
|
||||
}
|
||||
};
|
||||
|
||||
// A map storing the number of tabs last closed per windoow. This only
|
||||
// stores the most recent tab-close operation, and is used to undo
|
||||
// batch tab-closing operations.
|
||||
|
@ -1920,7 +1920,6 @@ let GroupItems = {
|
||||
minGroupHeight: 110,
|
||||
minGroupWidth: 125,
|
||||
_lastActiveList: null,
|
||||
_lastGroupToUpdateTabBar: null,
|
||||
|
||||
// ----------
|
||||
// Function: toString
|
||||
@ -2286,10 +2285,6 @@ let GroupItems = {
|
||||
});
|
||||
|
||||
this._lastActiveList.remove(groupItem);
|
||||
|
||||
if (this._lastGroupToUpdateTabBar == groupItem)
|
||||
this._lastGroupToUpdateTabBar = null;
|
||||
|
||||
UI.updateTabButton();
|
||||
},
|
||||
|
||||
@ -2423,13 +2418,8 @@ let GroupItems = {
|
||||
|
||||
Utils.assert(this._activeGroupItem, "There must be something to show in the tab bar!");
|
||||
|
||||
// Update list of visible tabs only once after switching to another group.
|
||||
if (this._activeGroupItem == this._lastGroupToUpdateTabBar)
|
||||
return;
|
||||
|
||||
let tabItems = this._activeGroupItem._children;
|
||||
gBrowser.showOnlyTheseTabs(tabItems.map(function(item) item.tab));
|
||||
this._lastGroupToUpdateTabBar = this._activeGroupItem;
|
||||
},
|
||||
|
||||
// ----------
|
||||
@ -2547,7 +2537,7 @@ let GroupItems = {
|
||||
if (tab._tabViewTabItem.parent && tab._tabViewTabItem.parent.id == groupItemId)
|
||||
return;
|
||||
|
||||
let shouldHideTab = false;
|
||||
let shouldUpdateTabBar = false;
|
||||
let shouldShowTabView = false;
|
||||
let groupItem;
|
||||
|
||||
@ -2555,12 +2545,12 @@ let GroupItems = {
|
||||
if (tab.selected) {
|
||||
if (gBrowser.visibleTabs.length > 1) {
|
||||
gBrowser._blurTab(tab);
|
||||
shouldHideTab = true;
|
||||
shouldUpdateTabBar = true;
|
||||
} else {
|
||||
shouldShowTabView = true;
|
||||
}
|
||||
} else {
|
||||
shouldHideTab = true;
|
||||
shouldUpdateTabBar = true
|
||||
}
|
||||
|
||||
// remove tab item from a groupItem
|
||||
@ -2583,8 +2573,8 @@ let GroupItems = {
|
||||
new GroupItem([ tab._tabViewTabItem ], { bounds: box, immediately: true });
|
||||
}
|
||||
|
||||
if (shouldHideTab)
|
||||
gBrowser.hideTab(tab);
|
||||
if (shouldUpdateTabBar)
|
||||
this._updateTabBar();
|
||||
else if (shouldShowTabView)
|
||||
UI.showTabView();
|
||||
},
|
||||
|
@ -99,14 +99,47 @@ function test() {
|
||||
}, aWindow);
|
||||
}
|
||||
|
||||
function testOnWindow(aCallback) {
|
||||
let win = OpenBrowserWindow({private: false});
|
||||
// [624102] check state after return from private browsing
|
||||
let testPrivateBrowsing = function (aWindow) {
|
||||
aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#1', {inBackground: true});
|
||||
aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#2', {inBackground: true});
|
||||
|
||||
let cw = getContentWindow(aWindow);
|
||||
let box = new cw.Rect(20, 20, 250, 200);
|
||||
let groupItem = new cw.GroupItem([], {bounds: box, immediately: true});
|
||||
cw.UI.setActive(groupItem);
|
||||
|
||||
aWindow.gBrowser.selectedTab = aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#3', {inBackground: true});
|
||||
aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#4', {inBackground: true});
|
||||
|
||||
afterAllTabsLoaded(function () {
|
||||
assertNumberOfVisibleTabs(aWindow, 2);
|
||||
|
||||
enterAndLeavePrivateBrowsing(function () {
|
||||
assertNumberOfVisibleTabs(aWindow, 2);
|
||||
aWindow.gBrowser.selectedTab = aWindow.gBrowser.tabs[0];
|
||||
closeGroupItem(cw.GroupItems.groupItems[1], function() {
|
||||
next(aWindow);
|
||||
});
|
||||
});
|
||||
}, aWindow);
|
||||
}
|
||||
|
||||
function testOnWindow(aIsPrivate, aCallback) {
|
||||
let win = OpenBrowserWindow({private: aIsPrivate});
|
||||
win.addEventListener("load", function onLoad() {
|
||||
win.removeEventListener("load", onLoad, false);
|
||||
executeSoon(function() { aCallback(win) });
|
||||
}, false);
|
||||
}
|
||||
|
||||
function enterAndLeavePrivateBrowsing(callback) {
|
||||
testOnWindow(true, function (aWindow) {
|
||||
aWindow.close();
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
// Tests for #624265
|
||||
@ -116,7 +149,10 @@ function test() {
|
||||
tests.push(testDuplicateTab);
|
||||
tests.push(testBackForwardDuplicateTab);
|
||||
|
||||
testOnWindow(function(aWindow) {
|
||||
// Tests for #624102
|
||||
tests.push(testPrivateBrowsing);
|
||||
|
||||
testOnWindow(false, function(aWindow) {
|
||||
loadTabView(function() {
|
||||
next(aWindow);
|
||||
}, aWindow);
|
||||
@ -127,4 +163,4 @@ function loadTabView(callback, aWindow) {
|
||||
showTabView(function () {
|
||||
hideTabView(callback, aWindow);
|
||||
}, aWindow);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -134,7 +134,6 @@ const WebProgress = {
|
||||
browser.messageManager.removeMessageListener(aMessage.name, arguments.callee);
|
||||
aTab._firstPaint = true;
|
||||
aTab.scrolledAreaChanged(true);
|
||||
aTab.updateThumbnailSource();
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
|
@ -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">
|
||||
<xul:box anonid="thumbnail" class="documenttab-thumbnail" />
|
||||
<xul:image anonid="favicon" class="documenttab-favicon"
|
||||
observes="bcast_urlbarState" width="26" height="26"/>
|
||||
<html:canvas anonid="thumbnail-canvas" class="documenttab-thumbnail" />
|
||||
<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>
|
||||
@ -36,12 +35,19 @@
|
||||
</handlers>
|
||||
|
||||
<implementation>
|
||||
<field name="_thumbnail" readonly="true">document.getAnonymousElementByAttribute(this, "anonid", "thumbnail");</field>
|
||||
<field name="thumbnailCanvas" readonly="true">document.getAnonymousElementByAttribute(this, "anonid", "thumbnail-canvas");</field>
|
||||
<field name="_close" readonly="true">document.getAnonymousElementByAttribute(this, "anonid", "close");</field>
|
||||
<field name="_title" readonly="true">document.getAnonymousElementByAttribute(this, "anonid", "title");</field>
|
||||
<field name="_favicon" readonly="true">document.getAnonymousElementByAttribute(this, "anonid", "favicon");</field>
|
||||
<field name="_container" readonly="true">this.parentNode;</field>
|
||||
|
||||
<constructor>
|
||||
<![CDATA[
|
||||
this.thumbnailCanvas.mozOpaque = true;
|
||||
this.thumbnailCanvas.mozImageSmoothingEnabled = true;
|
||||
]]>
|
||||
</constructor>
|
||||
|
||||
<method name="_onClick">
|
||||
<body>
|
||||
<![CDATA[
|
||||
@ -90,14 +96,6 @@
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="updateThumbnailSource">
|
||||
<parameter name="browser"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
this._thumbnail.style.backgroundImage = "-moz-element(#" + browser.id + ")";
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
</implementation>
|
||||
</binding>
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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])
|
||||
};
|
||||
|
@ -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() {
|
||||
|
@ -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"); },
|
||||
|
||||
|
@ -8,6 +8,11 @@ let Ci = Components.interfaces;
|
||||
let Cu = Components.utils;
|
||||
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
|
||||
@ -16,8 +21,13 @@ const kSetInactiveStateTimeout = 100;
|
||||
|
||||
const kDefaultMetadata = { autoSize: false, allowZoom: true, autoScale: true };
|
||||
|
||||
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");
|
||||
@ -171,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);
|
||||
}
|
||||
}
|
||||
@ -187,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;
|
||||
@ -286,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;
|
||||
@ -556,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;
|
||||
@ -1454,6 +1468,7 @@ function Tab(aURI, aParams, aOwner) {
|
||||
this._chromeTab = null;
|
||||
this._metadata = null;
|
||||
this._eventDeferred = null;
|
||||
this._updateThumbnailTimeout = null;
|
||||
|
||||
this.owner = aOwner || null;
|
||||
|
||||
@ -1603,13 +1618,45 @@ Tab.prototype = {
|
||||
self._eventDeferred = null;
|
||||
}
|
||||
browser.addEventListener("pageshow", onPageShowEvent, true);
|
||||
browser.messageManager.addMessageListener("Content:StateChange", this);
|
||||
Services.obs.addObserver(this, "metro_viewstate_changed", false);
|
||||
|
||||
if (aOwner)
|
||||
this._copyHistoryFrom(aOwner);
|
||||
this._loadUsingParams(browser, aURI, aParams);
|
||||
},
|
||||
|
||||
receiveMessage: function(aMessage) {
|
||||
switch (aMessage.name) {
|
||||
case "Content:StateChange":
|
||||
// update the thumbnail now...
|
||||
this.updateThumbnail();
|
||||
// ...and in a little while to capture page after load.
|
||||
if (aMessage.json.stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
|
||||
clearTimeout(this._updateThumbnailTimeout);
|
||||
this._updateThumbnailTimeout = setTimeout(() => {
|
||||
this.updateThumbnail();
|
||||
}, kTabThumbnailDelayCapture);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
observe: function BrowserUI_observe(aSubject, aTopic, aData) {
|
||||
switch (aTopic) {
|
||||
case "metro_viewstate_changed":
|
||||
if (aData !== "snapped") {
|
||||
this.updateThumbnail();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function destroy() {
|
||||
this._browser.messageManager.removeMessageListener("Content:StateChange", this);
|
||||
Services.obs.removeObserver(this, "metro_viewstate_changed", false);
|
||||
clearTimeout(this._updateThumbnailTimeout);
|
||||
|
||||
Elements.tabList.removeTab(this._chromeTab);
|
||||
this._chromeTab = null;
|
||||
this._destroyBrowser();
|
||||
@ -1818,8 +1865,8 @@ Tab.prototype = {
|
||||
return this.metadata.allowZoom && !Util.isURLEmpty(this.browser.currentURI.spec);
|
||||
},
|
||||
|
||||
updateThumbnailSource: function updateThumbnailSource() {
|
||||
this._chromeTab.updateThumbnailSource(this._browser);
|
||||
updateThumbnail: function updateThumbnail() {
|
||||
PageThumbs.captureToCanvas(this.browser.contentWindow, this._chromeTab.thumbnailCanvas);
|
||||
},
|
||||
|
||||
updateFavicon: function updateFavicon() {
|
||||
|
@ -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,49 +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>
|
||||
|
||||
<!-- Spacer to take extra space in snapped mode. -->
|
||||
<spacer flex="999"/>
|
||||
</scrollbox>
|
||||
|
||||
</hbox>
|
||||
</vbox> <!-- end tray -->
|
||||
|
||||
<!-- Content viewport -->
|
||||
@ -270,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>
|
||||
@ -294,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()"/>
|
||||
|
@ -107,7 +107,7 @@ var FindHelperUI = {
|
||||
},
|
||||
|
||||
show: function findHelperShow() {
|
||||
if (StartUI.isVisible || this._open)
|
||||
if (BrowserUI.isStartTabVisible || this._open)
|
||||
return;
|
||||
|
||||
// Hide any menus
|
||||
|
@ -12,7 +12,7 @@ var AutofillMenuUI = {
|
||||
|
||||
get _panel() { return document.getElementById("autofill-container"); },
|
||||
get _popup() { return document.getElementById("autofill-popup"); },
|
||||
get _commands() { return this._popup.childNodes[0]; },
|
||||
get commands() { return this._popup.childNodes[0]; },
|
||||
|
||||
get _menuPopup() {
|
||||
if (!this.__menuPopup) {
|
||||
@ -32,8 +32,8 @@ var AutofillMenuUI = {
|
||||
},
|
||||
|
||||
_emptyCommands: function _emptyCommands() {
|
||||
while (this._commands.firstChild)
|
||||
this._commands.removeChild(this._commands.firstChild);
|
||||
while (this.commands.firstChild)
|
||||
this.commands.removeChild(this.commands.firstChild);
|
||||
},
|
||||
|
||||
_positionOptions: function _positionOptions() {
|
||||
@ -57,7 +57,7 @@ var AutofillMenuUI = {
|
||||
label.setAttribute("value", aSuggestionsList[idx].label);
|
||||
item.setAttribute("data", aSuggestionsList[idx].value);
|
||||
item.appendChild(label);
|
||||
this._commands.appendChild(item);
|
||||
this.commands.appendChild(item);
|
||||
}
|
||||
|
||||
this._menuPopup.show(this._positionOptions());
|
||||
@ -65,7 +65,7 @@ var AutofillMenuUI = {
|
||||
|
||||
selectByIndex: function mn_selectByIndex(aIndex) {
|
||||
this._menuPopup.hide();
|
||||
FormHelperUI.doAutoComplete(this._commands.childNodes[aIndex].getAttribute("data"));
|
||||
FormHelperUI.doAutoComplete(this.commands.childNodes[aIndex].getAttribute("data"));
|
||||
},
|
||||
|
||||
hide: function hide () {
|
||||
@ -85,7 +85,7 @@ var ContextMenuUI = {
|
||||
|
||||
get _panel() { return document.getElementById("context-container"); },
|
||||
get _popup() { return document.getElementById("context-popup"); },
|
||||
get _commands() { return this._popup.childNodes[0]; },
|
||||
get commands() { return this._popup.childNodes[0]; },
|
||||
|
||||
get _menuPopup() {
|
||||
if (!this.__menuPopup) {
|
||||
@ -153,12 +153,12 @@ var ContextMenuUI = {
|
||||
contentTypes.indexOf("selected-text") != -1))
|
||||
multipleMediaTypes = true;
|
||||
|
||||
for (let command of Array.slice(this._commands.childNodes)) {
|
||||
for (let command of Array.slice(this.commands.childNodes)) {
|
||||
command.hidden = true;
|
||||
}
|
||||
|
||||
let optionsAvailable = false;
|
||||
for (let command of Array.slice(this._commands.childNodes)) {
|
||||
for (let command of Array.slice(this.commands.childNodes)) {
|
||||
let types = command.getAttribute("type").split(",");
|
||||
let lowPriority = (command.hasAttribute("priority") &&
|
||||
command.getAttribute("priority") == "low");
|
||||
@ -221,7 +221,7 @@ var MenuControlUI = {
|
||||
|
||||
get _panel() { return document.getElementById("menucontrol-container"); },
|
||||
get _popup() { return document.getElementById("menucontrol-popup"); },
|
||||
get _commands() { return this._popup.childNodes[0]; },
|
||||
get commands() { return this._popup.childNodes[0]; },
|
||||
|
||||
get _menuPopup() {
|
||||
if (!this.__menuPopup) {
|
||||
@ -240,8 +240,8 @@ var MenuControlUI = {
|
||||
},
|
||||
|
||||
_emptyCommands: function _emptyCommands() {
|
||||
while (this._commands.firstChild)
|
||||
this._commands.removeChild(this._commands.firstChild);
|
||||
while (this.commands.firstChild)
|
||||
this.commands.removeChild(this.commands.firstChild);
|
||||
},
|
||||
|
||||
_positionOptions: function _positionOptions() {
|
||||
@ -314,7 +314,7 @@ var MenuControlUI = {
|
||||
label.setAttribute("value", child.label);
|
||||
item.appendChild(label);
|
||||
|
||||
this._commands.appendChild(item);
|
||||
this.commands.appendChild(item);
|
||||
}
|
||||
|
||||
this._menuPopup.show(this._positionOptions());
|
||||
@ -338,84 +338,25 @@ function MenuPopup(aPanel, aPopup) {
|
||||
this._panel = aPanel;
|
||||
this._popup = aPopup;
|
||||
this._wantTypeBehind = false;
|
||||
this._willReshowPopup = false;
|
||||
|
||||
window.addEventListener('MozAppbarShowing', this, false);
|
||||
}
|
||||
MenuPopup.prototype = {
|
||||
get _visible() { return !this._panel.hidden; },
|
||||
get _commands() { return this._popup.childNodes[0]; },
|
||||
get visible() { return !this._panel.hidden; },
|
||||
get commands() { return this._popup.childNodes[0]; },
|
||||
|
||||
show: function (aPositionOptions) {
|
||||
if (this._visible) {
|
||||
this._willReshowPopup = true;
|
||||
let self = this;
|
||||
this._panel.addEventListener("transitionend", function () {
|
||||
self._show(aPositionOptions);
|
||||
self._panel.removeEventListener("transitionend", arguments.callee);
|
||||
});
|
||||
if (this.visible) {
|
||||
this._animateHide().then(() => this._animateShow(aPositionOptions));
|
||||
} else {
|
||||
this._show(aPositionOptions);
|
||||
this._animateShow(aPositionOptions);
|
||||
}
|
||||
},
|
||||
|
||||
_show: function (aPositionOptions) {
|
||||
window.addEventListener("keypress", this, true);
|
||||
window.addEventListener("mousedown", this, true);
|
||||
Elements.stack.addEventListener("PopupChanged", this, false);
|
||||
Elements.browsers.addEventListener("PanBegin", this, false);
|
||||
|
||||
this._panel.hidden = false;
|
||||
this._position(aPositionOptions || {});
|
||||
|
||||
let self = this;
|
||||
this._panel.addEventListener("transitionend", function () {
|
||||
self._panel.removeEventListener("transitionend", arguments.callee);
|
||||
self._panel.removeAttribute("showingfrom");
|
||||
|
||||
let eventName = self._willReshowPopup ? "popupmoved" : "popupshown";
|
||||
let event = document.createEvent("Events");
|
||||
event.initEvent(eventName, true, false);
|
||||
self._panel.dispatchEvent(event);
|
||||
|
||||
self._willReshowPopup = false;
|
||||
});
|
||||
|
||||
let popupFrom = !aPositionOptions.bottomAligned ? "above" : "below";
|
||||
this._panel.setAttribute("showingfrom", popupFrom);
|
||||
|
||||
// Ensure the panel actually gets shifted before getting animated
|
||||
setTimeout(function () {
|
||||
self._panel.setAttribute("showing", "true");
|
||||
}, 0);
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
if (!this._visible)
|
||||
return;
|
||||
|
||||
window.removeEventListener("keypress", this, true);
|
||||
window.removeEventListener("mousedown", this, true);
|
||||
Elements.stack.removeEventListener("PopupChanged", this, false);
|
||||
Elements.browsers.removeEventListener("PanBegin", this, false);
|
||||
|
||||
let self = this;
|
||||
this._panel.addEventListener("transitionend", function () {
|
||||
self._panel.removeEventListener("transitionend", arguments.callee);
|
||||
self._panel.removeAttribute("hiding");
|
||||
self._panel.hidden = true;
|
||||
self._popup.style.maxWidth = "none";
|
||||
self._popup.style.maxHeight = "none";
|
||||
|
||||
if (!self._willReshowPopup) {
|
||||
let event = document.createEvent("Events");
|
||||
event.initEvent("popuphidden", true, false);
|
||||
self._panel.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
this._panel.setAttribute("hiding", "true");
|
||||
setTimeout(()=>this._panel.removeAttribute("showing"), 0);
|
||||
if (this.visible) {
|
||||
this._animateHide();
|
||||
}
|
||||
},
|
||||
|
||||
_position: function _position(aPositionOptions) {
|
||||
@ -440,7 +381,7 @@ MenuPopup.prototype = {
|
||||
// Add padding on the side of the menu per the user's hand preference
|
||||
let leftHand = MetroUtils.handPreference == MetroUtils.handPreferenceLeft;
|
||||
if (aSource && aSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) {
|
||||
this._commands.setAttribute("left-hand", leftHand);
|
||||
this.commands.setAttribute("left-hand", leftHand);
|
||||
}
|
||||
|
||||
if (aPositionOptions.rightAligned)
|
||||
@ -486,6 +427,66 @@ MenuPopup.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
_animateShow: function (aPositionOptions) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
window.addEventListener("keypress", this, true);
|
||||
window.addEventListener("click", this, true);
|
||||
Elements.stack.addEventListener("PopupChanged", this, false);
|
||||
Elements.browsers.addEventListener("PanBegin", this, false);
|
||||
|
||||
this._panel.hidden = false;
|
||||
let popupFrom = !aPositionOptions.bottomAligned ? "above" : "below";
|
||||
this._panel.setAttribute("showingfrom", popupFrom);
|
||||
|
||||
// This triggers a reflow, which sets transitionability.
|
||||
// All animation/transition setup must happen before here.
|
||||
this._position(aPositionOptions || {});
|
||||
|
||||
let self = this;
|
||||
this._panel.addEventListener("transitionend", function popupshown () {
|
||||
self._panel.removeEventListener("transitionend", popupshown);
|
||||
self._panel.removeAttribute("showingfrom");
|
||||
|
||||
self._dispatch("popupshown");
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
this._panel.setAttribute("showing", "true");
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
_animateHide: function () {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
window.removeEventListener("keypress", this, true);
|
||||
window.removeEventListener("click", this, true);
|
||||
Elements.stack.removeEventListener("PopupChanged", this, false);
|
||||
Elements.browsers.removeEventListener("PanBegin", this, false);
|
||||
|
||||
let self = this;
|
||||
this._panel.addEventListener("transitionend", function popuphidden() {
|
||||
self._panel.removeEventListener("transitionend", popuphidden);
|
||||
self._panel.removeAttribute("hiding");
|
||||
self._panel.hidden = true;
|
||||
self._popup.style.maxWidth = "none";
|
||||
self._popup.style.maxHeight = "none";
|
||||
|
||||
self._dispatch("popuphidden");
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
this._panel.setAttribute("hiding", "true");
|
||||
this._panel.removeAttribute("showing");
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
_dispatch: function _dispatch(aName) {
|
||||
let event = document.createEvent("Events");
|
||||
event.initEvent(aName, true, false);
|
||||
this._panel.dispatchEvent(event);
|
||||
},
|
||||
|
||||
handleEvent: function handleEvent(aEvent) {
|
||||
switch (aEvent.type) {
|
||||
case "keypress":
|
||||
@ -497,7 +498,7 @@ MenuPopup.prototype = {
|
||||
this.hide();
|
||||
}
|
||||
break;
|
||||
case "mousedown":
|
||||
case "click":
|
||||
if (!this._popup.contains(aEvent.target)) {
|
||||
aEvent.stopPropagation();
|
||||
this.hide();
|
||||
|
@ -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) {
|
||||
|
385
browser/metro/base/content/startui/BookmarksView.js
Normal file
385
browser/metro/base/content/startui/BookmarksView.js
Normal 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])
|
||||
};
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
@ -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() {
|
78
browser/metro/base/content/startui/Start.xul
Normal file
78
browser/metro/base/content/startui/Start.xul
Normal 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>
|
151
browser/metro/base/content/startui/StartUI.js
Normal file
151
browser/metro/base/content/startui/StartUI.js
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
304
browser/metro/base/content/startui/TopSitesView.js
Normal file
304
browser/metro/base/content/startui/TopSitesView.js
Normal 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();
|
||||
}
|
||||
};
|
83
browser/metro/base/content/startui/startui-scripts.js
Normal file
83
browser/metro/base/content/startui/startui-scripts.js
Normal 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
|
@ -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
|
||||
|
@ -10,19 +10,17 @@ relativesrcdir = @relativesrcdir@
|
||||
|
||||
include $(DEPTH)/config/autoconf.mk
|
||||
|
||||
# Disabled for intermittent failures
|
||||
# Bug 880739
|
||||
# browser_context_menu_tests.js \
|
||||
# browser_context_menu_tests_01.html \
|
||||
# browser_context_menu_tests_02.html \
|
||||
# browser_context_menu_tests_03.html \
|
||||
|
||||
MOCHITEST_METRO_FILES = \
|
||||
head.js \
|
||||
browser_urlbar.js \
|
||||
browser_bookmarks.js \
|
||||
browser_canonizeURL.js \
|
||||
browser_circular_progress_indicator.js \
|
||||
browser_context_menu_tests.js \
|
||||
browser_context_menu_tests_01.html \
|
||||
browser_context_menu_tests_02.html \
|
||||
browser_context_menu_tests_03.html \
|
||||
browser_context_menu_tests_04.html \
|
||||
browser_context_ui.js \
|
||||
browser_downloads.js \
|
||||
browser_findbar.js \
|
||||
|
@ -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() {
|
||||
|
@ -58,7 +58,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
// selected text context:
|
||||
checkContextUIMenuItemVisibility(["context-copy",
|
||||
@ -91,7 +91,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
// selected text context:
|
||||
checkContextUIMenuItemVisibility(["context-copy",
|
||||
@ -113,7 +113,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
// selected text context:
|
||||
checkContextUIMenuItemVisibility(["context-open-in-new-tab",
|
||||
@ -135,7 +135,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
checkContextUIMenuItemVisibility(["context-select",
|
||||
"context-select-all"]);
|
||||
@ -159,7 +159,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
checkContextUIMenuItemVisibility(["context-cut",
|
||||
"context-copy"]);
|
||||
@ -191,7 +191,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
// selected text context:
|
||||
checkContextUIMenuItemVisibility(["context-cut",
|
||||
@ -212,7 +212,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
// selected text context:
|
||||
checkContextUIMenuItemVisibility(["context-cut",
|
||||
@ -236,7 +236,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
checkContextUIMenuItemVisibility(["context-cut",
|
||||
"context-copy"]);
|
||||
@ -272,7 +272,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
// selected text context:
|
||||
checkContextUIMenuItemVisibility(["context-paste"]);
|
||||
@ -295,7 +295,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should *not* be visible
|
||||
ok(!ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(!ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
// the test above will invoke the app bar
|
||||
yield hideContextUI();
|
||||
@ -336,7 +336,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible and at a specific position
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
let notificationBox = Browser.getNotificationBox();
|
||||
let notification = notificationBox.getNotificationWithValue("popup-blocked");
|
||||
@ -352,6 +352,9 @@ gTests.push({
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
XXX code used to diagnose bug 880739
|
||||
|
||||
var observeLogger = {
|
||||
observe: function (aSubject, aTopic, aData) {
|
||||
info("observeLogger: " + aTopic);
|
||||
@ -383,15 +386,18 @@ var observeLogger = {
|
||||
Services.obs.removeObserver(observeLogger, "dl-cancel");
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Image context menu tests
|
||||
gTests.push({
|
||||
desc: "image context menu",
|
||||
setUp: function() {
|
||||
observeLogger.init();
|
||||
// XXX code used to diagnose bug 880739
|
||||
//observeLogger.init();
|
||||
},
|
||||
tearDown: function() {
|
||||
observeLogger.shutdown();
|
||||
// XXX code used to diagnose bug 880739
|
||||
//observeLogger.shutdown();
|
||||
},
|
||||
run: function test() {
|
||||
info(chromeRoot + "browser_context_menu_tests_01.html");
|
||||
@ -411,7 +417,7 @@ gTests.push({
|
||||
// Context menu options
|
||||
/*
|
||||
XXX disabled temporarily due to bug 880739
|
||||
|
||||
|
||||
// image01 - 1x1x100x100
|
||||
let promise = waitForEvent(document, "popupshown");
|
||||
sendContextMenuClickToWindow(win, 10, 10);
|
||||
@ -419,7 +425,7 @@ gTests.push({
|
||||
|
||||
purgeEventQueue();
|
||||
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
checkContextUIMenuItemVisibility(["context-save-image-lib",
|
||||
"context-copy-image",
|
||||
@ -465,7 +471,7 @@ gTests.push({
|
||||
let promise = waitForEvent(document, "popupshown");
|
||||
sendContextMenuClickToWindow(win, 20, 20);
|
||||
yield promise;
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
let menuItem = document.getElementById("context-copy-image");
|
||||
ok(menuItem, "menu item exists");
|
||||
@ -486,7 +492,7 @@ gTests.push({
|
||||
promise = waitForEvent(document, "popupshown");
|
||||
sendContextMenuClickToWindow(win, 30, 30);
|
||||
yield promise;
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
menuItem = document.getElementById("context-copy-image-loc");
|
||||
ok(menuItem, "menu item exists");
|
||||
@ -519,7 +525,7 @@ gTests.push({
|
||||
promise = waitForEvent(document, "popupshown");
|
||||
sendContextMenuClickToWindow(win, 40, 40);
|
||||
yield promise;
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
menuItem = document.getElementById("context-open-image-tab");
|
||||
ok(menuItem, "menu item exists");
|
||||
@ -564,7 +570,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
checkContextMenuPositionRange(ContextMenuUI._panel, 265, 280, 175, 190);
|
||||
|
||||
@ -579,7 +585,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
checkContextMenuPositionRange(ContextMenuUI._panel, 265, 280, 95, 110);
|
||||
|
||||
@ -594,7 +600,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
checkContextMenuPositionRange(ContextMenuUI._panel, 295, 310, 540, 555);
|
||||
|
||||
@ -609,7 +615,7 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
checkContextMenuPositionRange(ContextMenuUI._panel, 295, 310, 340, 355);
|
||||
|
||||
@ -624,16 +630,87 @@ gTests.push({
|
||||
yield promise;
|
||||
|
||||
// should be visible
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
checkContextMenuPositionRange(ContextMenuUI._panel, 265, 280, 110, 125);
|
||||
|
||||
promise = waitForEvent(document, "popuphidden");
|
||||
ContextMenuUI.hide();
|
||||
yield promise;
|
||||
|
||||
Browser.closeTab(Browser.selectedTab, { forceClose: true });
|
||||
}
|
||||
});
|
||||
|
||||
function reopenSetUp() {
|
||||
info(chromeRoot + "browser_context_menu_tests_04.html");
|
||||
yield addTab(chromeRoot + "browser_context_menu_tests_04.html");
|
||||
|
||||
// Sometimes the context UI won't actually show up.
|
||||
// Since we're just normalizing, we don't want waitForCondition
|
||||
// to cause an orange, so we're putting a try/catch here.
|
||||
try {
|
||||
yield waitForCondition(() => ContextUI.isVisible);
|
||||
ContextUI.dismiss();
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function reopenTearDown() {
|
||||
let promise = waitForEvent(document, "popuphidden")
|
||||
ContextMenuUI.hide();
|
||||
yield promise;
|
||||
ok(!ContextMenuUI._menuPopup.visible, "popup is actually hidden");
|
||||
|
||||
Browser.closeTab(Browser.selectedTab, { forceClose: true });
|
||||
}
|
||||
|
||||
function getReopenTest(aElementInputFn, aWindowInputFn) {
|
||||
return function () {
|
||||
let win = Browser.selectedTab.browser.contentWindow;
|
||||
let panel = ContextMenuUI._menuPopup._panel;
|
||||
|
||||
let link1 = win.document.getElementById("text1-link");
|
||||
let link2 = win.document.getElementById("text2-link");
|
||||
|
||||
// Show the menu on link 1
|
||||
let showpromise = waitForEvent(panel, "popupshown");
|
||||
aElementInputFn(win, link1);
|
||||
|
||||
ok((yield showpromise), "popupshown event fired");
|
||||
ok(ContextMenuUI._menuPopup.visible, "initial popup is visible");
|
||||
|
||||
// Show the menu on link 2
|
||||
let hidepromise = waitForEvent(panel, "popuphidden");
|
||||
showpromise = waitForEvent(panel, "popupshown");
|
||||
aElementInputFn(win, link2);
|
||||
|
||||
ok((yield hidepromise), "popuphidden event fired");
|
||||
ok((yield showpromise), "popupshown event fired");
|
||||
ok(ContextMenuUI._menuPopup.visible, "popup is still visible");
|
||||
|
||||
// Hide the menu
|
||||
hidepromise = waitForEvent(panel, "popuphidden")
|
||||
aWindowInputFn(win, 10, 10);
|
||||
|
||||
ok((yield hidepromise), "popuphidden event fired");
|
||||
ok(!ContextMenuUI._menuPopup.visible, "popup is no longer visible");
|
||||
}
|
||||
}
|
||||
|
||||
gTests.push({
|
||||
desc: "bug 856264 - mouse - context menu should reopen on other links",
|
||||
setUp: reopenSetUp,
|
||||
tearDown: reopenTearDown,
|
||||
run: getReopenTest(sendContextMenuMouseClickToElement, sendMouseClick)
|
||||
});
|
||||
|
||||
gTests.push({
|
||||
desc: "bug 856264 - touch - context menu should reopen on other links",
|
||||
setUp: reopenSetUp,
|
||||
tearDown: reopenTearDown,
|
||||
run: getReopenTest(sendContextMenuClickToElement, sendTap)
|
||||
});
|
||||
|
||||
function test() {
|
||||
runTests();
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
</style>
|
||||
</head>
|
||||
<body style="padding: 10px; margin: 10px;">
|
||||
<div style="margin: 0; padding: 200px 0;">
|
||||
<span id="text1">hello, I'm sorry but I <a id="text1-link" href="#test">must be going</a>.</span>
|
||||
</div>
|
||||
<div style="margin: 0; padding: 200px 0;">
|
||||
<span id="text2"><a id="text2-link" href="#test">hello, I'm sorry but</a> I must be going.</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -29,8 +29,8 @@ function checkAutofillMenuItemContents(aItemList)
|
||||
{
|
||||
let errors = 0;
|
||||
let found = 0;
|
||||
for (let idx = 0; idx < AutofillMenuUI._commands.childNodes.length; idx++) {
|
||||
let item = AutofillMenuUI._commands.childNodes[idx];
|
||||
for (let idx = 0; idx < AutofillMenuUI.commands.childNodes.length; idx++) {
|
||||
let item = AutofillMenuUI.commands.childNodes[idx];
|
||||
let label = item.firstChild.getAttribute("value");
|
||||
let value = item.getAttribute("data");
|
||||
if (aItemList.indexOf(value) == -1) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -31,7 +31,7 @@ gTests.push({
|
||||
yield addTab(chromeRoot + "browser_selection_caretfocus.html");
|
||||
|
||||
yield waitForCondition(function () {
|
||||
return !StartUI.isStartPageVisible;
|
||||
return !BrowserUI.isStartTabVisible;
|
||||
});
|
||||
|
||||
yield hideContextUI();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
@ -138,7 +138,7 @@ gTests.push({
|
||||
|
||||
yield promise;
|
||||
ok(promise && !(promise instanceof Error), "promise error");
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
|
||||
let menuItem = document.getElementById("context-copy");
|
||||
ok(menuItem, "menu item exists");
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -44,7 +44,7 @@ gTests.push({
|
||||
yield addTab(chromeRoot + "browser_selection_inputs.html");
|
||||
|
||||
yield waitForCondition(function () {
|
||||
return !StartUI.isStartPageVisible;
|
||||
return !BrowserUI.isStartTabVisible;
|
||||
});
|
||||
|
||||
yield hideContextUI();
|
||||
|
@ -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();
|
||||
|
@ -23,7 +23,7 @@ gTests.push({
|
||||
yield addTab(chromeRoot + "res/textblock01.html");
|
||||
|
||||
yield waitForCondition(function () {
|
||||
return !StartUI.isStartPageVisible;
|
||||
return !BrowserUI.isStartTabVisible;
|
||||
});
|
||||
|
||||
yield hideContextUI();
|
||||
@ -78,7 +78,7 @@ gTests.push({
|
||||
sendContextMenuClickToElement(window, edit);
|
||||
yield waitForEvent(document, "popupshown");
|
||||
|
||||
ok(ContextMenuUI._menuPopup._visible, "is visible");
|
||||
ok(ContextMenuUI._menuPopup.visible, "is visible");
|
||||
let paste = document.getElementById("context-paste");
|
||||
ok(!paste.hidden, "paste item is visible");
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user