eregs/regulations-site

View on GitHub
regulations/static/regulations/js/source/views/main/child-view.js

Summary

Maintainability
A
3 hrs
Test Coverage
import { locationActiveEvt } from '../../redux/locationReduce';
import { activeParagraph } from '../../redux/reducers';
import storage from '../../redux/storage';

const $ = require('jquery');
const _ = require('underscore');
const Backbone = require('backbone');
require('../../events/scroll-stop.js');
const Router = require('../../router');
const HeaderEvents = require('../../events/header-events');
const MainEvents = require('../../events/main-events');
const GAEvents = require('../../events/ga-events');

Backbone.$ = $;

// Adjust the offset of how far past the section top before the wayfinder
// changes. Based on 2 * line-height + padding-top.
const WAYFINDER_SCROLL_OFFSET = window.WAYFINDER_SCROLL_OFFSET || 64;

const ChildView = Backbone.View.extend({
  el: '#content-wrapper',

  initialize: function initialize(options) {
    this.options = options;
    this.$topSection = this.$el.find('[data-page-type]');
    this.sectionLabel = this.$topSection.data('label');
    this.attachWayfinding();

    if (this.options.render) {
      if (!this.title) {
        this.title = this.assembleTitle();
      }
      this.route(this.options);
      GAEvents.trigger('section:open', this.options);
      this.render();
    } else if (this.options.id) {
      MainEvents.trigger('section:sethandlers');
      storage().dispatch(locationActiveEvt(this.options.id));
    }

    storage().dispatch(locationActiveEvt(this.options.id));
    this.$activeSection = $(`#${this.options.id}`);

    this.loadImages();
  },

  attachWayfinding: function attachWayfinding() {
    this.updateWayfinding();
        // * when a scroll event completes, check what the active secion is
        // we can't scope the scroll to this.$el because there's no localized
        // way to grab the scroll event, even with overflow:scroll
    $(window).on('scrollstop', (_.bind(this.checkActiveSection, this)));
  },

  render: function render() {
    this.updateWayfinding();
    this.loadImages();
    this.scroll();
    HeaderEvents.trigger('section:open', this.sectionLabel);
    storage().dispatch(locationActiveEvt(this.sectionLabel));
  },

  scroll: function scroll() {
    let offsetTop;
    let $scrollToId;
    if (this.options.scrollToId) {
      $scrollToId = $(`#${this.options.scrollToId}`);
      if ($scrollToId.length) {
        offsetTop = $scrollToId.offset().top;
      }
      window.scrollTo(0, offsetTop || 0);
    }
  },

  changeFocus: function changeFocus(id) {
    $(id).focus();
  },

  assembleTitle: function assembleTitle() {
    const titleParts = _.compact(document.title.split(' '));
    const newTitle = [titleParts[0], titleParts[1], this.sectionLabel, '|', 'eRegulations'];
    return newTitle.join(' ');
  },

    // naive way to update the active table of contents link and wayfinding header
    // once a scroll event ends, we loop through each content section DOM node
    // the first one whose offset is greater than the window scroll position, accounting
    // for the fixed position header (via margin/border offsets), is deemed the
    // active section
  checkActiveSection: function checkActiveSection() {
    $.each(this.$sections, (idx, $section) => {
      const previousSection = activeParagraph(storage()).paragraph;
      if ($section.offset().top + WAYFINDER_SCROLL_OFFSET >= $(window).scrollTop()) {
        if (_.isEmpty(previousSection) || (previousSection !== $section.id)) {
          const currentSection = $section[0].id;
          this.$activeSection = $($section[0]);
          // **Event** trigger active section change
          HeaderEvents.trigger('section:open', this.$activeSection.data('label'));
          storage().dispatch(locationActiveEvt(currentSection));

          if (typeof window.history !== 'undefined' && typeof window.history.replaceState !== 'undefined') {
              // update hash in url
            window.history.replaceState(
                null,
                null,
                `${window.location.origin + window.location.pathname + window.location.search}#${currentSection}`,
              );
          }

          return false;
        }
      }
      return true;
    });
  },

  updateWayfinding: function updateWayfinding() {
      // cache all sections in the DOM eligible to be the active section
      // also cache some jQobjs that we will refer to frequently.
      //
      // Sections that are eligible for being the active section.
    this.$sections = this.$el
        .find('li[id], .reg-section, .appendix-section, .supplement-section')
        .map((idx, elm) => $(elm));
  },

  route: function route(options) {
    if (Router.hasPushState && typeof options.noRoute === 'undefined') {
      let url = this.url;

            // if a hash has been passed in
      if (options && options.scrollToId) {
        url += `#${options.scrollToId}`;
        this.navigate(url);
        $('html, body').scrollTop($(`#${options.scrollToId}`).offset().top);
      } else {
        if (['diff', 'search-results'].indexOf(options.type) === -1) {
          url += `#${options.id}`;
        }
        this.navigate(url);
      }
    }
  },

  navigate: function navigate(url) {
    Router.navigate(url);
    MainEvents.trigger('route', url);
    document.title = this.title;
  },

  remove: function remove() {
    $(window).off('scrollstop');
    this.stopListening();
    this.off();
    return this;
  },

    // lazy load images as the user scrolls
  loadImages: function loadImages() {
    $('.reg-image').lazyload();
  },
});

module.exports = ChildView;