給力的右鍵選單
好吧,雖然這個blog還沒做好。但製作途中還是有很多技術可以拿出來討論的。

其中有幾點是我這近半年才發現的。

Scope Chain

Scope就是一般編程都必讀的一環,亦是經常讓人頭痛的東西。
簡單來說就是:
var yard = "Tommy";
function house()
{
    var bedroom = 'John';
    alert(yard); // Tommy
}

// bedroom不能從外面存取
alert(bedroom); // undefined

當然,這沒什麼大不了的,這是Scope的基本,犯不著我多說。

至於所謂的Scope Chain,就是Scope的連鎖。特點在於多個Scope環環相扣,就像骨牌一樣。

這個概念是我正煩惱要怎麼做一個右鍵選單時苦出來的。

為什麼我得自己另外苦一個出來呢?網絡上不是經已有很多類似的東西了?你憑什麼會覺得另起爐灶會做得比人家更好?


嘛第一點當然是我們的宗旨是做一個自家的API,第二點則是我煩惱了很久——覺得現今大部分的選單都無法做到我的要求(甚至是jQuery的ContextMenu)。

我們做用的GUI主要靠眼睛來接收這些圖像,而我們大腦處理這些影像的方式有一套特定的模式。我認為這點很重要,不然用戶使用起來便會感到挫折,大大打擊資訊傳遞的效率。

所以右鍵選單得配合大腦的運作模式,(這就是我覺得Windows做得好的地方。各種方面都很「brain-friendly」) 他的特點就是適當的停頓1以及選單的保留2

以下是這個blog所使用的模組(感謝微軟,其實很多方面我都是直接參考Windows的做法。):
Right click
右鍵點擊

當然現在還是有些缺陷(例如頁面捲動時會跟隨畫面),但在使用方面我覺得已經能乎合我的基準了(笑)。

好了,話題扯遠了。回到技術方面的討論:

以下是我所使用的編碼:
(function() {
  var writeLine = function(e)
  {
     alert('Clicked');
     return false;
  };
  new ContextMenu($('my_test_obj'), [
    {
        name: "Copy"
        , items: [
            new EventKey("Copy a random number", writeLine)
            , new EventKey("Copy a random character", writeLine)
        ]
    }
    , {
        name: "Group1"
        , items: [
            {
                name: "SubGroup1"
                , items: [
                    new EventKey("Subitem1", writeLine)
                    , new EventKey("Subitem2", writeLine)
                    , new EventKey("Subitem3", writeLine)
                ]
            }
            ,{
                name: "SubGroup2"
                , items: [
                    new EventKey("SubsubItem4", writeLine)
                    , new EventKey("SubsubItem5", writeLine)
                ]
            }
            , new EventKey("Item6", writeLine)
        ]
    }
        , new EventKey("Item1", writeLine)
        , new EventKey("Item2", writeLine)
        , new EventKey("Item3", writeLine)
        , new EventKey("Item4", writeLine)
    ], 'RMB');
})();

哈哈,當然這只是鍵單定義罷了(源碼很長。我會在文章最後附上)。

其實要做到這兩點,難度大大增加了。加上我自問組織能力不夠強,所以這花了我兩天的時間做呢。
抱歉我的塗鴉太難看了(哭)。

要解釋可能有點難度:由於選單的保留這一點,當滑鼠離開並於空白地方點擊時,選單得重設。
但是,選單已經打開了,在html方面就是這樣的狀態:
<ul>
  <ul style="display:block;">
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
    <ul style="display: block;">
       <li>Sub Item 1</li>
       <ul style="display: block;">Sub Item 1</ul>
    </ul>
    <li> ...
    .
    .
    .
  </ul>
  .
  .
  .
</ul>

現在我的問題是:到底要怎樣才能有效地收束經已打開了的了選單呢?(將所有display: block截掉)

