2013-01-06

ZK-1309 的故事

正確來說,ZK-1309 應該稱之為 onFloatUp 的故事...... [淚目]

故事發生在 modal window 當中如果用了 Clients.showNotification(), 要再對 modal window 作 drag 的時候,覆蓋畫面的 mask 就會消失, 在 drop 的時候才又出現;令人傻眼的是,如果繼續 DnD,就又恢復正常行為。

因為搭配 Notification 才會出問題,所以看了一下 Notification 是不是有做什麼手腳......

根本就是浪費時間 [死]

既然時間都浪費掉了,就順便記一下。 Clients.showNotification() 最後負責呈現的 JS 是 Notification.js,在 show() 當中會去新增一個 zul.wgt.Notification(),原則上行為都是繼承自 Popup

問題應該還是回歸到 Window 自己身上,不過先找到 mask 是誰處理再說。 用 css name 硬幹的結果找到 effect.js 中的 zk.eff.FullMask, 在 sync() 的時候會用 jqzk.isVisible()(在 dom.js 裡頭)去檢查 Window 是不是看得到 (要 css 的 display != "none" 而且 visibility != "hidden"), 如果看不到就把自己 hide 起來,故事的舞台就在這兒了。

於是在 debug 的過程中,難免會去印 sync() 的參數 el 的值,遇到一個靈異現象:

console.log(zk(el).jq[0]);  //DOM 的 visibility 沒有值
console.log(zk(el).jq[0].style.visibility);  //印出 hidden
//也有剛好顛倒過來的情形

真是有夠莫名的,完全不懂為什麼?一開始只用第一個寫法觀察值,就發生了結果跟過程對不起來的悲劇 [核爆]

從這往回推,是 Window.zsync() 呼叫這個 method,再往回推... 就被推倒了推不下去了,因為在重現問題的時候 FullMask.sync() 在一次動作中會被呼叫好幾次; 而 Window.zsync()Window 裡頭遍地開花。 只好用 console.trace() 大法來慢慢看,最後發現在這個 case 中一致的起點是 Window.onFloatUp(), 整理如下:

  • 單純 drag window
    1. 開始時:Drag._startDrag()Window._startmove()→zWatch.fire('onFloatUp, dg.control)
    2. 結束時:Drag._finishDrag()Window._aftermove()Window.zsync()
  • 叫出 Notification 然後點 window:Widget.mimicMouseDown_()
  • 叫出 Notification 然後 drag window:
    1. 下列選一(原因不明,好像還有測出另一個只是忘了記......)
      1. _domEvtProxy0()
      2. doMouseover()
    2. 回歸單純 drag window 流程

在發了 onFloatUp 的 event 之後,Window 方面流程大致上是:

Window.onFloatUp()
    Widget.setTopmost()
        Widget.setFloatZIndex_() :有給 opt = {fire:true},所以後面 onZIndex() 一定會 fire
            Window.setZIndex()
                Widget.setZIndex() : 上面有 super("setZIndex"),如果值沒變就不會往下作
                    Window.onZIndex() : 上面有 fire("onZIndex")
                        Window.zsync()
                Window.zsync()

只要有進入 setTopmost(), 那麼 onZIndex()setZIndex() 會各 call 一次 zsync(), 也就導致 mask 也會 sync() 兩次,雖然很無奈,但反正不是要修 performance issue,所以不管他 XD。

那麼,問題是出在哪裡呢?

簡單地說,就是 Notification 與 modal window 爭奪「最高」的機制(可以說是 setTopmost())所延伸出來的問題。

一開始 modal window 出來時,他得是「全 DOM 最高」,所以 setTopmost() 計算出來的結果 zIndex 是 1800。 後來殺出的 Notification 必須高過 modal window,所以 setTopmost() 決定給他 zIndex 為 1801。 當 Notification 消失(無論是哪一種方式)的那個瞬間,modal window 觸發 onFloatUp(), 於是要再次奪回「全 DOM 最高」的地位,偏偏這時候 Notification 還沒有真正死掉,所以 setTopmost() 就給了 1802。

真正開始處理 modal window 的 DnD 時(也會觸發 onFloatUp()),又會再確保一次自己是「全 DOM 最高」, 但是這時候 setTopmost() 發現只要給 1800 就夠了,就真的給 1800。 結果往下處理到 Widget.setZIndex() 的時候發現前後兩次的 zIndex 值不一樣, 按照規定去作(兩次)Window.zsync(),在 FullMask.sync() 那裡檢查 modal window 是不是還在......

登登! [炸]

根據 DnD 的設計原則(細節略),在開始 drag 的時候,end-user 看到、拖曳的其實是假的鬼魂, 真正的本尊還留在原本的位置只是隱藏不見,所以 FullMask 覺得 modal window 看不見,自己也就乖乖地跟著不見。

在解 bug 的途中(?)嘗試撰寫一個 Widget.isTopmost() 來判斷是不是已經是「全 DOM 最高」, 沒有經過嚴格驗證,不過在「叫出 Notification 然後點 modal window」的情況下測試會過, 就保留一份在這灌水字數以供緬懷:

isTopmost: function () {
    if (!this.desktop) return false;

    for (var wgt = this; wgt; wgt = wgt.parent) {
        if (wgt._floating) {
            for (var j = _floatings.length; j--;) {
                var w = _floatings[j].widget,
                    h = _floatings[j].node;
                if (wgt == w) continue;
                if (this.getFloatZIndex_(this.$n()) < w.getFloatZIndex_(h) && !zUtl.isAncestor(this, w)) {
                    return false;
                }
            }
        }
    }
    return true;
},

最後的解法還是在 Window_startmove() 時塞一個叫 isDragging 的 field, 在 _aftermove() 的時候拿掉。所以只要在 onFloatUp() 的時候檢查這個 field, 如果正在 drag 就不繼續往下作,bug 就圓滿地圓寂(?)了。

當然,不管是解 bug 的當下、或是在回顧這個故事時, 都不覺得這樣的作法有很好,總覺得應該去解決 setTopmost() 或是 onFloatUp() 機制著手來斬草除根......

這大概就是所謂 maintain 的情懷(?)吧......

No comments:

Post a Comment