2012-06-26 13 views
27

Ho letto molti thread correlati ma nessuno di loro sembra fornire una soluzione.Come gestire la posizione di scorrimento su hashchange nell'applicazione Backbone.js?

Quello che sto cercando di fare è gestire la barra di scorrimento in modo intelligente nella mia app Backbone.js. Come molti altri, ho più percorsi hash #mypage. Alcune di queste rotte sono gerarchiche. per esempio. Ho una pagina #list che elenca alcuni elementi, faccio clic su un elemento nell'elenco. Quindi apre una pagina # visualizzazione/ITEMID.

Le mie pagine condividono lo stesso div Content nel layout HTML. In un cambio di navigazione, ho iniettato un nuovo div che rappresenta la vista per quella rotta nel div Content, sostituendo qualsiasi cosa ci fosse prima.

Così ora il mio problema:

Se l'articolo è molto in basso nell'elenco avrei potuto scorrere per arrivarci. Quando faccio clic su di esso, il comportamento di backbone "predefinito" è che la pagina # view/ITEMID viene visualizzata nella stessa posizione di scorrimento della vista #list. Risolvere questo è abbastanza facile; aggiungi un $ (documento) .scrollTop (0) ogni volta che viene iniettata una nuova vista.

Il problema è che se premo il pulsante Indietro vorrei tornare alla visualizzazione #list nella posizione di scorrimento che era in precedenza.

Ho provato a prendere la soluzione ovvia a questo. Memorizzazione di una mappa di percorsi per scorrere le posizioni in memoria. Scrivo questa mappa all'inizio del gestore per l'evento hashchange, ma prima che la nuova vista venga effettivamente inserita nel DOM. Ho letto dalla mappa alla fine del gestore hashchange, dopo che la nuova vista è nel DOM.

Quello che sto notando è che qualcosa, da qualche parte, in Firefox almeno, sta scorrendo la pagina come parte di un evento di hashchange, così che quando viene chiamato il codice write-to-map, il documento ha un posizione scorretta dello scroll che non è stata fatta esplicitamente dall'utente.

Qualcuno sa come risolvere questo problema o una best practice che dovrei utilizzare al suo posto?

Ho verificato doppio e non ci sono tag di ancoraggio nel mio DOM che corrispondono agli hash che sto usando.

+0

Non è un caso che si usi 'position(). Top' e si abbiano elementi genitore che ottengono' position: anything' dopo il caricamento DOM sei tu? – Ohgodwhy

+0

no, stavo usando $ (document) .scrollTop() sia per leggere che per scrivere la posizione di scorrimento – schematic

risposta

13

La mia soluzione per questo è stata una cosa meno automatica di quanto volessi, ma almeno è coerente.

Questo era il mio codice per il salvataggio e il ripristino. Questo codice è stato praticamente trasferito dal mio tentativo alla mia soluzione attuale, l'ho chiamato solo su diversi eventi. "soft" è un flag che proviene da un'azione browser (back, forward o hash click) anziché una chiamata "hard" a Router.navigate(). Durante una chiamata di navigate() volevo solo scorrere verso l'alto.

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; 
} 

ho anche modificato Backbone.History in modo che possiamo capire la differenza tra reagire a un cambiamento storico "soft" (che chiama checkUrl) rispetto programmazione innescando un "duro" il cambiamento della storia. Passa questo flag al callback del router.

_.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; 
    }, 
}); 

Originariamente stavo cercando di eseguire il salvataggio e il ripristino interamente durante il gestore di hashchange. Più specificamente, all'interno del callback wrapper del router, la funzione anonima che richiama il gestore del percorso effettivo.

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; 
}, 

ho voluto gestire le cose in questo modo, perché permette di salvare in tutti i casi, se un click href, indietro, pulsante avanti, o chiamare il numero navigare().

