<commit_before>;(function ($, window, document, undefined) {
  'use strict';

  Foundation.libs.dropdown = {
    name : 'dropdown',

    version : '5.2.2',

    settings : {
      active_class: 'open',
      align: 'bottom',
      is_hover: false,
      opened: function(){},
      closed: function(){}
    },

    init : function (scope, method, options) {
      Foundation.inherit(this, 'throttle');

      this.bindings(method, options);
    },

    events : function (scope) {
      var self = this,
          S = self.S;

      S(this.scope)
        .off('.dropdown')
        .on('click.fndtn.dropdown', '[' + this.attr_name() + ']', function (e) {
          var settings = S(this).data(self.attr_name(true) + '-init') || self.settings;
          if (!settings.is_hover || Modernizr.touch) {
            e.preventDefault();
            self.toggle($(this));
          }
        })
        .on('mouseenter.fndtn.dropdown', '[' + this.attr_name() + '], [' + this.attr_name() + '-content]', function (e) {
          var $this = S(this);
          clearTimeout(self.timeout);

          if ($this.data(self.data_attr())) {
            var dropdown = S('#' + $this.data(self.data_attr())),
                target = $this;
          } else {
            var dropdown = $this;
                target = S("[" + self.attr_name() + "='" + dropdown.attr('id') + "']");
          }

          var settings = target.data(self.attr_name(true) + '-init') || self.settings;

          if(S(e.target).data(self.data_attr()) && settings.is_hover) {
            self.closeall.call(self);
          }

          if (settings.is_hover) self.open.apply(self, [dropdown, target]);
        })
        .on('mouseleave.fndtn.dropdown', '[' + this.attr_name() + '], [' + this.attr_name() + '-content]', function (e) {
          var $this = S(this);
          self.timeout = setTimeout(function () {
            if ($this.data(self.data_attr())) {
              var settings = $this.data(self.data_attr(true) + '-init') || self.settings;
              if (settings.is_hover) self.close.call(self, S('#' + $this.data(self.data_attr())));
            } else {
              var target = S('[' + self.attr_name() + '="' + S(this).attr('id') + '"]'),
                  settings = target.data(self.attr_name(true) + '-init') || self.settings;
              if (settings.is_hover) self.close.call(self, $this);
            }
          }.bind(this), 150);
        })
        .on('click.fndtn.dropdown', function (e) {
          var parent = S(e.target).closest('[' + self.attr_name() + '-content]');

          if (S(e.target).data(self.data_attr()) || S(e.target).parent().data(self.data_attr())) {
            return;
          }
          if (!(S(e.target).data('revealId')) &&
            (parent.length > 0 && (S(e.target).is('[' + self.attr_name() + '-content]') ||
              $.contains(parent.first()[0], e.target)))) {
            e.stopPropagation();
            return;
          }

          self.close.call(self, S('[' + self.attr_name() + '-content]'));
        })
        .on('opened.fndtn.dropdown', '[' + self.attr_name() + '-content]', function () {
            self.settings.opened.call(this);
        })
        .on('closed.fndtn.dropdown', '[' + self.attr_name() + '-content]', function () {
            self.settings.closed.call(this);
        });

      S(window)
        .off('.dropdown')
        .on('resize.fndtn.dropdown', self.throttle(function () {
          self.resize.call(self);
        }, 50));

      this.resize();
    },

    close: function (dropdown) {
      var self = this;
      dropdown.each(function () {
        if (self.S(this).hasClass(self.settings.active_class)) {
          self.S(this)
            .css(Foundation.rtl ? 'right':'left', '-99999px')
            .removeClass(self.settings.active_class)
            .prev('[' + self.attr_name() + ']')
            .removeClass(self.settings.active_class);

          self.S(this).trigger('closed', [dropdown]);
        }
      });
    },

    closeall: function() {
      var self = this;
      $.each(self.S('[' + this.attr_name() + '-content]'), function() {
        self.close.call(self, self.S(this))
      });
    },

    open: function (dropdown, target) {
        this
          .css(dropdown
            .addClass(this.settings.active_class), target);
        dropdown.prev('[' + this.attr_name() + ']').addClass(this.settings.active_class);
        dropdown.trigger('opened', [dropdown, target]);
    },

    data_attr: function () {
      if (this.namespace.length > 0) {
        return this.namespace + '-' + this.name;
      }

      return this.name;
    },

    toggle : function (target) {
      var dropdown = this.S('#' + target.data(this.data_attr()));
      if (dropdown.length === 0) {
        // No dropdown found, not continuing
        return;
      }

      this.close.call(this, this.S('[' + this.attr_name() + '-content]').not(dropdown));

      if (dropdown.hasClass(this.settings.active_class)) {
        this.close.call(this, dropdown);
      } else {
        this.close.call(this, this.S('[' + this.attr_name() + '-content]'))
        this.open.call(this, dropdown, target);
      }
    },

    resize : function () {
      var dropdown = this.S('[' + this.attr_name() + '-content].open'),
          target = this.S("[" + this.attr_name() + "='" + dropdown.attr('id') + "']");

      if (dropdown.length && target.length) {
        this.css(dropdown, target);
      }
    },

    css : function (dropdown, target) {
      this.clear_idx();

      if (this.small()) {
        var p = this.dirs.bottom.call(dropdown, target);

        dropdown.attr('style', '').removeClass('drop-left drop-right drop-top').css({
          position : 'absolute',
          width: '95%',
          'max-width': 'none',
          top: p.top
        });

        dropdown.css(Foundation.rtl ? 'right':'left', '2.5%');
      } else {
        var settings = target.data(this.attr_name(true) + '-init') || this.settings;

        this.style(dropdown, target, settings);
      }

      return dropdown;
    },

    style : function (dropdown, target, settings) {
      var css = $.extend({position: 'absolute'},
        this.dirs[settings.align].call(dropdown, target, settings));

      dropdown.attr('style', '').css(css);
    },

    // return CSS property object
    // `this` is the dropdown
    dirs : {
      // Calculate target offset
      _base : function (t) {
        var o_p = this.offsetParent(),
            o = o_p.offset(),
            p = t.offset();

        p.top -= o.top;
        p.left -= o.left;

        return p;
      },
      top: function (t, s) {
        var self = Foundation.libs.dropdown,
            p = self.dirs._base.call(this, t),
            pip_offset_base = (t.outerWidth() / 2) - 8;

        this.addClass('drop-top');

        if (t.outerWidth() < this.outerWidth() || self.small()) {
          self.adjust_pip(pip_offset_base, p);
        }

        if (Foundation.rtl) {
          return {left: p.left - this.outerWidth() + t.outerWidth(),
            top: p.top - this.outerHeight()};
        }

        return {left: p.left, top: p.top - this.outerHeight()};
      },
      bottom: function (t, s) {
        var self = Foundation.libs.dropdown,
            p = self.dirs._base.call(this, t),
            pip_offset_base = (t.outerWidth() / 2) - 8;

        if (t.outerWidth() < this.outerWidth() || self.small()) {
          self.adjust_pip(pip_offset_base, p);
        }

        if (self.rtl) {
          return {left: p.left - this.outerWidth() + t.outerWidth(), top: p.top + t.outerHeight()};
        }

        return {left: p.left, top: p.top + t.outerHeight()};
      },
      left: function (t, s) {
        var p = Foundation.libs.dropdown.dirs._base.call(this, t);

        this.addClass('drop-left');

        return {left: p.left - this.outerWidth(), top: p.top};
      },
      right: function (t, s) {
        var p = Foundation.libs.dropdown.dirs._base.call(this, t);

        this.addClass('drop-right');

        return {left: p.left + t.outerWidth(), top: p.top};
      }
    },

    // Insert rule to style psuedo elements
    adjust_pip : function (pip_offset_base, p) {
      var sheet = Foundation.stylesheet;

      if (this.small()) {
        pip_offset_base += p.left - 8;
      }

      this.rule_idx = sheet.cssRules.length;

      var sel_before = '.f-dropdown.open:before',
          sel_after  = '.f-dropdown.open:after',
          css_before = 'left: ' + pip_offset_base + 'px;',
          css_after  = 'left: ' + (pip_offset_base - 1) + 'px;';

      if (sheet.insertRule) {
        sheet.insertRule([sel_before, '{', css_before, '}'].join(' '), this.rule_idx);
        sheet.insertRule([sel_after, '{', css_after, '}'].join(' '), this.rule_idx + 1);
      } else {
        sheet.addRule(sel_before, css_before, this.rule_idx);
        sheet.addRule(sel_after, css_after, this.rule_idx + 1);
      }
    },

    // Remove old dropdown rule index
    clear_idx : function () {
      var sheet = Foundation.stylesheet;

      if (this.rule_idx) {
        sheet.deleteRule(this.rule_idx);
        sheet.deleteRule(this.rule_idx);
        delete this.rule_idx;
      }
    },

    small : function () {
      return matchMedia(Foundation.media_queries.small).matches &&
        !matchMedia(Foundation.media_queries.medium).matches;
    },

    off: function () {
      this.S(this.scope).off('.fndtn.dropdown');
      this.S('html, body').off('.fndtn.dropdown');
      this.S(window).off('.fndtn.dropdown');
      this.S('[data-dropdown-content]').off('.fndtn.dropdown');
    },

    reflow : function () {}
  };
}(jQuery, window, window.document));
<commit_msg>- Allow to reuse dropdown-content element. If a target opened the dropdown, clicking another target will directly open the same dropdown element instead of just closing the previous one. - Remove unnecessary call to this.close.call<commit_after>;(function ($, window, document, undefined) {
  'use strict';

  Foundation.libs.dropdown = {
    name : 'dropdown',

    version : '5.2.2',

    settings : {
      active_class: 'open',
      align: 'bottom',
      is_hover: false,
      opened: function(){},
      closed: function(){}
    },

    init : function (scope, method, options) {
      Foundation.inherit(this, 'throttle');

      this.bindings(method, options);
    },

    events : function (scope) {
      var self = this,
          S = self.S;

      S(this.scope)
        .off('.dropdown')
        .on('click.fndtn.dropdown', '[' + this.attr_name() + ']', function (e) {
          var settings = S(this).data(self.attr_name(true) + '-init') || self.settings;
          if (!settings.is_hover || Modernizr.touch) {
            e.preventDefault();
            self.toggle($(this));
          }
        })
        .on('mouseenter.fndtn.dropdown', '[' + this.attr_name() + '], [' + this.attr_name() + '-content]', function (e) {
          var $this = S(this);
          clearTimeout(self.timeout);

          if ($this.data(self.data_attr())) {
            var dropdown = S('#' + $this.data(self.data_attr())),
                target = $this;
          } else {
            var dropdown = $this;
                target = S("[" + self.attr_name() + "='" + dropdown.attr('id') + "']");
          }

          var settings = target.data(self.attr_name(true) + '-init') || self.settings;

          if(S(e.target).data(self.data_attr()) && settings.is_hover) {
            self.closeall.call(self);
          }

          if (settings.is_hover) self.open.apply(self, [dropdown, target]);
        })
        .on('mouseleave.fndtn.dropdown', '[' + this.attr_name() + '], [' + this.attr_name() + '-content]', function (e) {
          var $this = S(this);
          self.timeout = setTimeout(function () {
            if ($this.data(self.data_attr())) {
              var settings = $this.data(self.data_attr(true) + '-init') || self.settings;
              if (settings.is_hover) self.close.call(self, S('#' + $this.data(self.data_attr())));
            } else {
              var target = S('[' + self.attr_name() + '="' + S(this).attr('id') + '"]'),
                  settings = target.data(self.attr_name(true) + '-init') || self.settings;
              if (settings.is_hover) self.close.call(self, $this);
            }
          }.bind(this), 150);
        })
        .on('click.fndtn.dropdown', function (e) {
          var parent = S(e.target).closest('[' + self.attr_name() + '-content]');

          if (S(e.target).data(self.data_attr()) || S(e.target).parent().data(self.data_attr())) {
            return;
          }
          if (!(S(e.target).data('revealId')) &&
            (parent.length > 0 && (S(e.target).is('[' + self.attr_name() + '-content]') ||
              $.contains(parent.first()[0], e.target)))) {
            e.stopPropagation();
            return;
          }

          self.close.call(self, S('[' + self.attr_name() + '-content]'));
        })
        .on('opened.fndtn.dropdown', '[' + self.attr_name() + '-content]', function () {
            self.settings.opened.call(this);
        })
        .on('closed.fndtn.dropdown', '[' + self.attr_name() + '-content]', function () {
            self.settings.closed.call(this);
        });

      S(window)
        .off('.dropdown')
        .on('resize.fndtn.dropdown', self.throttle(function () {
          self.resize.call(self);
        }, 50));

      this.resize();
    },

    close: function (dropdown) {
      var self = this;
      dropdown.each(function () {
        if (self.S(this).hasClass(self.settings.active_class)) {
          self.S(this)
            .css(Foundation.rtl ? 'right':'left', '-99999px')
            .removeClass(self.settings.active_class)
            .prev('[' + self.attr_name() + ']')
            .removeClass(self.settings.active_class)
            .removeData('target');

          self.S(this).trigger('closed', [dropdown]);
        }
      });
    },

    closeall: function() {
      var self = this;
      $.each(self.S('[' + this.attr_name() + '-content]'), function() {
        self.close.call(self, self.S(this))
      });
    },

    open: function (dropdown, target) {
        this
          .css(dropdown
            .addClass(this.settings.active_class), target);
        dropdown.prev('[' + this.attr_name() + ']').addClass(this.settings.active_class);
        dropdown.data('target', target.get(0)).trigger('opened', [dropdown, target]);
    },

    data_attr: function () {
      if (this.namespace.length > 0) {
        return this.namespace + '-' + this.name;
      }

      return this.name;
    },

    toggle : function (target) {
      var dropdown = this.S('#' + target.data(this.data_attr()));
      if (dropdown.length === 0) {
        // No dropdown found, not continuing
        return;
      }

      this.close.call(this, this.S('[' + this.attr_name() + '-content]').not(dropdown));

      if (dropdown.hasClass(this.settings.active_class)) {
        this.close.call(this, dropdown);
        if (dropdown.data('target') !== target.get(0))
          this.open.call(this, dropdown, target);
      } else {
        this.open.call(this, dropdown, target);
      }
    },

    resize : function () {
      var dropdown = this.S('[' + this.attr_name() + '-content].open'),
          target = this.S("[" + this.attr_name() + "='" + dropdown.attr('id') + "']");

      if (dropdown.length && target.length) {
        this.css(dropdown, target);
      }
    },

    css : function (dropdown, target) {
      this.clear_idx();

      if (this.small()) {
        var p = this.dirs.bottom.call(dropdown, target);

        dropdown.attr('style', '').removeClass('drop-left drop-right drop-top').css({
          position : 'absolute',
          width: '95%',
          'max-width': 'none',
          top: p.top
        });

        dropdown.css(Foundation.rtl ? 'right':'left', '2.5%');
      } else {
        var settings = target.data(this.attr_name(true) + '-init') || this.settings;

        this.style(dropdown, target, settings);
      }

      return dropdown;
    },

    style : function (dropdown, target, settings) {
      var css = $.extend({position: 'absolute'},
        this.dirs[settings.align].call(dropdown, target, settings));

      dropdown.attr('style', '').css(css);
    },

    // return CSS property object
    // `this` is the dropdown
    dirs : {
      // Calculate target offset
      _base : function (t) {
        var o_p = this.offsetParent(),
            o = o_p.offset(),
            p = t.offset();

        p.top -= o.top;
        p.left -= o.left;

        return p;
      },
      top: function (t, s) {
        var self = Foundation.libs.dropdown,
            p = self.dirs._base.call(this, t),
            pip_offset_base = (t.outerWidth() / 2) - 8;

        this.addClass('drop-top');

        if (t.outerWidth() < this.outerWidth() || self.small()) {
          self.adjust_pip(pip_offset_base, p);
        }

        if (Foundation.rtl) {
          return {left: p.left - this.outerWidth() + t.outerWidth(),
            top: p.top - this.outerHeight()};
        }

        return {left: p.left, top: p.top - this.outerHeight()};
      },
      bottom: function (t, s) {
        var self = Foundation.libs.dropdown,
            p = self.dirs._base.call(this, t),
            pip_offset_base = (t.outerWidth() / 2) - 8;

        if (t.outerWidth() < this.outerWidth() || self.small()) {
          self.adjust_pip(pip_offset_base, p);
        }

        if (self.rtl) {
          return {left: p.left - this.outerWidth() + t.outerWidth(), top: p.top + t.outerHeight()};
        }

        return {left: p.left, top: p.top + t.outerHeight()};
      },
      left: function (t, s) {
        var p = Foundation.libs.dropdown.dirs._base.call(this, t);

        this.addClass('drop-left');

        return {left: p.left - this.outerWidth(), top: p.top};
      },
      right: function (t, s) {
        var p = Foundation.libs.dropdown.dirs._base.call(this, t);

        this.addClass('drop-right');

        return {left: p.left + t.outerWidth(), top: p.top};
      }
    },

    // Insert rule to style psuedo elements
    adjust_pip : function (pip_offset_base, p) {
      var sheet = Foundation.stylesheet;

      if (this.small()) {
        pip_offset_base += p.left - 8;
      }

      this.rule_idx = sheet.cssRules.length;

      var sel_before = '.f-dropdown.open:before',
          sel_after  = '.f-dropdown.open:after',
          css_before = 'left: ' + pip_offset_base + 'px;',
          css_after  = 'left: ' + (pip_offset_base - 1) + 'px;';

      if (sheet.insertRule) {
        sheet.insertRule([sel_before, '{', css_before, '}'].join(' '), this.rule_idx);
        sheet.insertRule([sel_after, '{', css_after, '}'].join(' '), this.rule_idx + 1);
      } else {
        sheet.addRule(sel_before, css_before, this.rule_idx);
        sheet.addRule(sel_after, css_after, this.rule_idx + 1);
      }
    },

    // Remove old dropdown rule index
    clear_idx : function () {
      var sheet = Foundation.stylesheet;

      if (this.rule_idx) {
        sheet.deleteRule(this.rule_idx);
        sheet.deleteRule(this.rule_idx);
        delete this.rule_idx;
      }
    },

    small : function () {
      return matchMedia(Foundation.media_queries.small).matches &&
        !matchMedia(Foundation.media_queries.medium).matches;
    },

    off: function () {
      this.S(this.scope).off('.fndtn.dropdown');
      this.S('html, body').off('.fndtn.dropdown');
      this.S(window).off('.fndtn.dropdown');
      this.S('[data-dropdown-content]').off('.fndtn.dropdown');
    },

    reflow : function () {}
  };
}(jQuery, window, window.document));
<|endoftext|>


