Labs/Jetpack/Reboot/JEP/112

From MozillaWiki
< Labs‎ | Jetpack‎ | Reboot‎ | JEP
Revision as of 22:47, 8 February 2010 by Adw (talk | contribs)
Jump to navigation Jump to search

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. No chrome 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 menu element and events and so on.

The prototype made it possible to modify menus both top-down, from the point of view of "the menu module"...

jetpack.menu.context.page.on("#node").add();

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

let menu = new jetpack.Menu();
menu.contextOn(node);

Setting aside the syntax, which is better?

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 contexts. It's wrong for an image-related extension to add a context menu item that shows up even when not clicking on images.

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.

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

This would be nice:

node.contextMenu.add(...);

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 menu element.)

So what about:

let cmModule = require("context-menu");
cmModule.contextMenuFor(node).add(...);

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:

cmModule.on("#foo").add(...);

Unlike the prototype, there is no static way to modify the context menu outside of a context. Consumers can still pass the "*" selector, though. Perhaps the API could explicitly outlaw it.

How about dynamically modifying the menu? The prototype's solutions:

jetpack.menu.context.page.beforeShow = function (menu, context) {};
jetpack.menu.context.page.add(function (context) {});

beforeShow() is essentially a popupshowing event listener. Under the reboot, this?

cmModule.on("#foo").beforeShow = function (menu, context) {};
cmModule.on("#foo").add(function (context) {});

What if the selector isn't known beforehand?

cmModule.beforeShow = function (menu, context) {};
cmModule.add(function (context) {});

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 on() takes. It's a context.

cmModule.on(function ok(context) {}).add(...);

ok() returns true if the context is targeted and false otherwise. Nothing prevents ok() 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:

cmModule.on(function ok(context) {
  var foo = doFoo();
  var baz = doBaz(foo);
  ...
  return qux * qix == 42;
}).add(...);

Wait, what does on() return? It's unnecessary. Each mutating method -- add(), replace(), so on -- should take the context:

cmModule.add("#foo", ...);
cmModule.add(function ok(context) {
  var foo = doFoo();
  var baz = doBaz(foo);
  ...
  return qux * qix == 42;
}, ...);

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:

cmModule.add(ON_SELECTION_CHANGED, ...);

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 polling. Most importantly, dynamic updates allow consumers to examine the context in which the menu was invoked.

Dynamic updates are too useful to disallow. Can we do it smartly?


Important points so far:

  1. 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 context menu and consider alternative UI if their context is truly universal.
  2. Contexts can be specified in different ways: selectors, functions, nodes?, ...
  3. Declarative context specifications are better.
  4. The act of specifying a context is distinct from the act of specifying how to modify the menu.

How's this then?

cmModule.add(context, menuitem);
cmModule.replace(context, target, menuitem);
cmModule.insertBefore(context, target, menuitem);
  • context: Describes the context in which the modifications should occur. It can be any of the following types:
    • string: A CSS selector. The "*" selector is not allowed. (That limitation is easy to circumvent, e.g., "html *"...)
    • 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 function(node). node is the node the user clicked to invoke the menu.
  • target: A string, regular expression, or integer describing an existing menuitem. Same as the prototype's menu implementation.
  • menuitem: 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 command function is called as command(clickedMenuitem, node). node is the node the user clicked to invoke the menu.

Note that, since menuitem 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 context function and then reference that var in menuitem.

What does it mean to do this:

cmModule.add("#foo", menuitem1);
cmModule.add("#foo", menuitem2);

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 item. It also means there's no need for any remove() or reset() or clear() functions. If 1, there is a need.

People will not understand the context param. Instead, 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.