Labs/Jetpack/Reboot/JEP/112: Difference between revisions

From MozillaWiki
< Labs‎ | Jetpack‎ | Reboot‎ | JEP
Jump to navigation Jump to search
(Rewrite)
Line 8: Line 8:
=== Proposal ===
=== Proposal ===


Provide access to the page's context menu.  No chrome context menu.
Provide access to the page's context menu.


HTML5 has a specification for menus and context menus.  Obviously that would be the best solution, but no browser implements it yet.  We can't really fake it, even in the narrow case of context menus; there's a new <tt>menu</tt> element and events and so on.
This proposal recognizes the following principles:


The prototype made it possible to modify menus both top-down, from the point of view of "the menu module"...
# The domain of browser extensions is not a single page but the set of all the user's pages.
# The page's context menu is for UI related to the page and its content. It should not be used simply because it's handy.


<pre class="brush:js;toolbar:false;">
We mention briefly that HTML5 includes a specification for menus, but no browser implements it yet, and its scope is not that of browser extensions anyway.
jetpack.menu.context.page.on("#node").add();
</pre>


... and bottom-up, from the point of view of the node:
=== Use Cases ===


<pre class="brush:js;toolbar:false;">
* Adding an item that acts on a portion of the page -- a node, selection, etc.  Examples:
let menu = new jetpack.Menu();
** An image-editing extension wants to add an "Edit Image" item when the context menu is invoked on an image.
menu.contextOn(node);
** A search extension wants to add a "Search" item when the context menu is invoked while a selection exists.
</pre>
* Adding an item that acts on the entire page.  Examples:
** An extension that makes it easy to edit page source wants to add an "Edit Page Source" item when the context menu is invoked anywhere on the page.
** An image-editing extension wants to add an "Edit Page Images" item when the context menu is invoked anywhere on a page that has at least one image.


Setting aside the syntax, which is better?
=== Non-Use Cases ===


Top-down is akin to applying a CSS rule.  Bottom-up acts directly on the context -- a node.  Bottom-up nudges the developer to be specific about contexts and makes it harder to modify the menu over all contextsIt's <em>wrong</em> for an image-related extension to add a context menu item that shows up even when not clicking on images.
* Adding an item unrelated to the page and its contentExamples:
 
** A tab-related extension wants to add an item that provides access to all the user's tabs.
But, bottom-up is fine in the context of a single page.  It's not very usable at all when you consider all the user's pages, which is exactly the domain of browser extensions. It also fails when nodes are inserted or modified.  Asking the developer to attach load and DOM-modification event handlers to all pages and then find her target nodes in those pages is overly burdensome.  It's bad.  It fails again when considering contexts that are entirely independent of nodes.
** An extension wants to add an item when it's three o'clock.


=== Dependencies & Requirements ===
=== Dependencies & Requirements ===
Line 42: Line 43:
=== API Methods ===
=== API Methods ===


This would be nice:
==== add(context, menuitem) ====
 
<pre class="brush:js;toolbar:false;">
node.contextMenu.add(...);
</pre>
 
But introducing non-standard properties on HTML elements is a terrible solution.  HTML5 defines a "contextmenu" attribute on all HTML elements, but again, Gecko doesn't support it yet.  (Under the spec it's the ID of a <tt>menu</tt> element.)
 
So what about:
 
<pre class="brush:js;toolbar:false;">
let cmModule = require("context-menu");
cmModule.contextMenuFor(node).add(...);
</pre>
 
As discussed above, though, the bottom-up approach, while good for single pages, does not make sense for browser extensions, whose domain is the set of all the user's pages.  Instead of nodes, then, selectors:
 
<pre class="brush:js;toolbar:false;">
cmModule.on("#foo").add(...);
</pre>
 
Unlike the prototype, there is no static way to modify the context menu outside of a context.  Consumers can still pass the <tt>"*"</tt> selector, though.  Perhaps the API could explicitly outlaw it.
 
How about dynamically modifying the menu?  The prototype's solutions:
 
<pre class="brush:js;toolbar:false;">
jetpack.menu.context.page.beforeShow = function (menu, context) {};
jetpack.menu.context.page.add(function (context) {});
</pre>
 
<tt>beforeShow()</tt> is essentially a <tt>popupshowing</tt> event listener.  Under the reboot, this?
 
<pre class="brush:js;toolbar:false;">
cmModule.on("#foo").beforeShow = function (menu, context) {};
cmModule.on("#foo").add(function (context) {});
</pre>
 
What if the selector isn't known beforehand?
 
<pre class="brush:js;toolbar:false;">
cmModule.beforeShow = function (menu, context) {};
cmModule.add(function (context) {});
</pre>
 
Yuck.  That lets people easily add items that stick around in all contexts.  The API should not encourage that.
 
