20120327

開發自己的 Firefox add-on 附加元件(八) Context-menu 模組

Context-menu 模組讓我們可以增加物件( item )到 Firefox 瀏覽器中的網頁的 context menu。

Context-menu API 提供了簡單、宣告式的方法來增加物件到網頁的 context menu 中。我們可以增加當被點擊時做出反應的物件、子選單( submenu )、和分隔符號( separator )。


我們結合物件( item )和某個情境( context ),然後新增和移除該物件的的動作就會自動處理,而不需要在某情境發生或結束時,手動新增和移除物件。物件和情境結合,就類似事件和其監聽函式綁定一樣。當使用者喚起某個 context menu,所有與其綁定的物件就自動備增加到其中。如果沒有物件被綁定,則 context menu 終究部會增加物件。相同地,任何原本在 menu 中,但沒有被綁定到當前情境的物件,會被自動從 menu 中移除。我們完全不需要手動移除 menu 中的物件,除非我們希望那些物件不要再出現。

舉例來說,如果我們的 add-on 需要在使用者每次拜訪某個頁面的時候,新增一個 context menu 物件,那麼當該頁面載入時不要建立這個物件、頁面停止載入時也不要移除這個物件。而是應該只建立物件一次,然後提供符合目標 URL 的情境。

指定情境

就如同字面上的意思,context menu 應該要為某特定情境的發生而保留。所謂情境,可以是與網頁的內容相關、或者是和網頁本身相關,但絕不會是超過網頁相關的範圍。

舉個例子,正確使用 menu 的用法是,當使用者右鍵點擊網頁中的圖片時,menu 中會顯示 "編輯圖片" 物件;不正確的用法會是,顯示列出所有使用者分頁的子選單,因為分頁既跟網頁無關也和使用者點擊的東西無關。

網頁情境( page context )

首先,我們可能根本不需要指定一個情境。當一個物件沒有指定情境時,等於套用網頁情境。

當使用者從網頁中非互動( non-interactive )的部分喚起 context menu 時,網頁情境就發生了。試試看右鍵點擊本頁中空白的地方、或者是文字(但不要反白到字)。這時候顯示的 menu 應該會包含"上一頁"、"下一頁"、"重新載入"、"停止"等等物件。這就是網頁情境( page context )。

當選單物件將網頁是為一個整體來反映時,用網頁情境才是適當的。當使用者從連結、圖片、或其他非文字的網頁節點( node )叫出 context menu 時,不會出現網頁情境。

宣告情境( Declarative Contexts )

我們可以藉由設定 context 屬性給 context menu 的建構子,來指定一些簡單、宣告式的情境:
var cm = require("context-menu");
cm.Item({
  label: "My Menu Item",
  context: cm.URLContext("*.mozilla.org")
});
這些情境可能藉由呼叫以下的建構子以被指定給 context menu。每個建構以都是由 context menu 模組產生:

Constructor Description
PageContext() 網頁情境( page context )。
SelectionContext() 當使用者在網頁中做了一個選擇時 menu 被喚起,則此情境發生。
SelectorContext(selector) 當 menu 從符合 selector、CSS selector、或有祖先符合,這三種情況中任一種情況的節點被喚起時發生。selector 可能包含多個 selectors,以逗號分隔。e.g.,"a[href], img"。
URLContext(matchPattern) 當 menu 從網頁中特定 URL 喚起時發生此情境。matchPattern 是一個比對模式的字串或比對模式字串組成的陣列。當 matchPattern 是一個陣列時,此情境會在 menu 從符合陣列中任一模式的 URL 被喚起時發生。這個 match pattern 就和使用在 page-mod include 屬性的一樣。
array 所有其他類別的陣列,當所有陣列中的情境發生時,會發生這個情境。

Menu 物件也有一個在建構之後可以增加和移除宣告情境( declarative contexts )的 context 屬性,如:
var context = require("context-menu").SelectorContext("img");
myMenuItem.context.add(context);
myMenuItem.context.remove(context);
當一個選單物件和一個以上的情境綁定時,這個物件會在所有這些情境發生時出現。

In Content Scripts

宣告式情境很方便使用但功能不強大。舉例來說,我們可能希望選單物件在任何網頁中有圖片時出現,但宣告式情境做不到。

當我們想要更深入操控選單物件出現的情境時,我們可以使用 content script。和 SDK 中其他 API 相同,context-menu API 使用 content script 來讓 add-on 與瀏覽器中的網頁互動。每個我們建立在第一層context menu 中的選單物件都可以有一個 content script。

當context menu 要被顯示出來時,有一個叫做 "context" 的特殊事件會在 context script 中被發佈(emit)出來。如果我們註冊監聽這個事件的函式且回傳 true,則與這個監聽函式的 content script 關聯的選單物件就會顯示在 menu 中。

例如,這個物件會在 context menu 從有圖片的網頁中被喚起時出現:
require("context-menu").Item({
  label: "This Page Has Images",
  contentScript: 'self.on("context", function (node) {' +
                 '  return !!document.querySelector("img");' + //weired double escalations
                 '});'
});
這裡要注意的是,監聽函式有一個參數 node。這個 node 代表使用者右鍵點擊到喚起 menu 的節點。我們可以用這個參數來決定選單物件該不該顯示。

