2012-06-26 18 views
27

我已閱讀了許多相關的主題,但他們似乎都沒有提供解決方案。如何在Backbone.js應用程序中處理hashchange上的滾動位置?

我想要做的是在我的Backbone.js應用程序中智能地處理滾動條。像其他許多人一樣,我有多個#mypage散列路由。其中一些路線是分層的。例如我有一個#list頁面列出了一些項目,我點擊列表中的一個項目。然後它打開一個#view/ITEMID頁面。

我的頁面在HTML佈局中共享相同的Content div。在導航更改時,我將一個新的div代表該路線的視圖放入Content div,替換之前的任何內容。

所以現在我的問題:

如果該項目是迄今爲止在列表中向下,我可能必須滾動到那裏。當我點擊它時,「默認」骨幹行爲是#view/ITEMID頁面顯示在與#list視圖相同的滾動位置。解決這個問題很簡單;每當注入一個新視圖時,只需添加一個$(document).scrollTop(0)。

問題是如果我點擊後退按鈕,我想回到之前滾動位置的#list視圖。

我試圖採取明顯的解決方案。存儲路線圖以在內存中滾動位置。我在hashchange事件處理程序的開始處寫入此映射,但在新視圖實際放入DOM之前。在新視圖位於DOM之後,我從hashchange處理程序結束時的地圖讀取。

我注意到的是,至少在Firefox的某個地方,將頁面作爲hashchange事件的一部分進行滾動,以便在寫入映射代碼被調用時,文檔具有不可靠的滾動位置,絕對不是用戶明確提出的。

任何人都知道如何解決這個問題,或者我應該使用的最佳實踐?

我重複檢查,並且在我的DOM中沒有與我使用的哈希相匹配的錨定標記。

+0

你不是偶然的使用'position()。top'並且父元素在DOM負載之後獲得'position:anything'嗎? – Ohgodwhy

+0

不,我正在使用$(document).scrollTop()來讀取和寫入滾動位置 – schematic

回答

13

我對此的解決方案最終導致自動化程度低於我想要的,但至少它是一致的。

這是我的代碼保存和恢復。這段代碼幾乎是從我的實際解決方案中進行的,只是在不同的事件中調用它。 「soft」是一個標誌,它來自瀏覽器操作(後退,前進或散列點擊),而不是對Router.navigate()的「硬」調用。在導航()調用期間,我只想滾動到頂部。

restoreScrollPosition: function(route, soft) { 
    var pos = 0; 
    if (soft) { 
     if (this.routesToScrollPositions[route]) { 
      pos = this.routesToScrollPositions[route]; 
     } 
    } 
    else { 
     delete this.routesToScrollPositions[route]; 
    } 
    $(window).scrollTop(pos); 
}, 

saveScrollPosition: function(route) { 
    var pos = $(window).scrollTop(); 
    this.routesToScrollPositions[route] = pos; 
} 

我也修改Backbone.History這樣我們就可以告訴反應「軟」的歷史變化(它調用checkUrl)與編程觸發一個「硬」的歷史變化之間的差異。它將此標誌傳遞給路由器回調。

_.extend(Backbone.History.prototype, { 

    // react to a back/forward button, or an href click. a "soft" route 
    checkUrl: function(e) { 
     var current = this.getFragment(); 
     if (current == this.fragment && this.iframe) 
      current = this.getFragment(this.getHash(this.iframe)); 
     if (current == this.fragment) return false; 
     if (this.iframe) this.navigate(current); 
     // CHANGE: tell loadUrl this is a soft route 
     this.loadUrl(undefined, true) || this.loadUrl(this.getHash(), true); 
    }, 

    // this is called in the whether a soft route or a hard Router.navigate call 
    loadUrl: function(fragmentOverride, soft) { 
     var fragment = this.fragment = this.getFragment(fragmentOverride); 
     var matched = _.any(this.handlers, function(handler) { 
      if (handler.route.test(fragment)) { 
       // CHANGE: tell Router if this was a soft route 
       handler.callback(fragment, soft); 
       return true; 
      } 
     }); 
     return matched; 
    }, 
}); 

最初我試圖做的滾動保存和整個恢復在hashchange處理程序。更具體地說,在路由器的回調包裝器中,調用實際路由處理器的匿名函數。

route: function(route, name, callback) { 
    Backbone.history || (Backbone.history = new Backbone.History); 
    if (!_.isRegExp(route)) route = this._routeToRegExp(route); 
    if (!callback) callback = this[name]; 
    Backbone.history.route(route, _.bind(function(fragment, soft) { 

     // CHANGE: save scroll position of old route prior to invoking callback 
     // & changing DOM 
     displayManager.saveScrollPosition(foo.lastRoute); 

     var args = this._extractParameters(route, fragment); 
     callback && callback.apply(this, args); 
     this.trigger.apply(this, ['route:' + name].concat(args)); 

     // CHANGE: restore scroll position of current route after DOM was changed 
     // in callback 
     displayManager.restoreScrollPosition(fragment, soft); 
     foo.lastRoute = fragment; 

     Backbone.history.trigger('route', this, name, args); 
    }, this)); 
    return this; 
}, 