But what if the target context is independent of nodes?  Modifying the menu on selection is a good example.  It's not a selector that <tt>on()</tt> takes.  It's a <em>context</em>.
 
<pre class="brush:js;toolbar:false;">
cmModule.on(function ok(context) {}).add(...);
</pre>
 
<tt>ok()</tt> returns true if the context is targeted and false otherwise.  Nothing prevents <tt>ok()</tt> from always returning true, but at least it makes it harder to modify all contexts without intending to.
 
As a practical matter, though, that could get nasty to write:
 
<pre class="brush:js;toolbar:false;">
cmModule.on(function ok(context) {
  var foo = doFoo();
  var baz = doBaz(foo);
  ...
  return qux * qix == 42;
}).add(...);
</pre>
 
Wait, what does <tt>on()</tt> return?  It's unnecessary.  Each mutating method -- <tt>add()</tt>, <tt>replace()</tt>, so on -- should take the context:
 
<pre class="brush:js;toolbar:false;">
cmModule.add("#foo", ...);
cmModule.add(function ok(context) {
  var foo = doFoo();
  var baz = doBaz(foo);
  ...
  return qux * qix == 42;
}, ...);
</pre>
 
Now it's not so different from what adding event handlers looks like.
 
----
 
bsmedberg has raised concerns about allowing jetpacks to modify the context menu before it's shown, because of Electrolysis.  Jetpacks will run in a separate process from chrome.  (Although under the reboot jetpacks are just extensions, so I don't know how that story changes.  I suspect it doesn't really.)  By letting jetpacks run code before the context menu is shown, they can adversely impact the performance of the browser's front-end.  He stressed the importance of making the API as declarative as possible.  For example, instead of a generic context or "beforeShow" function, things like this:
 
<pre class="brush:js;toolbar:false;">
cmModule.add(ON_SELECTION_CHANGED, ...);
</pre>
 
Clearly there are lots of cases, and not every case can be made declarative.


Consumers could listen for changes and update the menu then, rather than updating when the menu is shown.  But changes are often polled rather than being observed directly.  Being able to update the menu when it's shown lets you do less updates and pollingMost importantly, dynamic updates allow consumers to examine the context in which the menu was invoked.
Adds an item to the context menu.  The position at which the item is inserted is at the discretion of Jetpack.