「一般」的右鍵選單因為沒有保留機制,每當滑鼠移出選項時,選單就立即跟著改變了,根本不會有這個問題存在。

這就是我得出Status Chain這個概念的來源了(鳴,對不起,又是我的塗鴉):

抽出模組(ContextMenu)核心:
.
.
.
stepSubListeners = function (target)
{
        var lockedOn = null, j, overedOn
                , nodes = target.childNodes
        ;

        // Collapse menu chain, each chain is a route
        // Like dominoes
        target.chainRoute = target.chainRoute || [];

        for(var i in nodes)
        {
                j = nodes[i];
                if(!(j && j.nodeType == 1)) continue;

                // If this item has a submenu item
                if(j.lastChild.nodeType == 1)
                {
                        target.chainRoute[target.chainRoute.length] = {menu: j.lastChild, next: stepSubListeners(j.lastChild)};
                }

                IDOMElement(j).addEventListener(
                        new EventKey("MouseOver", function ()
                        {
                                // record "this (overed)" item
                                overedOn = this;

                                var subItem = this.lastChild;

                                registerDelay(function ()
                                {
                                        // Hide last displayed submenu if submenu is available
                                        if(lockedOn && lockedOn != subItem)
                                        {
                                                _chainHide ? _chainHide(lockedOn) : (lockedOn.style.display = "none");
                                                // bind the chains into chain reactor
                                                chainReactor.bind(lockedOn)();
                                        }

                                        // if mouse is still on "this (overed)" item
                                        if(overedOn == this)
                                        {

                                                // and if this item has sub item
                                                if(subItem.nodeType == 1)
                                                {
                                                        lockedOn = this.lastChild;
                                                        _chainShow ? _chainShow(subItem) : (subItem.style.display = "");
                                                }
                                        }
                                // 延遲300毫秒
                                }.bind(this), 300);
                        }.bind(j))
                );
        }

        return target.chainRoute.length ? chainReactor.bind(target) : null;
}

, chainReactor = function ()
{
        if(this && this.chainRoute)
        for(var i in this.chainRoute)
        {
                _chainHide ? _chainHide(this.chainRoute[i].menu) : (this.chainRoute[i].menu.style.display = "none");
                if(this.chainRoute[i].next) this.chainRoute[i].next();
        }
}

.
.
.

好吧,我不多說了。上面的編碼會為每個子選單項目創做一個觸發。每當選單收束時,會觸發一串檢查,而檢查過程中會再碰到觸發,於而做出連鎖反應(Chain Reaction)。

而stepSubListeners開頭的參數定義,會跟據項目的不同而改變,steuSubListeners的特點就是會自我呼叫(笑),因此參數的scope會變成巢狀(Nested Variable Scope)。這就是我命名為「Scope Chain」的原因。


附上源碼(注:只作參考,無法直接使用,但請大家隨便改吧。)

PS: 呼,第一次寫技術討論的文章呢。雖然我知道這些東西早就有人想過了,但我還是想將自己的發現分享出來啊。見笑了。

  1. 當滑鼠移到子選單時,會有大概300毫秒的延遲(默認動作),子選單才會彈出。而作為延遲的補正,當滑鼠按下的時候,子選單會立即彈出。
  2. 當子選單於滑鼠移出選單區域後,選單不會消失。
    由於眼晴對突然的動態(未預期,不在用戶設想之下所發生的變動)比較敏感,以上兩點能避免了「突然的改變」,減低了分散我們注意力的機會。
Profile picture
斟酌 鵬兄
Sat Jan 18 2014 02:00:49 GMT+0000 (Coordinated Universal Time)
Last modified: Sun Apr 10 2022 13:11:08 GMT+0000 (Coordinated Universal Time)
Comments
No comments here.
Do you even comment?
website: 
Not a valid website
Invalid email format
Please enter your email
*Name: 
Please enter a name
Submit
抱歉,Google Recaptcha 服務被牆掉了,所以不能回覆了