<commit_before>;(function ($, window, document, undefined) {
  'use strict';

  Foundation.libs.dropdown = {
    name : 'dropdown',

    version : '5.0.0',

    settings : {
      active_class: 'open',
      is_hover: false,
      opened: function(){},
      closed: function(){}
    },

    init : function (scope, method, options) {
      Foundation.inherit(this, 'throttle');

      this.bindings(method, options);
    },

    events : function (scope) {
      var self = this;

      $(this.scope)
        .off('.dropdown')
        .on('click.fndtn.dropdown', '[data-dropdown]', function (e) {
          var settings = $(this).data('dropdown-init');
          e.preventDefault();

          if (!settings.is_hover && !Modernizr.touch) self.toggle($(this));
        })
        .on('mouseenter.fndtn.dropdown', '[data-dropdown], [data-dropdown-content]', function (e) {
          var $this = $(this);
          clearTimeout(self.timeout);

          if ($this.data('dropdown')) {
            var dropdown = $('#' + $this.data('dropdown')),
                target = $this;
          } else {
            var dropdown = $this;
                target = $("[data-dropdown='" + dropdown.attr('id') + "']");
          }

          var settings = target.data('dropdown-init');
          if (settings.is_hover) self.open.apply(self, [dropdown, target]);
        })
        .on('mouseleave.fndtn.dropdown', '[data-dropdown], [data-dropdown-content]', function (e) {
          var $this = $(this);
          self.timeout = setTimeout(function () {
            if ($this.data('dropdown')) {
              var settings = $this.data('dropdown-init');
              if (settings.is_hover) self.close.call(self, $('#' + $this.data('dropdown')));
            } else {
              var target = $('[data-dropdown="' + $(this).attr('id') + '"]'),
                  settings = target.data('dropdown-init');
              if (settings.is_hover) self.close.call(self, $this);
            }
          }.bind(this), 150);
        })
        .on('click.fndtn.dropdown', function (e) {
          var parent = $(e.target).closest('[data-dropdown-content]');

          if ($(e.target).data('dropdown') || $(e.target).parent().data('dropdown')) {
            return;
          }
          if (!($(e.target).data('revealId')) &&
            (parent.length > 0 && ($(e.target).is('[data-dropdown-content]') ||
              $.contains(parent.first()[0], e.target)))) {
            e.stopPropagation();
            return;
          }

          self.close.call(self, $('[data-dropdown-content]'));
        })
        .on('opened.fndtn.dropdown', '[data-dropdown-content]', this.settings.opened)
        .on('closed.fndtn.dropdown', '[data-dropdown-content]', this.settings.closed);

      $(window)
        .off('.dropdown')
        .on('resize.fndtn.dropdown', self.throttle(function () {
          self.resize.call(self);
        }, 50)).trigger('resize');
    },

    close: function (dropdown) {
      var self = this;
      dropdown.each(function () {
        if ($(this).hasClass(self.settings.active_class)) {
          $(this)
            .css(Foundation.rtl ? 'right':'left', '-99999px')
            .removeClass(self.settings.active_class);
          $(this).trigger('closed');
        }
      });
    },

    open: function (dropdown, target) {
        this
          .css(dropdown
            .addClass(this.settings.active_class), target);
        dropdown.trigger('opened');
    },

    toggle : function (target) {
      var dropdown = $('#' + target.data('dropdown'));
      if (dropdown.length === 0) {
        // No dropdown found, not continuing
        return;
      }

      this.close.call(this, $('[data-dropdown-content]').not(dropdown));

      if (dropdown.hasClass(this.settings.active_class)) {
        this.close.call(this, dropdown);
      } else {
        this.close.call(this, $('[data-dropdown-content]'))
        this.open.call(this, dropdown, target);
      }
    },

    resize : function () {
      var dropdown = $('[data-dropdown-content].open'),
          target = $("[data-dropdown='" + dropdown.attr('id') + "']");

      if (dropdown.length && target.length) {
        this.css(dropdown, target);
      }
    },

    css : function (dropdown, target) {
      var offset_parent = dropdown.offsetParent(),
          position = target.offset();

      position.top -= offset_parent.offset().top;
      position.left -= offset_parent.offset().left;

      if (this.small()) {
        dropdown.css({
          position : 'absolute',
          width: '95%',
          'max-width': 'none',
          top: position.top + target.outerHeight()
        });
        dropdown.css(Foundation.rtl ? 'right':'left', '2.5%');
      } else {
        if (!Foundation.rtl && $(window).width() > dropdown.outerWidth() + target.offset().left) {
          var left = position.left;
          if (dropdown.hasClass('right')) {
            dropdown.removeClass('right');
          }
        } else {
          if (!dropdown.hasClass('right')) {
            dropdown.addClass('right');
          }
          var left = position.left - (dropdown.outerWidth() - target.outerWidth());
        }

        dropdown.attr('style', '').css({
          position : 'absolute',
          top: position.top + target.outerHeight(),
          left: left
        });
      }

      return dropdown;
    },

    small : function () {
      return matchMedia(Foundation.media_queries.small).matches &&
        !matchMedia(Foundation.media_queries.medium).matches;
    },

    off: function () {
      $(this.scope).off('.fndtn.dropdown');
      $('html, body').off('.fndtn.dropdown');
      $(window).off('.fndtn.dropdown');
      $('[data-dropdown-content]').off('.fndtn.dropdown');
      this.settings.init = false;
    },

    reflow : function () {}
  };
}(jQuery, this, this.document));
<commit_msg>Fix mobile dropdown hover.<commit_after>;(function ($, window, document, undefined) {
  'use strict';

  Foundation.libs.dropdown = {
    name : 'dropdown',

    version : '5.0.0',

    settings : {
      active_class: 'open',
      is_hover: false,
      opened: function(){},
      closed: function(){}
    },

    init : function (scope, method, options) {
      Foundation.inherit(this, 'throttle');

      this.bindings(method, options);
    },

    events : function (scope) {
      var self = this;

      $(this.scope)
        .off('.dropdown')
        .on('click.fndtn.dropdown', '[data-dropdown]', function (e) {
          var settings = $(this).data('dropdown-init');
          e.preventDefault();

          if (!settings.is_hover || Modernizr.touch) self.toggle($(this));
        })
        .on('mouseenter.fndtn.dropdown', '[data-dropdown], [data-dropdown-content]', function (e) {
          var $this = $(this);
          clearTimeout(self.timeout);

          if ($this.data('dropdown')) {
            var dropdown = $('#' + $this.data('dropdown')),
                target = $this;
          } else {
            var dropdown = $this;
                target = $("[data-dropdown='" + dropdown.attr('id') + "']");
          }

          var settings = target.data('dropdown-init');
          if (settings.is_hover) self.open.apply(self, [dropdown, target]);
        })
        .on('mouseleave.fndtn.dropdown', '[data-dropdown], [data-dropdown-content]', function (e) {
          var $this = $(this);
          self.timeout = setTimeout(function () {
            if ($this.data('dropdown')) {
              var settings = $this.data('dropdown-init');
              if (settings.is_hover) self.close.call(self, $('#' + $this.data('dropdown')));
            } else {
              var target = $('[data-dropdown="' + $(this).attr('id') + '"]'),
                  settings = target.data('dropdown-init');
              if (settings.is_hover) self.close.call(self, $this);
            }
          }.bind(this), 150);
        })
        .on('click.fndtn.dropdown', function (e) {
          var parent = $(e.target).closest('[data-dropdown-content]');

          if ($(e.target).data('dropdown') || $(e.target).parent().data('dropdown')) {
            return;
          }
          if (!($(e.target).data('revealId')) &&
            (parent.length > 0 && ($(e.target).is('[data-dropdown-content]') ||
              $.contains(parent.first()[0], e.target)))) {
            e.stopPropagation();
            return;
          }

          self.close.call(self, $('[data-dropdown-content]'));
        })
        .on('opened.fndtn.dropdown', '[data-dropdown-content]', this.settings.opened)
        .on('closed.fndtn.dropdown', '[data-dropdown-content]', this.settings.closed);

      $(window)
        .off('.dropdown')
        .on('resize.fndtn.dropdown', self.throttle(function () {
          self.resize.call(self);
        }, 50)).trigger('resize');
    },

    close: function (dropdown) {
      var self = this;
      dropdown.each(function () {
        if ($(this).hasClass(self.settings.active_class)) {
          $(this)
            .css(Foundation.rtl ? 'right':'left', '-99999px')
            .removeClass(self.settings.active_class);
          $(this).trigger('closed');
        }
      });
    },

    open: function (dropdown, target) {
        this
          .css(dropdown
            .addClass(this.settings.active_class), target);
        dropdown.trigger('opened');
    },

    toggle : function (target) {
      var dropdown = $('#' + target.data('dropdown'));
      if (dropdown.length === 0) {
        // No dropdown found, not continuing
        return;
      }

      this.close.call(this, $('[data-dropdown-content]').not(dropdown));

      if (dropdown.hasClass(this.settings.active_class)) {
        this.close.call(this, dropdown);
      } else {
        this.close.call(this, $('[data-dropdown-content]'))
        this.open.call(this, dropdown, target);
      }
    },

    resize : function () {
      var dropdown = $('[data-dropdown-content].open'),
          target = $("[data-dropdown='" + dropdown.attr('id') + "']");

      if (dropdown.length && target.length) {
        this.css(dropdown, target);
      }
    },

    css : function (dropdown, target) {
      var offset_parent = dropdown.offsetParent(),
          position = target.offset();

      position.top -= offset_parent.offset().top;
      position.left -= offset_parent.offset().left;

      if (this.small()) {
        dropdown.css({
          position : 'absolute',
          width: '95%',
          'max-width': 'none',
          top: position.top + target.outerHeight()
        });
        dropdown.css(Foundation.rtl ? 'right':'left', '2.5%');
      } else {
        if (!Foundation.rtl && $(window).width() > dropdown.outerWidth() + target.offset().left) {
          var left = position.left;
          if (dropdown.hasClass('right')) {
            dropdown.removeClass('right');
          }
        } else {
          if (!dropdown.hasClass('right')) {
            dropdown.addClass('right');
          }
          var left = position.left - (dropdown.outerWidth() - target.outerWidth());
        }

        dropdown.attr('style', '').css({
          position : 'absolute',
          top: position.top + target.outerHeight(),
          left: left
        });
      }

      return dropdown;
    },

    small : function () {
      return matchMedia(Foundation.media_queries.small).matches &&
        !matchMedia(Foundation.media_queries.medium).matches;
    },

    off: function () {
      $(this.scope).off('.fndtn.dropdown');
      $('html, body').off('.fndtn.dropdown');
      $(window).off('.fndtn.dropdown');
      $('[data-dropdown-content]').off('.fndtn.dropdown');
      this.settings.init = false;
    },

    reflow : function () {}
  };
}(jQuery, this, this.document));
<|endoftext|>


