Skip to content
This repository has been archived by the owner on Feb 26, 2022. It is now read-only.

JEP Navbar Buttons

ZER0 edited this page Apr 18, 2013 · 15 revisions

This Button component is the foundation of mostly all the new UI components we're going to introduce in jetpack.

Requirements

  • two button modes: 'Toggle' and 'Action'
  • toggle mode has two visual states, on & off
  • when the button is in 'toggle' mode, the api allows developers to react to this change.
  • when the api is in 'action' mode, the api allows developers to assign a callback to handle the event created when a user clicks on the button.
  • an arbitrary number of buttons can be visually grouped together
  • following UI specs, the size of a button can be only 16x16 (default), 32x16 or 64x16.
  • button has an icon and a label. However, in default Firefox settings label for button are hidden. If an icon is not provided, the generic one is used (the puzzle's piece)
  • button can be disabled
  • button can have a badge to display text (just few characters, tipically numbers), see Google Chrome API for example.

Note

I will personally leave out the "grouping" feature for the first iteration, because to me it's a different kind of object – it's not just a "visual thing", but also behavioral, see the proposal below – and should be implemented separately. Plus, we do not have any UX mockup yet for it. Same could be applied for the "badge" feature, we still missing the UX mockup, but maybe in this case it's more strictly related to the button itself, and could be implemented in the first iteration, but maybe makes sense to leave out as well for the moment.

I'd like also to implement Image's Sprite, in order to avoid to use several images; and an 'accessKeyfeature (or 'shortcut') where we pass a combination keys that automatically trigger the button. This it will be possible in any case usingsdk/hotkeys` module and writes some code, it would be just a sugar syntax. But those are definitely future enhancements.

API

The Canonical Jetpack API proposal

    const { Button } = require("sdk/button"); // or maybe `sdk/ui`?

    // Minimal "action" button, default icon, 16x16
    let actionButton = Button({
      id: "my-button",
      label: "My Button"
    });

    // Minimal "toggle" button, default icon, 16x16
    let toggleButton = Button({
      id: "another-button",
      label: "Another Button",
      // `type` takes "button" (default) or "checkbox"
      type: "checkbox" // it's consistent with web and XUL
    });

    actionButton.on("click", function() {
      // do something when the button is clicked
    });

    toggleButton.on("change", function() {
      if (this.checked) { // consistent with web and XUL
        // do something when is "on"
      }
    }

    toggleButton.on("click", function() {
      // this is fired before "change" event when the state is not changed yet
    })

    actionButton.on("change", function() {
      // never fired, it's an action button
    })

    // disable a button

    actionButton.disabled = true; // web / XUL

    // a more complex button
    let drinkButton = Button({
      id: "drink-button",
      label: "Drink a Beer",
      image: data.url("beer.png"), // only local scheme or 'data:'
      type: "checkbox",
      disabled: true, // `disabled` can be set in the options too, default is `false`
      // `size` can takes "small" (16, default), "medium" (32), or "large" (64)
      // we can also accept both number and string identifier
      size: "medium",
      // badge will display up to around four characters, exceeding characters will
      // be cut out
      badge: {
        text: "+1",
        color: "#5fc24f" // any CSS color syntax is valid
      },
      // like other jetpack API, we can set the listener directly in the object's
      // options
      onChange: function() {
        if (this.checked) {
          // `label` and `image` can be set dynamically
          this.label = "Drink a Coffee";
          this.image = data.url("coffee.png");
        } else {
          this.label = "Drink a Beer";
          this.image = data.url("beer.png");
        }
      },
      onClick: function() {
        // update the badge text each click
        let drinkNumber = +this.badge.text;
        this.badge.text = "+" + (drinkNumber + 1);

        // update the badge background color
        this.badge.color = (drinkNumber > 10) ? "#fb2500" : "#5fc24f";
      }
    });

    // it should be possible trigger the click programmatically too
    drinkButton.click();

    // Once `size` is set, it can't be change, it's read-only. Therefore this code
    // will throw an exception
    drinkButton.size = "small";

    // Group buttons together:
    const { Button, ButtonSet } = require("sdk/button");
    let mySet = ButtonSet({
      // not sure if we need a mandatory id here, it depends by the implementation
      // in XUL.
      id: "my-set",
      // optional. Without it all buttons with type "checkbox" can checked
      // at the same time, otherwise are mutually exclusive:
      exclusive: true // "mutually exclusive" is too long, but I'm not sure this is good enough
    });

    // read-only, it will throw an exception
    myGroup.exclusive = false;

    let actionButton = Button({
      id: "my-button",
      label: "My Button",
      set: mySet
    });

    let toggleButton = Button({
      id: "another-button",
      label: "Another Button",
      type: "checkbox",
      set: mySet
    });

    let drinkButton = Button({
      id: "drink-button",
      label: "Drink a Beer",
      image: data.url("beer.png"),
      type: "checkbox",
      set: mySet
    });

    // iterate all buttons of a set
    for (let button of mySet.buttons)
      console.log(button.id);

    // get the checked buttons of a group, of course in case of mutuallu exclusive
    // it returns an array of one element.
    let checkedButtons = mySet.buttons.filter(function (button) button.checked)

    // ... or maybe we could have this as shortcut:
    for (let button of mySet.checkedButtons)
      console.log(button.id)

    // disable an entire set of buttons
    mySet.disabled = true;

    // add listeners to an entire set of buttons
    // they are emitted after the listener added to each individual button
    mySet.on("change", function(button) {
      // `button` is the button that fired that event
    });

The Web Standard API proposal

There is another approach that is totally different from the previous one. You can have a look to the working prototype, with some comments, here: https://github.com/ZER0/ui-html-addon/

The basic idea is: because an add-on already has its own window, we could load HTML on it. And this HTML, could be used to define the components to add to the Firefox UI, like button, toolbar, panel, etc. Basically it works a "user interface definition language".

The advantage is that we reuse totally web standards, that is more attractive for web developers, and the code used in the add-on could be easily reused somewhere else, like Firefox OS / gaia for example.

<!-- a simple 'action' button -->
<button id="my-button">My Button</button>

<!-- A 'toggle' button, using the standard -->
<input id="another-button" type="checkbox" />
<label for="another-button">Another Button</label>

<!-- Non standard -->
<button type="checkbox">Another Button</button>
<!-- or -->
<button data-type="checkbox">Another Button</button>
  const { document } = require("sdk/addon/window");

  // just use DOM API
  let actionButton = document.getElementById("my-button");

  actionButton.addEventListener("click", function(){
    // do something when the button is clicked
  });

  // or you can use jQuery too
  const $ = require("./jquery").inject(document);

  $("#another-button").on("change", function() {
    if (this.checked) {
      // do something when is "on"
    }
  });

  // to disabled a button with DOM API
  actionButton.disabled = true;
<!-- a more complex button -->
<style>
  /* we use that to have a compatible markup with regular html / gaia */
  #drink-button + label[for=drink-button] {
    background-image: url(./beer.png)
  }

  .badge[for=drink-button] {
    background-color: #5fc24f;
  }
</style>

<input id="drink-button" type="checkbox" class="size-medium" />
<label for="drink-button">Drink Beer!</label>
<label for="drink-button" class="badge">+1</label>
  // DOM API
  let drinkButton = document.getElementById("drink-button");

  drinkButton.addEventListener("change", function() {
    let label = this.nextElementSibling;
    if (this.checked) {
      label.style.backgroundImage = "url(./coffe.png)";

      // web standards have `this.labels` but it's not implemented in FF yet
      label.textContent = "Drink a Coffee";
    } else {
      label.style.backgroundImage = "url(./beer.png)";
      label.nextElementSibling.textContent = "Drink a Beer";
    }
  });

  drinkButton.addEventListener("click", function() {
    // of course we could provide utility functions, but the cool part is that
    // are regular DOM API, and we could use anything on top of that - e.g.
    // jQuery.
    let badge = this.querySelector("[for=" + this.id + "].badge");
    let drinkNumber = +badge.textContent;
    badge.textContent = "+" + (drinkNumber + 1);

    badge.style.backgroundColor = (drinkNumber > 10) ? "#fb2500" : "#5fc24f";
  });
<!-- group buttons together -->
<menu id="my-set">
  <button id="my-button" name="my-button-set">My Button</button>

  <!-- for mutually exclusive buttons we use `radio` and set a `name` attribute -->
  <input id="another-button" type="radio" name="my-button-set" />
  <label for="another-button">Another button</label>

  <input id="drink-button" type="radio" name="my-button-set" />
  <label for="drink-button" style="background-image: url(./beer.png)">Drink a Beer</label>
</menu>
    // iterate all buttons of a set
    let buttons = document.getElementsByName("my-button-set");

    for (let button of buttons)
      console.log(button.id);

    // get the checked buttons of a group, of course in case of mutuallu exclusive
    // it returns an array of one element.
    let checkedButtons = buttons.filter(function (button) button.checked)

    // disable an entire set of buttons
    let mySet = document.getElementById("my-set");
    mySet.disabled = true;

    // add listeners to an entire set of buttons
    // of course they're fired after the listener attached to the individual
    // elements, because that's how DOM API works. The event can be also cancelled
    // so that the "my-set" won't receive the event from a specific button
    mySet.addEventListener("change", function (event) {
      
    });

Here a list of pros & cons of this approach, and here the feedback on etherpad.

The "Both" proposal

During the development of the DOM API prototype, I noticed that we have a lot of work in common between the canonical jetpack API and the DOM one; for instance we need to track the windows and have different "view" in both cases, like it happens now for the widget. So I was thinking to use the DOM API proposal exactly like a UI definition language, that under the hood creates the Button instance, like it happens in a lot of languages. At the end, it doesn't really matters if we map the HTML node with a XUL node, or with a Button object.

In this way we could implement as first iteration the first proposal - or something similar – and implement the second in a next iteration, on top of the first one. We could also have an utility method that generates the HTML from a given UI, in order to "export" the HTML to other platform (browser, Firefox OS) if they were made with the first proposal before the second approach was implemented.

That will give us more time also to define better the DOM API proposal, and makes sure that we're aligned with Gaia team for example, about the HTML code we're using, to define some components.

The cons: we introduce two mechanism to do exactly the same thing, at the end, and that could be confusing.

Mockups