Dynamic updates are too useful to disallowCan we do it smartly?
; context : An object describing the context under which to add the itemSee [[#Specifying Contexts]].
; menuitem : The menuitem to add.  See [[#Specifying Menuitems]].


----
==== replace(context, target, menuitem) ====


Important points so far:
Replaces an existing item in the context menu.  If the target item does not exist, this function silently fails, and the context menu is not modified.


# The domain of browser extensions is not a single page but the set of all the user's pages.  The API should reflect that.
; context : An object describing the context under which to add the item. See [[#Specifying Contexts]].
# The context menu is for context-related UI.  API consumers must specify a context.  They can game the system by specifying a universal context, but being forced to specify a context nudges them to not abuse the <em>context</em> menu and consider alternative UI if their context is truly universal.
; target : A value describing the existing item to replace. See [[#Specifying Existing Menuitems]].
# Contexts can be specified in different ways: selectors, functions, nodes?, ...
; menuitem : The menuitem to add.  See [[#Specifying Menuitems]].
# Declarative context specifications are better.
# The act of specifying a context is distinct from the act of specifying how to modify the menu.


----
==== insertBefore(context, target, menuitem) ====


How's this then?
Inserts a new item before an existing one.  If the target item does not exist, this function silently fails, and the context menu not modified.


<pre class="brush:js;toolbar:false;">
; context : An object describing the context under which to add the item.  See [[#Specifying Contexts]].
cmModule.add(context, menuitem);
; target : A value describing the existing item before which to insert the new item.  See [[#Specifying Existing Menuitems]].
cmModule.replace(context, target, menuitem);
; menuitem : The menuitem to add. See [[#Specifying Menuitems]].
cmModule.insertBefore(context, target, menuitem);
</pre>


* <tt>context</tt>: Describes the context in which the modifications should occur.  It can be any of the following types:
==== Specifying Contexts ====
** string: A CSS selector.  Selectors that match the <tt>html</tt> and <tt>body</tt> elements (such as <tt>"*"</tt>) are not allowed.
** function: An arbitrary predicate.  Will be invoked just before the menu is shown.  If it returns true, the modifications will occur.  It will be called as <tt>function(node)</tt>.  <tt>node</tt> is the node the user clicked to invoke the menu.
* <tt>target</tt>: A string, regular expression, or integer describing an existing menuitem.  Same as the prototype's menu implementation.
* <tt>menuitem</tt>: The new menuitem to insert.  The legal types are mostly the same as those under the prototype's menu implementation except for these changes:
** Functions are not allowed.
** The <tt>command</tt> function is called as <tt>command(clickedMenuitem, node)</tt>.  <tt>node</tt> is the node the user clicked to invoke the menu.


Note that, since <tt>menuitem</tt> can't be a function, there's no way to modify the menu before it's shown.  Actually, that's not true.  You can cheat and stuff something into an upvar using the <tt>context</tt> function and then reference that var in <tt>menuitem</tt>.
As its name implies, the context menu should be used only when some particular context arises.  The context can be related to page content or the page itself, but it should never be external to the page.


What does it mean to do this:
A context must be specified when modifying the menu.  When the menu is invoked under a specified context, all of the context's associated modifications are applied.  When the menu is invoked without matching any specified context, no modifications are applied.


<pre class="brush:js;toolbar:false;">
Contexts may be specified with any of the following types:
cmModule.add("#foo", menuitem1);
cmModule.add("#foo", menuitem2);
</pre>


Do 1) both menuitems get added, or 2) does the second call override the first?  If 2, that forces extensions to use a submenu if they want more than one itemIt also means there's no need for any <tt>remove()</tt> or <tt>reset()</tt> or <tt>clear()</tt> functions.  If 1, there is a need.
; string : A CSS selectorThe context arises when the menu is invoked on a node that matches this selector.


People will not understand the context paramInstead, they'll add to the menu depending on external context.  e.g., to add an item that appears when page X is browsed, they'll use a page mod to sniff for X and add the item then.  They'll see examples on random sites that pass "*" for context and never understand what it means.
; function : An arbitrary predicateThe context arises when the function returns true.

Revision as of 20:39, 9 February 2010

JEP 112 - Context Menu

  • Champion: Drew Willcoxon - adw at mozilla.com
  • Status: Accepted/In-Queue
  • Bug Ticket:
  • Type: API

Proposal

Provide access to the page's context menu.

This proposal recognizes the following principles:

  1. The domain of browser extensions is not a single page but the set of all the user's pages.
  2. The page's context menu is for UI related to the page and its content. It should not be used simply because it's handy.

We mention briefly that HTML5 includes a specification for menus, but no browser implements it yet, and its scope is not that of browser extensions anyway.

Use Cases

  • Adding an item that acts on a portion of the page -- a node, selection, etc. Examples:
    • An image-editing extension wants to add an "Edit Image" item when the context menu is invoked on an image.
    • A search extension wants to add a "Search" item when the context menu is invoked while a selection exists.
  • Adding an item that acts on the entire page. Examples:
    • An extension that makes it easy to edit page source wants to add an "Edit Page Source" item when the context menu is invoked anywhere on the page.
    • An image-editing extension wants to add an "Edit Page Images" item when the context menu is invoked anywhere on a page that has at least one image.

Non-Use Cases

  • Adding an item unrelated to the page and its content. Examples:
    • A tab-related extension wants to add an item that provides access to all the user's tabs.
    • An extension wants to add an item when it's three o'clock.

Dependencies & Requirements

  • Access to content pages and their DOMs.
  • Access to the browser's DOM, including popupshowing and popuphiding events.

Internal Methods

This JEP doesn't export anything into the Cuddlefish Platform Library.

API Methods

add(context, menuitem)

Adds an item to the context menu. The position at which the item is inserted is at the discretion of Jetpack.

context
An object describing the context under which to add the item. See #Specifying Contexts.
menuitem
The menuitem to add. See #Specifying Menuitems.

replace(context, target, menuitem)

Replaces an existing item in the context menu. If the target item does not exist, this function silently fails, and the context menu is not modified.

context
An object describing the context under which to add the item. See #Specifying Contexts.
target
A value describing the existing item to replace. See #Specifying Existing Menuitems.
menuitem
The menuitem to add. See #Specifying Menuitems.

insertBefore(context, target, menuitem)

Inserts a new item before an existing one. If the target item does not exist, this function silently fails, and the context menu not modified.

context
An object describing the context under which to add the item. See #Specifying Contexts.
target
A value describing the existing item before which to insert the new item. See #Specifying Existing Menuitems.
menuitem
The menuitem to add. See #Specifying Menuitems.

Specifying Contexts

As its name implies, the context menu should be used only when some particular context arises. The context can be related to page content or the page itself, but it should never be external to the page.

A context must be specified when modifying the menu. When the menu is invoked under a specified context, all of the context's associated modifications are applied. When the menu is invoked without matching any specified context, no modifications are applied.

Contexts may be specified with any of the following types:

string
A CSS selector. The context arises when the menu is invoked on a node that matches this selector.
function
An arbitrary predicate. The context arises when the function returns true.