<commit_before>/**
 * Created by eamonnmaguire on 19/06/2014.
 */

var ISATabViewer = {};

ISATabViewer.investigation = {"ONTOLOGY SOURCE REFERENCE": {},
    "INVESTIGATION": {},
    "INVESTIGATION CONTACTS": {},
    "INVESTIGATION PUBLICATIONS": {},
    "STUDY": []};

ISATabViewer.options = {
    splitter: "\t"  // for TSV or "," for CSV
};

ISATabViewer.spreadsheets = {
    "files": {}
};

hashCode = function(s){
  return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);
}

ISATabViewer.rendering = {

    /*
     * Renders ISATab given an investigation url which is retrieved and processed and options to configure the display
     *
     */

    process_file: function (file_name, file_contents, placement) {
        var lines = file_contents.split("\n");

        var current_section = "";
        var current_study;

        for (var line in lines) {
            var __ret = ISATabViewer.rendering.process_investigation_file_line(lines[line], current_study, current_section);
            if (__ret != undefined) {
                current_study = __ret.current_study;
                current_section = __ret.current_section;
            }
        }

        if (current_study != undefined)
            ISATabViewer.investigation["STUDY"].push(current_study);
        ISATabViewer.rendering.render_study_list(placement);

        // TODO: Now we need to load the rest of the files. Study samples and assays in to the ISATabViewer.spreadsheets.files object

        for (var study_index in ISATabViewer.investigation.STUDY) {

            var study_information = ISATabViewer.investigation.STUDY[study_index];
            var study_file = ISATabViewer.rendering.replace_str("\"", "", study_information.STUDY["Study File Name"][0]);
            var base_directory = file_name.substr(0, file_name.lastIndexOf("/") + 1);

            var assays = ISATabViewer.rendering.generate_records(study_information, "STUDY ASSAYS");

            $.ajax({
                url: base_directory + study_file,
                success: function (study_file_contents) {
                    ISATabViewer.spreadsheets.files[study_file] = {"headers": [], "rows": []};

                    var lines = study_file_contents.split("\n");
                    var count = 0;

                    var position_to_characteristic = {};
                    var characteristics = {};

                    for (var line in lines) {

                        var line_contents = lines[line].trim();
                        parts = line_contents.split(ISATabViewer.options.splitter);
                        var processed_parts = [];

                        parts.forEach(function (part, index) {
                            var column_value = ISATabViewer.rendering.replace_str("\"", "", part);
                            processed_parts.push(column_value);

                            // we store information about the characteristics of the samples (sample files begin with 's_')
                            // to give an overview in the study info page

                            if (count == 0 && column_value.indexOf("Characteristics") >= 0) {

                                characteristics[column_value] = {};
                                position_to_characteristic[index] = column_value;
                            } else {
                                if (index in position_to_characteristic) {
                                    var characteristic_name = position_to_characteristic[index];

                                    if (!(column_value in characteristics[characteristic_name])) {
                                        characteristics[characteristic_name][column_value] = 0;
                                    }
                                    characteristics[characteristic_name][column_value]++;
                                }
                            }

                        });
                        if (count == 0) {
                            // we have the headers
                            ISATabViewer.spreadsheets.files[study_file]["headers"] = processed_parts;
                        } else {
                            ISATabViewer.spreadsheets.files[study_file]["rows"].push({"columns": processed_parts});
                        }
                        count++;
                    }

                    ISATabViewer.spreadsheets.files[study_file]["stats"] = characteristics;

                    if ($('#sample-distribution').length) {
                        var sample_stats = ISATabViewer.rendering.process_study_sample_statistics(characteristics);

                        var source = $("#sample-distribution-template").html();
                        var template = Handlebars.compile(source);
                        var html = template({"sample_stats": sample_stats});
                        $("#sample-distribution").html(html);
                    }

                }
            });
        }

    },

    process_investigation_file_line: function (line_contents, current_study, current_section) {
        if (line_contents.lastIndexOf("#", 0) === 0)
            return;
        if (line_contents in ISATabViewer.investigation || (current_study != undefined && line_contents in current_study)) {
            current_section = line_contents;
            if (current_section == 'STUDY') {
                if (current_study != undefined) ISATabViewer.investigation["STUDY"].push(current_study);
                current_study = ISATabViewer.rendering.create_study_template();
            }
        } else {
            var parts = line_contents.split(ISATabViewer.options.splitter);

            if (parts.length > 0) {

                if (current_study != undefined) {
                    current_study[current_section][parts[0]] = $.grep(parts, function (v, i) {
                        return v != "" && i != 0;
                    });
                } else {
                    ISATabViewer.investigation[current_section][parts[0]] = $.grep(parts, function (v, i) {
                        return v != "" && i != 0;
                    });
                }
            }
        }
        return {current_study: current_study, current_section: current_section, parts: parts};
    },

    process_gist: function (gist, gist_id, placement) {

        for (var file in gist.files) {
            $.ajax({
                url: 'https://gist.github.com/' + gist_id + '.json?file=' + gist.files[file],
                dataType: 'jsonp',
                success: function (file_data) {

                    // we will be breaking things down in to investigation, study and assays. Then rendering everything at the end.
                    var xmlDoc = $.parseXML(file_data.div);

                    var filename = file_data.files[0];

                    if (filename.indexOf('i_') != -1) {
                        // process investigation file

                        var divs = $(xmlDoc).find('div.line');

                        var current_section = "";
                        var current_study;

                        for (var div in divs) {
                            if (divs[div].textContent) {
                                var line_contents = divs[div].textContent.trim();
                                var __ret = ISATabViewer.rendering.process_investigation_file_line(line_contents, current_study, current_section);
                                current_study = __ret.current_study;
                                current_section = __ret.current_section;
                            }
                        }

                        if (current_study != undefined) ISATabViewer.investigation["STUDY"].push(current_study);

                        ISATabViewer.rendering.render_study_list(placement);
                    } else {

                        var divs = $(xmlDoc).find('div.line');

                        ISATabViewer.spreadsheets.files[filename] = {"headers": [], "rows": []};

                        var count = 0;

                        var position_to_characteristic = {};
                        var characteristics = {};

                        for (var div in divs) {
                            if (divs[div].textContent) {
                                var line_contents = divs[div].textContent.trim();
                                parts = line_contents.split(ISATabViewer.options.splitter);
                                var processed_parts = [];

                                parts.forEach(function (part, index) {
                                    var column_value = ISATabViewer.rendering.replace_str("\"", "", part);
                                    processed_parts.push(column_value);

                                    // we store information about the characteristics of the samples (sample files begin with 's_')
                                    // to give an overview in the study info page
                                    if (filename.indexOf("s_") != -1) {
                                        if (count == 0 && column_value.indexOf("Characteristics") >= 0) {

                                            characteristics[column_value] = {};
                                            position_to_characteristic[index] = column_value;
                                        } else {
                                            if (index in position_to_characteristic) {
                                                var characteristic_name = position_to_characteristic[index];

                                                if (!(column_value in characteristics[characteristic_name])) {
                                                    characteristics[characteristic_name][column_value] = 0;
                                                }
                                                characteristics[characteristic_name][column_value]++;
                                            }
                                        }
                                    }
                                });
                                if (count == 0) {
                                    // we have the headers
                                    ISATabViewer.spreadsheets.files[filename]["headers"] = processed_parts;
                                } else {
                                    ISATabViewer.spreadsheets.files[filename]["rows"].push({"columns": processed_parts});
                                }
                                count++;
                            }
                        }

                        if (filename.indexOf("s_") != -1) {
                            ISATabViewer.spreadsheets.files[filename]["stats"] = characteristics;

                            if ($('#sample-distribution').length) {
                                var sample_stats = ISATabViewer.rendering.process_study_sample_statistics(characteristics);

                                var source = $("#sample-distribution-template").html();
                                var template = Handlebars.compile(source);
                                var html = template({"sample_stats": sample_stats});
                                $("#sample-distribution").html(html);
                            }
                        }
                    }

                }
            });
        }
    },

    render_study_list: function () {
        // Render study list
        var studies = [];

        for (var study_index in ISATabViewer.investigation.STUDY) {

            studies.push(hashCode(ISATabViewer.rendering.replace_str("\"", "", ISATabViewer.investigation.STUDY[study_index].STUDY["Study Identifier"][0])));
        }

        $("#isa-breadcrumb-items").html('
' + studies[0] + '
');

        var source = $("#study-list-template").html();
        var template = Handlebars.compile(source);
        var html = template({"studies": studies});
        $("#study-list").html(html);

        ISATabViewer.rendering.render_study(studies[0]);
    },

    set_active_list_item: function (study_id) {
        console.log(study_id);
        $("#study-list").find("li").each(function () {
            $(this).removeClass("active");
        });

        $("#list-" + study_id).addClass("active");
    },

    process_study_sample_statistics: function (stats) {
        var study_sample_stats = [];
        for (var characteristic_name in stats) {
            var record = {"name": characteristic_name, "distribution": []};

            for (var distribution_item in stats[characteristic_name]) {
                record["distribution"].push({"name": distribution_item, "value": stats[characteristic_name][distribution_item]})
            }
            study_sample_stats.push(record);
        }
        return study_sample_stats
    },

    render_study: function (study_id) {
        console.log(study_id);
        this.set_active_list_item(study_id);

        $("#isa-breadcrumb-items").html('
' + study_id + '
');
        var study = {};
        for (var study_index in ISATabViewer.investigation.STUDY) {

            var study_information = ISATabViewer.investigation.STUDY[study_index];

            if (study_information.STUDY["Study Identifier"][0].indexOf(study_id) != -1) {
                study.study_id = ISATabViewer.rendering.replace_str("\"", "", study_information.STUDY["Study Identifier"][0]);
                study.study_title = ISATabViewer.rendering.replace_str("\"", "", study_information.STUDY["Study Title"][0]);
                study.study_description = ISATabViewer.rendering.replace_str("\"", "", study_information.STUDY["Study Description"][0]);
                study.study_file = ISATabViewer.rendering.replace_str("\"", "", study_information.STUDY["Study File Name"][0]);

                study.publications = ISATabViewer.rendering.generate_records(study_information, "STUDY PUBLICATIONS");
                study.protocols = ISATabViewer.rendering.generate_records(study_information, "STUDY PROTOCOLS");
                study.contacts = ISATabViewer.rendering.generate_records(study_information, "STUDY CONTACTS");
                study.factors = ISATabViewer.rendering.generate_records(study_information, "STUDY FACTORS");
                study.assays = ISATabViewer.rendering.generate_records(study_information, "STUDY ASSAYS");

                ISATabViewer.rendering.postprocess_assay_records(study.assays);

                if (study.study_file in ISATabViewer.spreadsheets.files) {
                    // we have already loaded the study sample file, so can load the distributions

                    study.sample_stats = ISATabViewer.spreadsheets.files[study.study_file]["stats"];

                    study.sample_stats = this.process_study_sample_statistics(study.sample_stats);

                }
            }
        }

        var source = $("#study-template").html();
        var template = Handlebars.compile(source);
        var html = template(study);

        $("#study-info").html(html);
    },

    render_assay: function (study_id, file_name) {

        $("#isa-breadcrumb-items").html('
' + study_id + '

' + file_name + '
');

        var spreadsheet = ISATabViewer.spreadsheets.files[file_name];
        var source = $("#table-template").html();
        var template = Handlebars.compile(source);
        var html = template(spreadsheet);

        $("#study-info").html(html);
    },

    /*
     This function adds in information about which images to use for example to show a particular type of assay.
     */
    postprocess_assay_records: function (records) {
        for (var assay_index in records) {
            var assay = records[assay_index];

            var measurement_type = assay["Study Assay Measurement Type"];

            assay.icon = measurement_type.indexOf("metabolite") >= 0 ? "assay-icon-metabolomics"
                : measurement_type.indexOf("prote") >= 0 ? "assay-icon-proteomics"
                : measurement_type.indexOf("transcript") >= 0 ? "assay-icon-transcriptomics"
                : measurement_type.indexOf("chemistry") >= 0 ? "assay-icon-chemistry"
                : measurement_type.indexOf("genom") >= 0 ? "assay-icon-genomics"
                : "";

        }
    },

    generate_records: function (study_information, field_name) {
        var result = [];
        for (var field in study_information[field_name]) {

            var records = study_information[field_name][field];
            for (var i = 0; i < records.length; i++) {
                if (result[i] == undefined) {
                    result[i] = {}
                }
                result[i][field] = ISATabViewer.rendering.replace_str("\"", "", records[i]);
            }
        }

        return result;
    },

    replace_str: function (find, replace, str) {
        return str.replace(new RegExp(find, 'g'), replace);
    },

    render_isatab_from_gist: function (gist_id, placement, options) {

        $.ajax({
            url: 'https://gist.github.com/' + gist_id + '.json',
            dataType: 'jsonp',
            success: function (gist) {
                ISATabViewer.investigation.STUDY = [];
                ISATabViewer.rendering.process_gist(gist, gist_id, placement)
                $("#download-button").html('Download this study.');
            }
        });
    },

    render_isatab_from_file: function (investigation_file, placement, options) {

        $.ajax({
            url: investigation_file,
            success: function (file) {
                ISATabViewer.investigation.STUDY = [];
                ISATabViewer.rendering.process_file(investigation_file, file, placement);
//                $("#download-button").html('Download this study.');
            }
        });
    },

    create_study_template: function () {
        return {
            "STUDY": {},
            "STUDY CONTACTS": {},
            "STUDY PUBLICATIONS": {},
            "STUDY FACTORS": {},
            "STUDY DESIGN DESCRIPTORS": {},
            "STUDY ASSAYS": {},
            "STUDY PROTOCOLS": {}
        };
    }
};<commit_msg>Renamed study_id to study_id_hash<commit_after>/**
 * Created by eamonnmaguire on 19/06/2014.
 */

var ISATabViewer = {};

ISATabViewer.investigation = {"ONTOLOGY SOURCE REFERENCE": {},
    "INVESTIGATION": {},
    "INVESTIGATION CONTACTS": {},
    "INVESTIGATION PUBLICATIONS": {},
    "STUDY": []};

ISATabViewer.options = {
    splitter: "\t"  // for TSV or "," for CSV
};

ISATabViewer.spreadsheets = {
    "files": {}
};

hashCode = function(s){
  return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);
}

ISATabViewer.rendering = {

    /*
     * Renders ISATab given an investigation url which is retrieved and processed and options to configure the display
     *
     */

    process_file: function (file_name, file_contents, placement) {
        var lines = file_contents.split("\n");

        var current_section = "";
        var current_study;

        for (var line in lines) {
            var __ret = ISATabViewer.rendering.process_investigation_file_line(lines[line], current_study, current_section);
            if (__ret != undefined) {
                current_study = __ret.current_study;
                current_section = __ret.current_section;
            }
        }

        if (current_study != undefined)
            ISATabViewer.investigation["STUDY"].push(current_study);
        ISATabViewer.rendering.render_study_list(placement);

        // TODO: Now we need to load the rest of the files. Study samples and assays in to the ISATabViewer.spreadsheets.files object

        for (var study_index in ISATabViewer.investigation.STUDY) {

            var study_information = ISATabViewer.investigation.STUDY[study_index];
            var study_file = ISATabViewer.rendering.replace_str("\"", "", study_information.STUDY["Study File Name"][0]);
            var base_directory = file_name.substr(0, file_name.lastIndexOf("/") + 1);

            var assays = ISATabViewer.rendering.generate_records(study_information, "STUDY ASSAYS");

            $.ajax({
                url: base_directory + study_file,
                success: function (study_file_contents) {
                    ISATabViewer.spreadsheets.files[study_file] = {"headers": [], "rows": []};

                    var lines = study_file_contents.split("\n");
                    var count = 0;

                    var position_to_characteristic = {};
                    var characteristics = {};

                    for (var line in lines) {

                        var line_contents = lines[line].trim();
                        parts = line_contents.split(ISATabViewer.options.splitter);
                        var processed_parts = [];

                        parts.forEach(function (part, index) {
                            var column_value = ISATabViewer.rendering.replace_str("\"", "", part);
                            processed_parts.push(column_value);

                            // we store information about the characteristics of the samples (sample files begin with 's_')
                            // to give an overview in the study info page

                            if (count == 0 && column_value.indexOf("Characteristics") >= 0) {

                                characteristics[column_value] = {};
                                position_to_characteristic[index] = column_value;
                            } else {
                                if (index in position_to_characteristic) {
                                    var characteristic_name = position_to_characteristic[index];

                                    if (!(column_value in characteristics[characteristic_name])) {
                                        characteristics[characteristic_name][column_value] = 0;
                                    }
                                    characteristics[characteristic_name][column_value]++;
                                }
                            }

                        });
                        if (count == 0) {
                            // we have the headers
                            ISATabViewer.spreadsheets.files[study_file]["headers"] = processed_parts;
                        } else {
                            ISATabViewer.spreadsheets.files[study_file]["rows"].push({"columns": processed_parts});
                        }
                        count++;
                    }

                    ISATabViewer.spreadsheets.files[study_file]["stats"] = characteristics;

                    if ($('#sample-distribution').length) {
                        var sample_stats = ISATabViewer.rendering.process_study_sample_statistics(characteristics);

                        var source = $("#sample-distribution-template").html();
                        var template = Handlebars.compile(source);
                        var html = template({"sample_stats": sample_stats});
                        $("#sample-distribution").html(html);
                    }

                }
            });
        }

    },

    process_investigation_file_line: function (line_contents, current_study, current_section) {
        if (line_contents.lastIndexOf("#", 0) === 0)
            return;
        if (line_contents in ISATabViewer.investigation || (current_study != undefined && line_contents in current_study)) {
            current_section = line_contents;
            if (current_section == 'STUDY') {
                if (current_study != undefined) ISATabViewer.investigation["STUDY"].push(current_study);
                current_study = ISATabViewer.rendering.create_study_template();
            }
        } else {
            var parts = line_contents.split(ISATabViewer.options.splitter);

            if (parts.length > 0) {

                if (current_study != undefined) {
                    current_study[current_section][parts[0]] = $.grep(parts, function (v, i) {
                        return v != "" && i != 0;
                    });
                } else {
                    ISATabViewer.investigation[current_section][parts[0]] = $.grep(parts, function (v, i) {
                        return v != "" && i != 0;
                    });
                }
            }
        }
        return {current_study: current_study, current_section: current_section, parts: parts};
    },

    process_gist: function (gist, gist_id, placement) {

        for (var file in gist.files) {
            $.ajax({
                url: 'https://gist.github.com/' + gist_id + '.json?file=' + gist.files[file],
                dataType: 'jsonp',
                success: function (file_data) {

                    // we will be breaking things down in to investigation, study and assays. Then rendering everything at the end.
                    var xmlDoc = $.parseXML(file_data.div);

                    var filename = file_data.files[0];

                    if (filename.indexOf('i_') != -1) {
                        // process investigation file

                        var divs = $(xmlDoc).find('div.line');

                        var current_section = "";
                        var current_study;

                        for (var div in divs) {
                            if (divs[div].textContent) {
                                var line_contents = divs[div].textContent.trim();
                                var __ret = ISATabViewer.rendering.process_investigation_file_line(line_contents, current_study, current_section);
                                current_study = __ret.current_study;
                                current_section = __ret.current_section;
                            }
                        }

                        if (current_study != undefined) ISATabViewer.investigation["STUDY"].push(current_study);

                        ISATabViewer.rendering.render_study_list(placement);
                    } else {

                        var divs = $(xmlDoc).find('div.line');

                        ISATabViewer.spreadsheets.files[filename] = {"headers": [], "rows": []};

                        var count = 0;

                        var position_to_characteristic = {};
                        var characteristics = {};

                        for (var div in divs) {
                            if (divs[div].textContent) {
                                var line_contents = divs[div].textContent.trim();
                                parts = line_contents.split(ISATabViewer.options.splitter);
                                var processed_parts = [];

                                parts.forEach(function (part, index) {
                                    var column_value = ISATabViewer.rendering.replace_str("\"", "", part);
                                    processed_parts.push(column_value);

                                    // we store information about the characteristics of the samples (sample files begin with 's_')
                                    // to give an overview in the study info page
                                    if (filename.indexOf("s_") != -1) {
                                        if (count == 0 && column_value.indexOf("Characteristics") >= 0) {

                                            characteristics[column_value] = {};
                                            position_to_characteristic[index] = column_value;
                                        } else {
                                            if (index in position_to_characteristic) {
                                                var characteristic_name = position_to_characteristic[index];

                                                if (!(column_value in characteristics[characteristic_name])) {
                                                    characteristics[characteristic_name][column_value] = 0;
                                                }
                                                characteristics[characteristic_name][column_value]++;
                                            }
                                        }
                                    }
                                });
                                if (count == 0) {
                                    // we have the headers
                                    ISATabViewer.spreadsheets.files[filename]["headers"] = processed_parts;
                                } else {
                                    ISATabViewer.spreadsheets.files[filename]["rows"].push({"columns": processed_parts});
                                }
                                count++;
                            }
                        }

                        if (filename.indexOf("s_") != -1) {
                            ISATabViewer.spreadsheets.files[filename]["stats"] = characteristics;

                            if ($('#sample-distribution').length) {
                                var sample_stats = ISATabViewer.rendering.process_study_sample_statistics(characteristics);

                                var source = $("#sample-distribution-template").html();
                                var template = Handlebars.compile(source);
                                var html = template({"sample_stats": sample_stats});
                                $("#sample-distribution").html(html);
                            }
                        }
                    }

                }
            });
        }
    },

    render_study_list: function () {
        // Render study list
        var studies = [];

        for (var study_index in ISATabViewer.investigation.STUDY) {

            studies.push(hashCode(ISATabViewer.rendering.replace_str("\"", "", ISATabViewer.investigation.STUDY[study_index].STUDY["Study Identifier"][0])));
        }

        $("#isa-breadcrumb-items").html('
' + studies[0] + '
');

        var source = $("#study-list-template").html();
        var template = Handlebars.compile(source);
        var html = template({"studies": studies});
        $("#study-list").html(html);

        ISATabViewer.rendering.render_study(studies[0]);
    },

    set_active_list_item: function (study_id) {
        console.log(study_id);
        $("#study-list").find("li").each(function () {
            $(this).removeClass("active");
        });

        $("#list-" + study_id).addClass("active");
    },

    process_study_sample_statistics: function (stats) {
        var study_sample_stats = [];
        for (var characteristic_name in stats) {
            var record = {"name": characteristic_name, "distribution": []};

            for (var distribution_item in stats[characteristic_name]) {
                record["distribution"].push({"name": distribution_item, "value": stats[characteristic_name][distribution_item]})
            }
            study_sample_stats.push(record);
        }
        return study_sample_stats
    },

    render_study: function (study_id_hash) {
        console.log(study_id_hash);
        this.set_active_list_item(study_id_hash);

        $("#isa-breadcrumb-items").html('
' + study_id_hash + '
');
        var study = {};
        for (var study_index in ISATabViewer.investigation.STUDY) {

            var study_information = ISATabViewer.investigation.STUDY[study_index];

            if (study_information.STUDY["Study Identifier"][0].indexOf(study_id_hash) != -1) {
                study.study_id = ISATabViewer.rendering.replace_str("\"", "", study_information.STUDY["Study Identifier"][0]);
                study.study_title = ISATabViewer.rendering.replace_str("\"", "", study_information.STUDY["Study Title"][0]);
                study.study_description = ISATabViewer.rendering.replace_str("\"", "", study_information.STUDY["Study Description"][0]);
                study.study_file = ISATabViewer.rendering.replace_str("\"", "", study_information.STUDY["Study File Name"][0]);

                study.publications = ISATabViewer.rendering.generate_records(study_information, "STUDY PUBLICATIONS");
                study.protocols = ISATabViewer.rendering.generate_records(study_information, "STUDY PROTOCOLS");
                study.contacts = ISATabViewer.rendering.generate_records(study_information, "STUDY CONTACTS");
                study.factors = ISATabViewer.rendering.generate_records(study_information, "STUDY FACTORS");
                study.assays = ISATabViewer.rendering.generate_records(study_information, "STUDY ASSAYS");

                ISATabViewer.rendering.postprocess_assay_records(study.assays);

                if (study.study_file in ISATabViewer.spreadsheets.files) {
                    // we have already loaded the study sample file, so can load the distributions

                    study.sample_stats = ISATabViewer.spreadsheets.files[study.study_file]["stats"];

                    study.sample_stats = this.process_study_sample_statistics(study.sample_stats);

                }
            }
        }

        var source = $("#study-template").html();
        var template = Handlebars.compile(source);
        var html = template(study);

        $("#study-info").html(html);
    },

    render_assay: function (study_id, file_name) {

        $("#isa-breadcrumb-items").html('
' + study_id + '

' + file_name + '
');

        var spreadsheet = ISATabViewer.spreadsheets.files[file_name];
        var source = $("#table-template").html();
        var template = Handlebars.compile(source);
        var html = template(spreadsheet);

        $("#study-info").html(html);
    },

    /*
     This function adds in information about which images to use for example to show a particular type of assay.
     */
    postprocess_assay_records: function (records) {
        for (var assay_index in records) {
            var assay = records[assay_index];

            var measurement_type = assay["Study Assay Measurement Type"];

            assay.icon = measurement_type.indexOf("metabolite") >= 0 ? "assay-icon-metabolomics"
                : measurement_type.indexOf("prote") >= 0 ? "assay-icon-proteomics"
                : measurement_type.indexOf("transcript") >= 0 ? "assay-icon-transcriptomics"
                : measurement_type.indexOf("chemistry") >= 0 ? "assay-icon-chemistry"
                : measurement_type.indexOf("genom") >= 0 ? "assay-icon-genomics"
                : "";

        }
    },

    generate_records: function (study_information, field_name) {
        var result = [];
        for (var field in study_information[field_name]) {

            var records = study_information[field_name][field];
            for (var i = 0; i < records.length; i++) {
                if (result[i] == undefined) {
                    result[i] = {}
                }
                result[i][field] = ISATabViewer.rendering.replace_str("\"", "", records[i]);
            }
        }

        return result;
    },

    replace_str: function (find, replace, str) {
        return str.replace(new RegExp(find, 'g'), replace);
    },

    render_isatab_from_gist: function (gist_id, placement, options) {

        $.ajax({
            url: 'https://gist.github.com/' + gist_id + '.json',
            dataType: 'jsonp',
            success: function (gist) {
                ISATabViewer.investigation.STUDY = [];
                ISATabViewer.rendering.process_gist(gist, gist_id, placement)
                $("#download-button").html('Download this study.');
            }
        });
    },

    render_isatab_from_file: function (investigation_file, placement, options) {

        $.ajax({
            url: investigation_file,
            success: function (file) {
                ISATabViewer.investigation.STUDY = [];
                ISATabViewer.rendering.process_file(investigation_file, file, placement);
//                $("#download-button").html('Download this study.');
            }
        });
    },

    create_study_template: function () {
        return {
            "STUDY": {},
            "STUDY CONTACTS": {},
            "STUDY PUBLICATIONS": {},
            "STUDY FACTORS": {},
            "STUDY DESIGN DESCRIPTORS": {},
            "STUDY ASSAYS": {},
            "STUDY PROTOCOLS": {}
        };
    }
};<|endoftext|>


<commit_before>/* eslint import/no-unresolved: off*/

const invertedIndex = new InvertedIndex();
const validBook = require('../books.json');
const emptyBook = require('../empty-book.json');
const invalidBook = require('../no-title-books.json');
const noContent = require('../empty.json');
const feedbackBook = require('../feedback.json');

invertedIndex.createIndex('books.json', validBook);
invertedIndex.createIndex('feedback.json', feedbackBook);

// this is test suite
describe('Read book data', () => {
  it('Should return false for empty json file', () => {
    expect(invertedIndex.validateFile(emptyBook).status).toEqual(false);
  });

  it('Should return true for valid json file', () => {
    expect(invertedIndex.validateFile(feedbackBook).status).toEqual(true);
  });

  const report = 'Invalid file content';
  it(`Should return ${report} if json does not contain title and text`, () => {
    expect(invertedIndex.validateFile(invalidBook).msg).toEqual(report);
  });

  const result = 'Invalid File';
  it(`Should return ${result} for empty json file`, () => {
    expect(invertedIndex.validateFile(noContent).msg).toEqual(result);
  });
});

describe('Populate Index', () => {
  it('Should ensure that index is created once the file has been read', () => {
    expect(invertedIndex.getIndex('books.json', validBook)).toBeDefined();
  });

  it('Should maps the string keys to the correct object', () => {
    expect(invertedIndex.getIndex('feedback.json').checkpoint).toEqual([0, 1]);
  });

  it('Should return an object that is an accurate index of the json file',
    () => {
      expect(invertedIndex.getIndex('feedback.json')).toEqual({
        checkpoint: [0, 1],
        one: [0],
        testing: [0],
        getindex: [0],
        function: [0],
        two: [1],
        coming: [1],
        soon: [1]
      });
    });
});

describe('Search Index', () => {
  it('Should return correct index of the search term', () => {
    expect(invertedIndex.searchIndex('alice, a')).toEqual({
      'books.json': {
        alice: [0],
        a: [0, 1]
      },
      'feedback.json': {}
    });
  });

  it('Should return empty object if no result is found',
    () => {
      expect(invertedIndex.searchIndex(
      'along', invertedIndex.getIndex()[0])).toEqual({
        'books.json': {},
        'feedback.json': {}
      });
    });

  it('Should return correct index in an array search terms', () => {
    expect(invertedIndex.searchIndex('checkpoint [one]')).toEqual({
      'books.json': {},
      'feedback.json': {
        checkpoint: [0, 1],
        one: [0]
      }
    });
  });
});
<commit_msg>remove eslint rule config<commit_after>const invertedIndex = new InvertedIndex();
const validBook = require('../books.json');
const emptyBook = require('../empty-book.json');
const invalidBook = require('../no-title-books.json');
const noContent = require('../empty.json');
const feedbackBook = require('../feedback.json');

invertedIndex.createIndex('books.json', validBook);
invertedIndex.createIndex('feedback.json', feedbackBook);

// this is test suite
describe('Read book data', () => {
  it('Should return false for empty json file', () => {
    expect(invertedIndex.validateFile(emptyBook).status).toEqual(false);
  });

  it('Should return true for valid json file', () => {
    expect(invertedIndex.validateFile(feedbackBook).status).toEqual(true);
  });

  const report = 'Invalid file content';
  it(`Should return ${report} if json does not contain title and text`, () => {
    expect(invertedIndex.validateFile(invalidBook).msg).toEqual(report);
  });

  const result = 'Invalid File';
  it(`Should return ${result} for empty json file`, () => {
    expect(invertedIndex.validateFile(noContent).msg).toEqual(result);
  });
});

describe('Populate Index', () => {
  it('Should ensure that index is created once the file has been read', () => {
    expect(invertedIndex.getIndex('books.json', validBook)).toBeDefined();
  });

  it('Should maps the string keys to the correct object', () => {
    expect(invertedIndex.getIndex('feedback.json').checkpoint).toEqual([0, 1]);
  });

  it('Should return an object that is an accurate index of the json file',
    () => {
      expect(invertedIndex.getIndex('feedback.json')).toEqual({
        checkpoint: [0, 1],
        one: [0],
        testing: [0],
        getindex: [0],
        function: [0],
        two: [1],
        coming: [1],
        soon: [1]
      });
    });
});

describe('Search Index', () => {
  it('Should return correct index of the search term', () => {
    expect(invertedIndex.searchIndex('alice, a')).toEqual({
      'books.json': {
        alice: [0],
        a: [0, 1]
      },
      'feedback.json': {}
    });
  });

  it('Should return empty object if no result is found',
    () => {
      expect(invertedIndex.searchIndex(
      'along', invertedIndex.getIndex()[0])).toEqual({
        'books.json': {},
        'feedback.json': {}
      });
    });

  it('Should return correct index in an array search terms', () => {
    expect(invertedIndex.searchIndex('checkpoint [one]')).toEqual({
      'books.json': {},
      'feedback.json': {
        checkpoint: [0, 1],
        one: [0]
      }
    });
  });
});
<|endoftext|>


<commit_before>/* eslint-env browser */

const isFunction = require('lodash/isFunction');
const isNumber = require('lodash/isNumber');
const isObject = require('lodash/isObject');
const isString = require('lodash/isString');
const last = require('lodash/last');

const Message = require('./types/message');
const { deferredToPromise } = require('./deferred_to_promise');

const DATABASE_NAME = 'signal';
// Last version with attachment data stored in database:
const EXPECTED_DATABASE_VERSION = 17;
const MESSAGES_STORE_NAME = 'messages';
const ITEMS_STORE_NAME = 'items';

exports.processNext = async ({
  BackboneMessage,
  BackboneMessageCollection,
  count,
  upgradeMessageSchema,
} = {}) => {
  if (!isFunction(BackboneMessage)) {
    throw new TypeError('"BackboneMessage" (Whisper.Message) constructor is required');
  }

  if (!isFunction(BackboneMessageCollection)) {
    throw new TypeError('"BackboneMessageCollection" (Whisper.MessageCollection)' +
      ' constructor is required');
  }

  if (!isNumber(count)) {
    throw new TypeError('"count" is required');
  }

  if (!isFunction(upgradeMessageSchema)) {
    throw new TypeError('"upgradeMessageSchema" is required');
  }

  const startTime = Date.now();

  const startFetchTime = Date.now();
  const messagesRequiringSchemaUpgrade =
    await _fetchMessagesRequiringSchemaUpgrade({ BackboneMessageCollection, count });
  const fetchDuration = Date.now() - startFetchTime;

  const startUpgradeTime = Date.now();
  const upgradedMessages =
    await Promise.all(messagesRequiringSchemaUpgrade.map(upgradeMessageSchema));
  const upgradeDuration = Date.now() - startUpgradeTime;

  const startSaveTime = Date.now();
  const saveMessage = _saveMessageBackbone({ BackboneMessage });
  await Promise.all(upgradedMessages.map(saveMessage));
  const saveDuration = Date.now() - startSaveTime;

  const totalDuration = Date.now() - startTime;
  const numProcessed = messagesRequiringSchemaUpgrade.length;
  const hasMore = numProcessed > 0;
  return {
    hasMore,
    numProcessed,
    fetchDuration,
    upgradeDuration,
    saveDuration,
    totalDuration,
  };
};

exports.processAll = async ({
  Backbone,
  storage,
  upgradeMessageSchema,
} = {}) => {
  if (!isObject(Backbone)) {
    throw new TypeError('"Backbone" is required');
  }

  if (!isObject(storage)) {
    throw new TypeError('"storage" is required');
  }

  if (!isFunction(upgradeMessageSchema)) {
    throw new TypeError('"upgradeMessageSchema" is required');
  }

  const connection = await openDatabase(DATABASE_NAME, EXPECTED_DATABASE_VERSION);
  const isComplete = await isMigrationComplete(connection);
  console.log('Is attachment migration complete?', isComplete);
  if (isComplete) {
    return;
  }

  const migrationStartTime = Date.now();
  let unprocessedMessages = [];
  do {
    // eslint-disable-next-line no-await-in-loop
    const lastProcessedIndex = (await getLastProcessedIndex(connection)) || null;

    const fetchUnprocessedMessagesStartTime = Date.now();
    unprocessedMessages =
      // eslint-disable-next-line no-await-in-loop
      await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({
        connection,
        count: 10,
        lastIndex: lastProcessedIndex,
      });
    const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime;
    const numUnprocessedMessages = unprocessedMessages.length;

    const upgradeStartTime = Date.now();
    const upgradedMessages =
      // eslint-disable-next-line no-await-in-loop
      await Promise.all(unprocessedMessages.map(upgradeMessageSchema));
    const upgradeDuration = Date.now() - upgradeStartTime;

    const saveMessagesStartTime = Date.now();
    const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readwrite');
    const transactionCompletion = completeTransaction(transaction);
    // eslint-disable-next-line no-await-in-loop
    await Promise.all(upgradedMessages.map(_saveMessage({ transaction })));
    // eslint-disable-next-line no-await-in-loop
    await transactionCompletion;
    const saveDuration = Date.now() - saveMessagesStartTime;

    // TODO: Confirm transaction is complete

    const lastMessage = last(upgradedMessages);
    const newLastProcessedIndex = lastMessage ? lastMessage.id : null;
    if (newLastProcessedIndex) {
      // eslint-disable-next-line no-await-in-loop
      await setLastProcessedIndex(connection, newLastProcessedIndex);
    }

    console.log('Upgrade message schema on startup:', {
      lastProcessedIndex,
      numUnprocessedMessages,
      fetchDuration,
      saveDuration,
      upgradeDuration,
      newLastProcessedIndex,
    });
  } while (unprocessedMessages.length > 0);

  await markMigrationComplete(connection);
  connection.close();

  const totalDuration = Date.now() - migrationStartTime;
  console.log('Attachment migration complete:', { totalDuration });
};

const _saveMessageBackbone = ({ BackboneMessage } = {}) => (message) => {
  const backboneMessage = new BackboneMessage(message);
  return deferredToPromise(backboneMessage.save());
};

const _saveMessage = ({ transaction } = {}) => (message) => {
  if (!isObject(transaction)) {
    throw new TypeError('"transaction" is required');
  }

  const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);
  const request = messagesStore.put(message, message.id);
  return new Promise((resolve, reject) => {
    request.onsuccess = () =>
      resolve();
    request.onerror = event =>
      reject(event.target.error);
  });
};

const _fetchMessagesRequiringSchemaUpgrade =
  async ({ BackboneMessageCollection, count } = {}) => {
    if (!isFunction(BackboneMessageCollection)) {
      throw new TypeError('"BackboneMessageCollection" (Whisper.MessageCollection)' +
        ' constructor is required');
    }

    if (!isNumber(count)) {
      throw new TypeError('"count" is required');
    }

    const collection = new BackboneMessageCollection();
    return new Promise(resolve => collection.fetch({
      limit: count,
      index: {
        name: 'schemaVersion',
        upper: Message.CURRENT_SCHEMA_VERSION,
        excludeUpper: true,
        order: 'desc',
      },
    }).always(() => {
      const models = collection.models || [];
      const messages = models.map(model => model.toJSON());
      resolve(messages);
    }));
  };

const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
  ({ connection, count, lastIndex } = {}) => {
    if (!isObject(connection)) {
      throw new TypeError('"connection" is required');
    }

    if (!isNumber(count)) {
      throw new TypeError('"count" is required');
    }

    if (lastIndex && !isString(lastIndex)) {
      throw new TypeError('"lastIndex" must be a string');
    }

    const hasLastIndex = Boolean(lastIndex);

    const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly');
    const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);

    const excludeLowerBound = true;
    const query = hasLastIndex
      ? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound)
      : undefined;
    const request = messagesStore.getAll(query, count);
    return new Promise((resolve, reject) => {
      request.onsuccess = event =>
        resolve(event.target.result);
      request.onerror = event =>
        reject(event.target.error);
    });
  };

const openDatabase = (name, version) => {
  const request = window.indexedDB.open(name, version);
  return new Promise((resolve, reject) => {
    request.onblocked = () =>
      reject(new Error('Database blocked'));

    request.onupgradeneeded = event =>
      reject(new Error('Unexpected database upgrade required:' +
        `oldVersion: ${event.oldVersion}, newVersion: ${event.newVersion}`));

    request.onerror = event =>
      reject(event.target.error);

    request.onsuccess = (event) => {
      const connection = event.target.result;
      resolve(connection);
    };
  });
};

const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex';
const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete';

const getLastProcessedIndex = connection =>
  getItem(connection, LAST_PROCESSED_INDEX_KEY);

const setLastProcessedIndex = (connection, value) =>
  setItem(connection, LAST_PROCESSED_INDEX_KEY, value);

const isMigrationComplete = async (connection) => {
  const value = await getItem(connection, IS_MIGRATION_COMPLETE_KEY);
  return Boolean(value);
};

const markMigrationComplete = connection =>
  setItem(connection, IS_MIGRATION_COMPLETE_KEY, true);

const getItem = (connection, key) => {
  if (!isObject(connection)) {
    throw new TypeError('"connection" is required');
  }

  if (!isString(key)) {
    throw new TypeError('"key" must be a string');
  }

  const transaction = connection.transaction(ITEMS_STORE_NAME, 'readonly');
  const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
  const request = itemsStore.get(key);
  return new Promise((resolve, reject) => {
    request.onerror = event =>
      reject(event.target.error);

    request.onsuccess = event =>
      resolve(event.target.result);
  });
};

const setItem = (connection, key, value) => {
  if (!isObject(connection)) {
    throw new TypeError('"connection" is required');
  }

  if (!isString(key)) {
    throw new TypeError('"key" must be a string');
  }

  const transaction = connection.transaction(ITEMS_STORE_NAME, 'readwrite');
  const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
  const request = itemsStore.put(value, key);
  return new Promise((resolve, reject) => {
    request.onerror = event =>
      reject(event.target.error);

    request.onsuccess = () =>
      resolve();
  });
};

const completeTransaction = transaction =>
  new Promise((resolve, reject) => {
    // eslint-disable-next-line no-param-reassign
    transaction.onabort = event =>
      reject(event.target.error);
    // eslint-disable-next-line no-param-reassign
    transaction.onerror = event =>
      reject(event.target.error);
    // eslint-disable-next-line no-param-reassign
    transaction.oncomplete = () =>
      resolve();
  });
<commit_msg>Avoid `no-param-reassign` violation<commit_after>/* eslint-env browser */

const isFunction = require('lodash/isFunction');
const isNumber = require('lodash/isNumber');
const isObject = require('lodash/isObject');
const isString = require('lodash/isString');
const last = require('lodash/last');

const Message = require('./types/message');
const { deferredToPromise } = require('./deferred_to_promise');

const DATABASE_NAME = 'signal';
// Last version with attachment data stored in database:
const EXPECTED_DATABASE_VERSION = 17;
const MESSAGES_STORE_NAME = 'messages';
const ITEMS_STORE_NAME = 'items';

exports.processNext = async ({
  BackboneMessage,
  BackboneMessageCollection,
  count,
  upgradeMessageSchema,
} = {}) => {
  if (!isFunction(BackboneMessage)) {
    throw new TypeError('"BackboneMessage" (Whisper.Message) constructor is required');
  }

  if (!isFunction(BackboneMessageCollection)) {
    throw new TypeError('"BackboneMessageCollection" (Whisper.MessageCollection)' +
      ' constructor is required');
  }

  if (!isNumber(count)) {
    throw new TypeError('"count" is required');
  }

  if (!isFunction(upgradeMessageSchema)) {
    throw new TypeError('"upgradeMessageSchema" is required');
  }

  const startTime = Date.now();

  const startFetchTime = Date.now();
  const messagesRequiringSchemaUpgrade =
    await _fetchMessagesRequiringSchemaUpgrade({ BackboneMessageCollection, count });
  const fetchDuration = Date.now() - startFetchTime;

  const startUpgradeTime = Date.now();
  const upgradedMessages =
    await Promise.all(messagesRequiringSchemaUpgrade.map(upgradeMessageSchema));
  const upgradeDuration = Date.now() - startUpgradeTime;

  const startSaveTime = Date.now();
  const saveMessage = _saveMessageBackbone({ BackboneMessage });
  await Promise.all(upgradedMessages.map(saveMessage));
  const saveDuration = Date.now() - startSaveTime;

  const totalDuration = Date.now() - startTime;
  const numProcessed = messagesRequiringSchemaUpgrade.length;
  const hasMore = numProcessed > 0;
  return {
    hasMore,
    numProcessed,
    fetchDuration,
    upgradeDuration,
    saveDuration,
    totalDuration,
  };
};

exports.processAll = async ({
  Backbone,
  storage,
  upgradeMessageSchema,
} = {}) => {
  if (!isObject(Backbone)) {
    throw new TypeError('"Backbone" is required');
  }

  if (!isObject(storage)) {
    throw new TypeError('"storage" is required');
  }

  if (!isFunction(upgradeMessageSchema)) {
    throw new TypeError('"upgradeMessageSchema" is required');
  }

  const connection = await openDatabase(DATABASE_NAME, EXPECTED_DATABASE_VERSION);
  const isComplete = await isMigrationComplete(connection);
  console.log('Is attachment migration complete?', isComplete);
  if (isComplete) {
    return;
  }

  const migrationStartTime = Date.now();
  let unprocessedMessages = [];
  do {
    // eslint-disable-next-line no-await-in-loop
    const lastProcessedIndex = (await getLastProcessedIndex(connection)) || null;

    const fetchUnprocessedMessagesStartTime = Date.now();
    unprocessedMessages =
      // eslint-disable-next-line no-await-in-loop
      await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({
        connection,
        count: 10,
        lastIndex: lastProcessedIndex,
      });
    const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime;
    const numUnprocessedMessages = unprocessedMessages.length;

    const upgradeStartTime = Date.now();
    const upgradedMessages =
      // eslint-disable-next-line no-await-in-loop
      await Promise.all(unprocessedMessages.map(upgradeMessageSchema));
    const upgradeDuration = Date.now() - upgradeStartTime;

    const saveMessagesStartTime = Date.now();
    const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readwrite');
    const transactionCompletion = completeTransaction(transaction);
    // eslint-disable-next-line no-await-in-loop
    await Promise.all(upgradedMessages.map(_saveMessage({ transaction })));
    // eslint-disable-next-line no-await-in-loop
    await transactionCompletion;
    const saveDuration = Date.now() - saveMessagesStartTime;

    // TODO: Confirm transaction is complete

    const lastMessage = last(upgradedMessages);
    const newLastProcessedIndex = lastMessage ? lastMessage.id : null;
    if (newLastProcessedIndex) {
      // eslint-disable-next-line no-await-in-loop
      await setLastProcessedIndex(connection, newLastProcessedIndex);
    }

    console.log('Upgrade message schema on startup:', {
      lastProcessedIndex,
      numUnprocessedMessages,
      fetchDuration,
      saveDuration,
      upgradeDuration,
      newLastProcessedIndex,
    });
  } while (unprocessedMessages.length > 0);

  await markMigrationComplete(connection);
  connection.close();

  const totalDuration = Date.now() - migrationStartTime;
  console.log('Attachment migration complete:', { totalDuration });
};

const _saveMessageBackbone = ({ BackboneMessage } = {}) => (message) => {
  const backboneMessage = new BackboneMessage(message);
  return deferredToPromise(backboneMessage.save());
};

const _saveMessage = ({ transaction } = {}) => (message) => {
  if (!isObject(transaction)) {
    throw new TypeError('"transaction" is required');
  }

  const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);
  const request = messagesStore.put(message, message.id);
  return new Promise((resolve, reject) => {
    request.onsuccess = () =>
      resolve();
    request.onerror = event =>
      reject(event.target.error);
  });
};

const _fetchMessagesRequiringSchemaUpgrade =
  async ({ BackboneMessageCollection, count } = {}) => {
    if (!isFunction(BackboneMessageCollection)) {
      throw new TypeError('"BackboneMessageCollection" (Whisper.MessageCollection)' +
        ' constructor is required');
    }

    if (!isNumber(count)) {
      throw new TypeError('"count" is required');
    }

    const collection = new BackboneMessageCollection();
    return new Promise(resolve => collection.fetch({
      limit: count,
      index: {
        name: 'schemaVersion',
        upper: Message.CURRENT_SCHEMA_VERSION,
        excludeUpper: true,
        order: 'desc',
      },
    }).always(() => {
      const models = collection.models || [];
      const messages = models.map(model => model.toJSON());
      resolve(messages);
    }));
  };

const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
  ({ connection, count, lastIndex } = {}) => {
    if (!isObject(connection)) {
      throw new TypeError('"connection" is required');
    }

    if (!isNumber(count)) {
      throw new TypeError('"count" is required');
    }

    if (lastIndex && !isString(lastIndex)) {
      throw new TypeError('"lastIndex" must be a string');
    }

    const hasLastIndex = Boolean(lastIndex);

    const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly');
    const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);

    const excludeLowerBound = true;
    const query = hasLastIndex
      ? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound)
      : undefined;
    const request = messagesStore.getAll(query, count);
    return new Promise((resolve, reject) => {
      request.onsuccess = event =>
        resolve(event.target.result);
      request.onerror = event =>
        reject(event.target.error);
    });
  };