Il browser ha una "funzione" che tenta di ricordare lo scroll su un hashchange e spostarsi su di esso quando si torna a un hash. Normalmente questo sarebbe stato grandioso e mi avrebbe risparmiato tutta la fatica di implementarlo da solo. Il problema è che la mia app, come molti, modifica l'altezza del DOM da una pagina all'altra.

Ad esempio, sono su una vista #list alta e ho fatto scorrere verso il basso, quindi fare clic su un elemento e andare a una breve visualizzazione #detail che non ha alcuna barra di scorrimento. Quando premo il pulsante Indietro, il browser proverà a scorrere fino all'ultima posizione che ero per la visualizzazione #list. Ma il documento non è ancora così alto, quindi non è in grado di farlo. Nel momento in cui il mio percorso per #elenco viene chiamato e riappare l'elenco, la posizione di scorrimento viene persa.

Quindi, non è possibile utilizzare la memoria di scorrimento incorporata del browser. A meno che non abbia reso il documento un'altezza fissa o abbia fatto alcuni trucchi DOM, cosa che non volevo fare.

Inoltre, il comportamento di scorrimento incorporato compromette il tentativo precedente, poiché la chiamata a saveScrollPosition viene eseguita troppo tardi: il browser ha già modificato la posizione di scorrimento per quel momento.

La soluzione a questo, che avrebbe dovuto essere ovvia, era chiamare saveScrollPosition da Router.navigate() al posto del callback wrapper di route. Questo garantisce che sto salvando la posizione di scorrimento prima che il browser esegua qualcosa su 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); 
}, 

Purtroppo significa anche devo sempre chiamare in modo esplicito navigare() se sono interessato a salvare posizione di scorrimento, piuttosto che semplicemente utilizzando href = "# myhash" nei miei modelli.

Oh bene. Funziona. :-)

+0

Voglio solo dare credito che la maggior parte del codice di override sopra è stato preso dal codice originale Backbone, perché avevo bisogno di fare piccole modifiche a quel codice esistente che non potrebbe essere fatto in un modo più OOP-ish. – schematic

0

Ho una soluzione leggermente povera per questo. Nella mia app, ho avuto un problema simile. Ho risolto mettendo la visualizzazione elenco e la vista oggetto in un contenitore con:

height: 100% 

Poi impostare sia la visualizzazione elenco e la vista oggetto di avere:

overflow-y: auto 
height: 100% 

Poi, quando clicco su un oggetto, nascondo la lista e mostro la vista dell'elemento. In questo modo quando chiudo l'oggetto e torno all'elenco, manterrò il mio posto nell'elenco. Funziona con il pulsante Indietro una volta, anche se ovviamente non conserva la cronologia, quindi più clic sul pulsante indietro non ti portano dove devi essere. Eppure, una soluzione senza JS, quindi se è abbastanza buono ...

5

Una soluzione semplice: Conservare la posizione della visualizzazione elenco su ogni evento di scorrimento in una variabile:

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

Quando ritorno da vista elementi, scorrere la visualizzazione elenco alla posizione memorizzata:

window.scrollTo(0, pos); 
+0

Sì, funziona, e mi ricorda che possiamo semplicemente allegare una scrollTo a qualsiasi evento onShow (tramite Marionette) per garantire manualmente il proprio nella parte superiore della pagina. – MBHNYC

0

@ Mirage114 Grazie per pubblicare la tua soluzione. Esso funziona magicamente. Solo una cosa secondaria, presuppone che le funzioni di rotta siano sincrone. Se è presente un'operazione asincrona, ad esempio il recupero dei dati remoti prima di eseguire il rendering di una vista, la finestra scorre prima che il contenuto della vista venga aggiunto al DOM.Nel mio caso, memorizzo i dati quando una rotta viene visitata per la prima volta. In questo modo, quando un utente preme il pulsante Indietro/Avanti del browser, viene evitata l'operazione asincrona di recupero dei dati. Tuttavia, potrebbe non essere sempre possibile memorizzare nella cache tutti i dati necessari per un percorso.

Problemi correlati