我們可以既指定宣告式情境、也監聽某 content script 中的情境。在這種情況下,宣告式情境會被優先處理。如果當前情境不符合我們設定的宣告式情境,則 content script 中的監聽函式不會被執行。

以下的例子就是利用這種特性。監聽函式可以被保證 node 一定是圖片:
require("context-menu").Item({
  label: "A Mozilla Image",
  context: contextMenu.SelectorContext("img"),
  contentScript: 'self.on("context", function (node) {' +
                 '  return /mozilla/.test(node.src);' +
                 '});'
});
上例中,我們的選單物件只會在同時滿足當前情境符合所有宣告式情境,且 context 監聽函式回傳真的時候顯示。

Handling Menu Item Clicks

除了如上述使用 content script 來監聽 "context" 事件,我們也可以使用 content script 來處理選單物件被點擊的事件。當使用者點擊選單物件,一個叫做 "click" 的事件會在此物件的 content script 中被發佈出來。

因此,若要處理物件被點擊的情況,則在該物件的 content script 中監聽 "click" 事件,如下:
require("context-menu").Item({
  label: "My Item",
  contentScript: 'self.on("click", function (node, data) {' +
                 '  console.log("Item clicked!");' +
                 '});'
});
程式碼中第 4 行的 Console 用法請見這裡

注意,監聽函是有兩個參數 nodedatanode 使用者點擊叫出 context menu 的節點,我們可以用其來執行某些動作。data 是被點擊的選單物件的 data 屬性。因為只有最上層的選單物件(Item 建構子)有 content script,所以我們用 data 來判斷哪個 Menu 中的物件被點擊:
var cm = require("context-menu");
cm.Menu({
  label: "My Menu",
  contentScript: 'self.on("click", function (node, data) {' +
                 '  console.log("You clicked " + data);' +
                 '});',
  items: [
    cm.Item({ label: "Item 1", data: "item1" }),
    cm.Item({ label: "Item 2", data: "item2" }),
    cm.Item({ label: "Item 3", data: "item3" })
  ]
});
上例中我們使用的是 Menu 建構子而不是 Item 建構子。兩者的差異是,Menu 產生 context menu 中一個可以點擊的選單物件;而 Item 產生的是有子選單的選單物件。

通常我們會需要從 click 事件的監聽函式蒐集某些資訊來執行與網頁內容無關的動作。為了和選單物件及其 content script 溝通,content script 可以呼叫全域 self 物件的 postMessage 方法,來傳遞JSON-able data。選單物件的 "message" 事件監聽函式接收到這個 data 時會被呼叫。
require("context-menu").Item({
  label: "Edit Image",
  context: contextMenu.SelectorContext("img"),
  contentScript: 'self.on("click", function (node, data) {' +
                 '  self.postMessage(node.src);' +
                 '});',
  onMessage: function (imgSrc) {
    openImageEditor(imgSrc);
  }
});

Updating a Menu Item's Label

每個選單物件在被建立時都必須指定 label,不過我們可以在之後使用一些方法來改變它。

最簡單的方法就是設定選單物件的 label 屬性。以下的例子根據物件被點擊的次數來改變 label 的內容:
var numClicks = 0;
var myItem = require("context-menu").Item({
  label: "Click Me: " + numClicks,
  contentScript: 'self.on("click", self.postMessage);',
  onMessage: function () {
    numClicks++;
    this.label = "Click Me: " + numClicks;
    // Setting myItem.label is equivalent.
  }
});

有時候我們可能想要根據情境來改變 label 的內容。舉例來說,如果我們的物件會對使用者所選取的詞句進行搜尋,那麼把要搜尋的詞句顯示在選單物件中,以提供回饋給使用者,會是個不錯的做法。在這種情況下,我們可以使用第二個方法。回想一下,我們的 content script 可以監聽 "context" 事件,若回傳值為真,選單物件與其 content script 就會顯示在選單中。除了回傳值為真之外,我們的 "context" 監聽函式也可以回傳字串。當 "context" 監聽函式回傳字串時,其會變成物件的新 label。

以下實作前述的搜尋範例:
var cm = require("context-menu");
cm.Item({
  label: "Search Google",
  context: cm.SelectionContext(),
  contentScript: 'self.on("context", function () {' +
                 '  var text = window.getSelection().toString();' +
                 '  if (text.length  20)' +
                 '    text = text.substr(0, 20) + "...";' +
                 '  return "Search Google for " + text;' +
                 '});'
});
"context" 監聽函式取得視窗內當前所選取的內容,如果太長則將其截短,並把這個內容包含到回傳的字串中。當選單物件顯示出來時,其 label 會顯示 "Search Google for text",其中 text 就是所選取的(可能被截短過的)內容。

了解更多 Context-menu 類別的詳細建構子、方法、屬性、事件,請參考這裡

沒有留言:

張貼留言