const openDatabase = (name, version) => {
  const request = window.indexedDB.open(name, version);
  return new Promise((resolve, reject) => {
    request.onblocked = () =>
      reject(new Error('Database blocked'));

    request.onupgradeneeded = event =>
      reject(new Error('Unexpected database upgrade required:' +
        `oldVersion: ${event.oldVersion}, newVersion: ${event.newVersion}`));

    request.onerror = event =>
      reject(event.target.error);

    request.onsuccess = (event) => {
      const connection = event.target.result;
      resolve(connection);
    };
  });
};

const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex';
const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete';

const getLastProcessedIndex = connection =>
  getItem(connection, LAST_PROCESSED_INDEX_KEY);

const setLastProcessedIndex = (connection, value) =>
  setItem(connection, LAST_PROCESSED_INDEX_KEY, value);

const isMigrationComplete = async (connection) => {
  const value = await getItem(connection, IS_MIGRATION_COMPLETE_KEY);
  return Boolean(value);
};

const markMigrationComplete = connection =>
  setItem(connection, IS_MIGRATION_COMPLETE_KEY, true);

const getItem = (connection, key) => {
  if (!isObject(connection)) {
    throw new TypeError('"connection" is required');
  }

  if (!isString(key)) {
    throw new TypeError('"key" must be a string');
  }

  const transaction = connection.transaction(ITEMS_STORE_NAME, 'readonly');
  const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
  const request = itemsStore.get(key);
  return new Promise((resolve, reject) => {
    request.onerror = event =>
      reject(event.target.error);

    request.onsuccess = event =>
      resolve(event.target.result);
  });
};

const setItem = (connection, key, value) => {
  if (!isObject(connection)) {
    throw new TypeError('"connection" is required');
  }

  if (!isString(key)) {
    throw new TypeError('"key" must be a string');
  }

  const transaction = connection.transaction(ITEMS_STORE_NAME, 'readwrite');
  const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
  const request = itemsStore.put(value, key);
  return new Promise((resolve, reject) => {
    request.onerror = event =>
      reject(event.target.error);

    request.onsuccess = () =>
      resolve();
  });
};

const completeTransaction = transaction =>
  new Promise((resolve, reject) => {
    transaction.addEventListener('abort', event => reject(event.target.error));
    transaction.addEventListener('error', event => reject(event.target.error));
    transaction.addEventListener('complete', () => resolve());
  });
<|endoftext|>