我想處理事情這種方式,因爲它允許在所有情況下保存,無論是HREF點擊,後退按鈕,前進按鈕,或導航()調用。

瀏覽器有一個「功能」,它試圖記住你在hashchange上的滾動,並在回到散列時移動到它。通常情況下,這會很好,並且可以節省我自己實施它的所有麻煩。問題是我的應用程序與許多應用程序一樣,會改變DOM頁面之間的高度。

例如,我在一個高大的#list視圖中滾動到底部,然後單擊一個項目並轉到一個簡短的#detail視圖,該視圖根本沒有滾動條。當我按下「後退」按鈕時,瀏覽器會嘗試將我滾動到我爲#list視圖所處的最後位置。但該文件還沒有那麼高,所以它不能這樣做。當我的#list路由被調用並重新顯示列表時,滾動位置丟失。

因此,無法使用瀏覽器的內置滾動內存。除非我將文檔固定在一個固定的高度,或者做了一些DOM欺騙,這是我不想做的。

此外,內置的滾動行爲混淆了上述嘗試,因爲saveScrollPosition的調用太遲 - 瀏覽器已經改變了滾動位置。

對此的解決方案應該是顯而易見的,它從Router.navigate()調用saveScrollPosition而不是路由回調包裝器。這保證了我在瀏覽器對hashchange執行任何操作之前保存了滾動位置。

route: function(route, name, callback) { 
    Backbone.history || (Backbone.history = new Backbone.History); 
    if (!_.isRegExp(route)) route = this._routeToRegExp(route); 
    if (!callback) callback = this[name]; 
    Backbone.history.route(route, _.bind(function(fragment, soft) { 

     // CHANGE: don't saveScrollPosition at this point, it's too late. 

     var args = this._extractParameters(route, fragment); 
     callback && callback.apply(this, args); 
     this.trigger.apply(this, ['route:' + name].concat(args)); 

     // CHANGE: restore scroll position of current route after DOM was changed 
     // in callback 
     displayManager.restoreScrollPosition(fragment, soft); 
     foo.lastRoute = fragment; 

     Backbone.history.trigger('route', this, name, args); 
    }, this)); 
    return this; 
}, 

navigate: function(route, options) { 
    // CHANGE: save scroll position prior to triggering hash change 
    nationalcity.displayManager.saveScrollPosition(foo.lastRoute); 
    Backbone.Router.prototype.navigate.call(this, route, options); 
}, 

不幸的是,這也意味着我總是有顯式調用瀏覽()如果我想救滾動位置,而不是在我的模板,只是用的href =「#myhash」。

唉。有用。 :-)

+0

只是想說明上面的大部分重寫代碼都是從原始的Backbone代碼中獲取的,因爲我需要做一些小的調整到那些無法用更多OOP方式完成的現有代碼。 – schematic

0

我有一個稍微窮人的修復。在我的應用程序中,我遇到了類似的問題。我解決它通過將列表視圖和項目視圖成容器:

height: 100% 

然後我設置列表視圖和項目視圖既具有:

overflow-y: auto 
height: 100% 

然後當我點擊一個項目,我隱藏列表並顯示項目視圖。當我關閉這個項目並回到列表中時,我將這個位置保存在列表中。它與後退按鈕一起工作,但顯然它不會保留您的歷史記錄,因此多次後退按鈕點擊不會讓您獲得您需要的位置。儘管如此,沒有JS的解決方案,因此,如果它不夠好......

5

一個簡單的辦法: 存儲在一個變量中的每個滾動事件列表視圖中的位置:

var pos; 
$(window).scroll(function() { 
    pos = window.pageYOffset; 
}); 

當從返回項目視圖,滾動列表視圖到存儲位置:

window.scrollTo(0, pos); 
+0

是的,這是有效的,並且提醒我我們可以將scrollTo附加到任何onShow事件(通過Marionette)以手動確保頁面頂部的人。 – MBHNYC

0

@ Mirage114感謝您發佈您的解決方案。它像一個魅力。只是一件小事,它假定路由功能是同步的。如果存在異步操作,例如在呈現視圖之前獲取遠程數據,則在將視圖內容添加到DOM之前,將滾動窗口。在我的情況下,我第一次訪問路由時緩存數據。這樣當用戶點擊瀏覽器後退/前進按鈕時,可以避免獲取數據的異步操作。但是,並不總是可以緩存路由所需的每個數據。

相關問題