<commit_before>'use strict';

import $ from 'jquery';
import { Plugin } from './foundation.core.plugin';
import { onLoad } from './foundation.core.utils';
import { Keyboard } from './foundation.util.keyboard';
import { onImagesLoaded } from './foundation.util.imageLoader';
/**
 * Tabs module.
 * @module foundation.tabs
 * @requires foundation.util.keyboard
 * @requires foundation.util.imageLoader if tabs contain images
 */

class Tabs extends Plugin {
  /**
   * Creates a new instance of tabs.
   * @class
   * @name Tabs
   * @fires Tabs#init
   * @param {jQuery} element - jQuery object to make into tabs.
   * @param {Object} options - Overrides to the default plugin settings.
   */
  _setup(element, options) {
    this.$element = element;
    this.options = $.extend({}, Tabs.defaults, this.$element.data(), options);
    this.className = 'Tabs'; // ie9 back compat

    this._init();
    Keyboard.register('Tabs', {
      'ENTER': 'open',
      'SPACE': 'open',
      'ARROW_RIGHT': 'next',
      'ARROW_UP': 'previous',
      'ARROW_DOWN': 'next',
      'ARROW_LEFT': 'previous'
      // 'TAB': 'next',
      // 'SHIFT_TAB': 'previous'
    });
  }

  /**
   * Initializes the tabs by showing and focusing (if autoFocus=true) the preset active tab.
   * @private
   */
  _init() {
    var _this = this;
    this._isInitializing = true;

    this.$element.attr({'role': 'tablist'});
    this.$tabTitles = this.$element.find(`.${this.options.linkClass}`);
    this.$tabContent = $(`[data-tabs-content="${this.$element[0].id}"]`);

    this.$tabTitles.each(function(){
      var $elem = $(this),
          $link = $elem.find('a'),
          isActive = $elem.hasClass(`${_this.options.linkActiveClass}`),
          hash = $link.attr('data-tabs-target') || $link[0].hash.slice(1),
          linkId = $link[0].id ? $link[0].id : `${hash}-label`,
          $tabContent = $(`#${hash}`);

      $elem.attr({'role': 'presentation'});

      $link.attr({
        'role': 'tab',
        'aria-controls': hash,
        'aria-selected': isActive,
        'id': linkId,
        'tabindex': isActive ? '0' : '-1'
      });

      $tabContent.attr({
        'role': 'tabpanel',
        'aria-labelledby': linkId
      });

      // Save up the initial hash to return to it later when going back in history
      if (isActive) {
        _this._initialAnchor = `#${hash}`;
      }

      if(!isActive) {
        $tabContent.attr('aria-hidden', 'true');
      }

      if(isActive && _this.options.autoFocus){
        _this.onLoadListener = onLoad($(window), function() {
          $('html, body').animate({ scrollTop: $elem.offset().top }, _this.options.deepLinkSmudgeDelay, () => {
            $link.focus();
          });
        });
      }
    });

    if(this.options.matchHeight) {
      var $images = this.$tabContent.find('img');

      if ($images.length) {
        onImagesLoaded($images, this._setHeight.bind(this));
      } else {
        this._setHeight();
      }
    }

     // Current context-bound function to open tabs on page load or history hashchange
    this._checkDeepLink = () => {
      var anchor = window.location.hash;

      if (!anchor.length) {
        // If we are still initializing and there is no anchor, then there is nothing to do
        if (this._isInitializing) return;
        // Otherwise, move to the initial anchor
        if (this._initialAnchor) anchor = this._initialAnchor;
      }

      var anchorNoHash = anchor.indexOf('#') >= 0 ? anchor.slice(1) : anchor;
      var $anchor = anchorNoHash && $(`#${anchorNoHash}`);
      var $link = anchor && this.$element.find(`[href$="${anchor}"],[data-tabs-target="${anchorNoHash}"]`).first();
      // Whether the anchor element that has been found is part of this element
      var isOwnAnchor = !!($anchor.length && $link.length);

      // If there is an anchor for the hash, select it
      if ($anchor && $anchor.length && $link && $link.length) {
        this.selectTab($anchor, true);
      }
      // Otherwise, collapse everything
      else {
        this._collapse();
      }

      if (isOwnAnchor) {
        // Roll up a little to show the titles
        if (this.options.deepLinkSmudge) {
          var offset = this.$element.offset();
          $('html, body').animate({ scrollTop: offset.top }, this.options.deepLinkSmudgeDelay);
        }

        /**
         * Fires when the plugin has deeplinked at pageload
         * @event Tabs#deeplink
         */
        this.$element.trigger('deeplink.zf.tabs', [$link, $anchor]);
      }
    }

    //use browser to open a tab, if it exists in this tabset
    if (this.options.deepLink) {
      this._checkDeepLink();
    }

    this._events();

    this._isInitializing = false;
  }

  /**
   * Adds event handlers for items within the tabs.
   * @private
   */
  _events() {
    this._addKeyHandler();
    this._addClickHandler();
    this._setHeightMqHandler = null;

    if (this.options.matchHeight) {
      this._setHeightMqHandler = this._setHeight.bind(this);

      $(window).on('changed.zf.mediaquery', this._setHeightMqHandler);
    }

    if(this.options.deepLink) {
      $(window).on('hashchange', this._checkDeepLink);
    }
  }

  /**
   * Adds click handlers for items within the tabs.
   * @private
   */
  _addClickHandler() {
    var _this = this;

    this.$element
      .off('click.zf.tabs')
      .on('click.zf.tabs', `.${this.options.linkClass}`, function(e){
        e.preventDefault();
        _this._handleTabChange($(this));
      });
  }

  /**
   * Adds keyboard event handlers for items within the tabs.
   * @private
   */
  _addKeyHandler() {
    var _this = this;

    this.$tabTitles.off('keydown.zf.tabs').on('keydown.zf.tabs', function(e){
      if (e.which === 9) return;

      var $element = $(this),
        $elements = $element.parent('ul').children('li'),
        $prevElement,
        $nextElement;

      $elements.each(function(i) {
        if ($(this).is($element)) {
          if (_this.options.wrapOnKeys) {
            $prevElement = i === 0 ? $elements.last() : $elements.eq(i-1);
            $nextElement = i === $elements.length -1 ? $elements.first() : $elements.eq(i+1);
          } else {
            $prevElement = $elements.eq(Math.max(0, i-1));
            $nextElement = $elements.eq(Math.min(i+1, $elements.length-1));
          }
          return;
        }
      });

      // handle keyboard event with keyboard util
      Keyboard.handleKey(e, 'Tabs', {
        open: function() {
          $element.find('[role="tab"]').focus();
          _this._handleTabChange($element);
        },
        previous: function() {
          $prevElement.find('[role="tab"]').focus();
          _this._handleTabChange($prevElement);
        },
        next: function() {
          $nextElement.find('[role="tab"]').focus();
          _this._handleTabChange($nextElement);
        },
        handled: function() {
          e.preventDefault();
        }
      });
    });
  }

  /**
   * Opens the tab `$targetContent` defined by `$target`. Collapses active tab.
   * @param {jQuery} $target - Tab to open.
   * @param {boolean} historyHandled - browser has already handled a history update
   * @fires Tabs#change
   * @function
   */
  _handleTabChange($target, historyHandled) {

    // With `activeCollapse`, if the target is the active Tab, collapse it.
    if ($target.hasClass(`${this.options.linkActiveClass}`)) {
        if(this.options.activeCollapse) {
            this._collapse();
        }
        return;
    }

    var $oldTab = this.$element.
          find(`.${this.options.linkClass}.${this.options.linkActiveClass}`),
          $tabLink = $target.find('[role="tab"]'),
          hash = $tabLink.attr('data-tabs-target') || $tabLink[0].hash.slice(1),
          $targetContent = this.$tabContent.find(`#${hash}`);

    //close old tab
    this._collapseTab($oldTab);

    //open new tab
    this._openTab($target);

    //either replace or update browser history
    if (this.options.deepLink && !historyHandled) {
      if (this.options.updateHistory) {
        history.pushState({}, '', hash);
      } else {
        history.replaceState({}, '', hash);
      }
    }

    /**
     * Fires when the plugin has successfully changed tabs.
     * @event Tabs#change
     */
    this.$element.trigger('change.zf.tabs', [$target, $targetContent]);

    //fire to children a mutation event
    $targetContent.find("[data-mutate]").trigger("mutateme.zf.trigger");
  }

  /**
   * Opens the tab `$targetContent` defined by `$target`.
   * @param {jQuery} $target - Tab to open.
   * @function
   */
  _openTab($target) {
      var $tabLink = $target.find('[role="tab"]'),
          hash = $tabLink.attr('data-tabs-target') || $tabLink[0].hash.slice(1),
          $targetContent = this.$tabContent.find(`#${hash}`);

      $target.addClass(`${this.options.linkActiveClass}`);

      $tabLink.attr({
        'aria-selected': 'true',
        'tabindex': '0'
      });

      $targetContent
        .addClass(`${this.options.panelActiveClass}`).removeAttr('aria-hidden');
  }

  /**
   * Collapses `$targetContent` defined by `$target`.
   * @param {jQuery} $target - Tab to collapse.
   * @function
   */
  _collapseTab($target) {
    var $target_anchor = $target
      .removeClass(`${this.options.linkActiveClass}`)
      .find('[role="tab"]')
      .attr({
        'aria-selected': 'false',
        'tabindex': -1
      });

    $(`#${$target_anchor.attr('aria-controls')}`)
      .removeClass(`${this.options.panelActiveClass}`)
      .attr({ 'aria-hidden': 'true' })
  }

  /**
   * Collapses the active Tab.
   * @fires Tabs#collapse
   * @function
   */
  _collapse() {
    var $activeTab = this.$element.find(`.${this.options.linkClass}.${this.options.linkActiveClass}`);

    if ($activeTab.length) {
      this._collapseTab($activeTab);

      /**
      * Fires when the plugin has successfully collapsed tabs.
      * @event Tabs#collapse
      */
      this.$element.trigger('collapse.zf.tabs', [$activeTab]);
    }
  }

  /**
   * Public method for selecting a content pane to display.
   * @param {jQuery | String} elem - jQuery object or string of the id of the pane to display.
   * @param {boolean} historyHandled - browser has already handled a history update
   * @function
   */
  selectTab(elem, historyHandled) {
    var idStr, hashIdStr;

    if (typeof elem === 'object') {
      idStr = elem[0].id;
    } else {
      idStr = elem;
    }

    if (idStr.indexOf('#') < 0) {
      hashIdStr = `#${idStr}`;
    } else {
      hashIdStr = idStr;
      idStr = idStr.slice(1);
    }

    var $target = this.$tabTitles.has(`[href$="${hashIdStr}"],[data-tabs-target="${idStr}"]`).first();

    this._handleTabChange($target, historyHandled);
  };

  /**
   * Sets the height of each panel to the height of the tallest panel.
   * If enabled in options, gets called on media query change.
   * If loading content via external source, can be called directly or with _reflow.
   * If enabled with `data-match-height="true"`, tabs sets to equal height
   * @function
   * @private
   */
  _setHeight() {
    var max = 0,
        _this = this; // Lock down the `this` value for the root tabs object

    this.$tabContent
      .find(`.${this.options.panelClass}`)
      .css('height', '')
      .each(function() {

        var panel = $(this),
            isActive = panel.hasClass(`${_this.options.panelActiveClass}`); // get the options from the parent instead of trying to get them from the child

        if (!isActive) {
          panel.css({'visibility': 'hidden', 'display': 'block'});
        }

        var temp = this.getBoundingClientRect().height;

        if (!isActive) {
          panel.css({
            'visibility': '',
            'display': ''
          });
        }

        max = temp > max ? temp : max;
      })
      .css('height', `${max}px`);
  }

  /**
   * Destroys an instance of tabs.
   * @fires Tabs#destroyed
   */
  _destroy() {
    this.$element
      .find(`.${this.options.linkClass}`)
      .off('.zf.tabs').hide().end()
      .find(`.${this.options.panelClass}`)
      .hide();

    if (this.options.matchHeight) {
      if (this._setHeightMqHandler != null) {
         $(window).off('changed.zf.mediaquery', this._setHeightMqHandler);
      }
    }

    if (this.options.deepLink) {
      $(window).off('hashchange', this._checkDeepLink);
    }

    if (this.onLoadListener) {
      $(window).off(this.onLoadListener);
    }
  }
}

Tabs.defaults = {
  /**
   * Link the location hash to the active pane.
   * Set the location hash when the active pane changes, and open the corresponding pane when the location changes.
   * @option
   * @type {boolean}
   * @default false
   */
  deepLink: false,

  /**
   * If `deepLink` is enabled, adjust the deep link scroll to make sure the top of the tab panel is visible
   * @option
   * @type {boolean}
   * @default false
   */
  deepLinkSmudge: false,

  /**
   * If `deepLinkSmudge` is enabled, animation time (ms) for the deep link adjustment
   * @option
   * @type {number}
   * @default 300
   */
  deepLinkSmudgeDelay: 300,

  /**
   * If `deepLink` is enabled, update the browser history with the open tab
   * @option
   * @type {boolean}
   * @default false
   */
  updateHistory: false,

  /**
   * Allows the window to scroll to content of active pane on load.
   * Not recommended if more than one tab panel per page.
   * @option
   * @type {boolean}
   * @default false
   */
  autoFocus: false,

  /**
   * Allows keyboard input to 'wrap' around the tab links.
   * @option
   * @type {boolean}
   * @default true
   */
  wrapOnKeys: true,

  /**
   * Allows the tab content panes to match heights if set to true.
   * @option
   * @type {boolean}
   * @default false
   */
  matchHeight: false,

  /**
   * Allows active tabs to collapse when clicked.
   * @option
   * @type {boolean}
   * @default false
   */
  activeCollapse: false,

  /**
   * Class applied to `li`'s in tab link list.
   * @option
   * @type {string}
   * @default 'tabs-title'
   */
  linkClass: 'tabs-title',

  /**
   * Class applied to the active `li` in tab link list.
   * @option
   * @type {string}
   * @default 'is-active'
   */
  linkActiveClass: 'is-active',

  /**
   * Class applied to the content containers.
   * @option
   * @type {string}
   * @default 'tabs-panel'
   */
  panelClass: 'tabs-panel',

  /**
   * Class applied to the active content container.
   * @option
   * @type {string}
   * @default 'is-active'
   */
  panelActiveClass: 'is-active'
};

export {Tabs};
<commit_msg>fix: ensure Tab deep-linking to redirect to hash with "#"<commit_after>'use strict';

import $ from 'jquery';
import { Plugin } from './foundation.core.plugin';
import { onLoad } from './foundation.core.utils';
import { Keyboard } from './foundation.util.keyboard';
import { onImagesLoaded } from './foundation.util.imageLoader';
/**
 * Tabs module.
 * @module foundation.tabs
 * @requires foundation.util.keyboard
 * @requires foundation.util.imageLoader if tabs contain images
 */

class Tabs extends Plugin {
  /**
   * Creates a new instance of tabs.
   * @class
   * @name Tabs
   * @fires Tabs#init
   * @param {jQuery} element - jQuery object to make into tabs.
   * @param {Object} options - Overrides to the default plugin settings.
   */
  _setup(element, options) {
    this.$element = element;
    this.options = $.extend({}, Tabs.defaults, this.$element.data(), options);
    this.className = 'Tabs'; // ie9 back compat

    this._init();
    Keyboard.register('Tabs', {
      'ENTER': 'open',
      'SPACE': 'open',
      'ARROW_RIGHT': 'next',
      'ARROW_UP': 'previous',
      'ARROW_DOWN': 'next',
      'ARROW_LEFT': 'previous'
      // 'TAB': 'next',
      // 'SHIFT_TAB': 'previous'
    });
  }

  /**
   * Initializes the tabs by showing and focusing (if autoFocus=true) the preset active tab.
   * @private
   */
  _init() {
    var _this = this;
    this._isInitializing = true;

    this.$element.attr({'role': 'tablist'});
    this.$tabTitles = this.$element.find(`.${this.options.linkClass}`);
    this.$tabContent = $(`[data-tabs-content="${this.$element[0].id}"]`);

    this.$tabTitles.each(function(){
      var $elem = $(this),
          $link = $elem.find('a'),
          isActive = $elem.hasClass(`${_this.options.linkActiveClass}`),
          hash = $link.attr('data-tabs-target') || $link[0].hash.slice(1),
          linkId = $link[0].id ? $link[0].id : `${hash}-label`,
          $tabContent = $(`#${hash}`);

      $elem.attr({'role': 'presentation'});

      $link.attr({
        'role': 'tab',
        'aria-controls': hash,
        'aria-selected': isActive,
        'id': linkId,
        'tabindex': isActive ? '0' : '-1'
      });

      $tabContent.attr({
        'role': 'tabpanel',
        'aria-labelledby': linkId
      });

      // Save up the initial hash to return to it later when going back in history
      if (isActive) {
        _this._initialAnchor = `#${hash}`;
      }

      if(!isActive) {
        $tabContent.attr('aria-hidden', 'true');
      }

      if(isActive && _this.options.autoFocus){
        _this.onLoadListener = onLoad($(window), function() {
          $('html, body').animate({ scrollTop: $elem.offset().top }, _this.options.deepLinkSmudgeDelay, () => {
            $link.focus();
          });
        });
      }
    });

    if(this.options.matchHeight) {
      var $images = this.$tabContent.find('img');

      if ($images.length) {
        onImagesLoaded($images, this._setHeight.bind(this));
      } else {
        this._setHeight();
      }
    }

     // Current context-bound function to open tabs on page load or history hashchange
    this._checkDeepLink = () => {
      var anchor = window.location.hash;

      if (!anchor.length) {
        // If we are still initializing and there is no anchor, then there is nothing to do
        if (this._isInitializing) return;
        // Otherwise, move to the initial anchor
        if (this._initialAnchor) anchor = this._initialAnchor;
      }

      var anchorNoHash = anchor.indexOf('#') >= 0 ? anchor.slice(1) : anchor;
      var $anchor = anchorNoHash && $(`#${anchorNoHash}`);
      var $link = anchor && this.$element.find(`[href$="${anchor}"],[data-tabs-target="${anchorNoHash}"]`).first();
      // Whether the anchor element that has been found is part of this element
      var isOwnAnchor = !!($anchor.length && $link.length);

      // If there is an anchor for the hash, select it
      if ($anchor && $anchor.length && $link && $link.length) {
        this.selectTab($anchor, true);
      }
      // Otherwise, collapse everything
      else {
        this._collapse();
      }

      if (isOwnAnchor) {
        // Roll up a little to show the titles
        if (this.options.deepLinkSmudge) {
          var offset = this.$element.offset();
          $('html, body').animate({ scrollTop: offset.top }, this.options.deepLinkSmudgeDelay);
        }

        /**
         * Fires when the plugin has deeplinked at pageload
         * @event Tabs#deeplink
         */
        this.$element.trigger('deeplink.zf.tabs', [$link, $anchor]);
      }
    }

    //use browser to open a tab, if it exists in this tabset
    if (this.options.deepLink) {
      this._checkDeepLink();
    }

    this._events();

    this._isInitializing = false;
  }

  /**
   * Adds event handlers for items within the tabs.
   * @private
   */
  _events() {
    this._addKeyHandler();
    this._addClickHandler();
    this._setHeightMqHandler = null;

    if (this.options.matchHeight) {
      this._setHeightMqHandler = this._setHeight.bind(this);

      $(window).on('changed.zf.mediaquery', this._setHeightMqHandler);
    }

    if(this.options.deepLink) {
      $(window).on('hashchange', this._checkDeepLink);
    }
  }

  /**
   * Adds click handlers for items within the tabs.
   * @private
   */
  _addClickHandler() {
    var _this = this;

    this.$element
      .off('click.zf.tabs')
      .on('click.zf.tabs', `.${this.options.linkClass}`, function(e){
        e.preventDefault();
        _this._handleTabChange($(this));
      });
  }

  /**
   * Adds keyboard event handlers for items within the tabs.
   * @private
   */
  _addKeyHandler() {
    var _this = this;

    this.$tabTitles.off('keydown.zf.tabs').on('keydown.zf.tabs', function(e){
      if (e.which === 9) return;

      var $element = $(this),
        $elements = $element.parent('ul').children('li'),
        $prevElement,
        $nextElement;

      $elements.each(function(i) {
        if ($(this).is($element)) {
          if (_this.options.wrapOnKeys) {
            $prevElement = i === 0 ? $elements.last() : $elements.eq(i-1);
            $nextElement = i === $elements.length -1 ? $elements.first() : $elements.eq(i+1);
          } else {
            $prevElement = $elements.eq(Math.max(0, i-1));
            $nextElement = $elements.eq(Math.min(i+1, $elements.length-1));
          }
          return;
        }
      });

      // handle keyboard event with keyboard util
      Keyboard.handleKey(e, 'Tabs', {
        open: function() {
          $element.find('[role="tab"]').focus();
          _this._handleTabChange($element);
        },
        previous: function() {
          $prevElement.find('[role="tab"]').focus();
          _this._handleTabChange($prevElement);
        },
        next: function() {
          $nextElement.find('[role="tab"]').focus();
          _this._handleTabChange($nextElement);
        },
        handled: function() {
          e.preventDefault();
        }
      });
    });
  }

  /**
   * Opens the tab `$targetContent` defined by `$target`. Collapses active tab.
   * @param {jQuery} $target - Tab to open.
   * @param {boolean} historyHandled - browser has already handled a history update
   * @fires Tabs#change
   * @function
   */
  _handleTabChange($target, historyHandled) {

    // With `activeCollapse`, if the target is the active Tab, collapse it.
    if ($target.hasClass(`${this.options.linkActiveClass}`)) {
        if(this.options.activeCollapse) {
            this._collapse();
        }
        return;
    }

    var $oldTab = this.$element.
          find(`.${this.options.linkClass}.${this.options.linkActiveClass}`),
          $tabLink = $target.find('[role="tab"]'),
          target = $tabLink.attr('data-tabs-target'),
          anchor = target && target.length ? `#${target}` : $tabLink[0].hash,
          $targetContent = this.$tabContent.find(anchor);

    //close old tab
    this._collapseTab($oldTab);

    //open new tab
    this._openTab($target);

    //either replace or update browser history
    if (this.options.deepLink && !historyHandled) {
      if (this.options.updateHistory) {
        history.pushState({}, '', anchor);
      } else {
        history.replaceState({}, '', anchor);
      }
    }

    /**
     * Fires when the plugin has successfully changed tabs.
     * @event Tabs#change
     */
    this.$element.trigger('change.zf.tabs', [$target, $targetContent]);

    //fire to children a mutation event
    $targetContent.find("[data-mutate]").trigger("mutateme.zf.trigger");
  }

  /**
   * Opens the tab `$targetContent` defined by `$target`.
   * @param {jQuery} $target - Tab to open.
   * @function
   */
  _openTab($target) {
      var $tabLink = $target.find('[role="tab"]'),
          hash = $tabLink.attr('data-tabs-target') || $tabLink[0].hash.slice(1),
          $targetContent = this.$tabContent.find(`#${hash}`);

      $target.addClass(`${this.options.linkActiveClass}`);

      $tabLink.attr({
        'aria-selected': 'true',
        'tabindex': '0'
      });

      $targetContent
        .addClass(`${this.options.panelActiveClass}`).removeAttr('aria-hidden');
  }

  /**
   * Collapses `$targetContent` defined by `$target`.
   * @param {jQuery} $target - Tab to collapse.
   * @function
   */
  _collapseTab($target) {
    var $target_anchor = $target
      .removeClass(`${this.options.linkActiveClass}`)
      .find('[role="tab"]')
      .attr({
        'aria-selected': 'false',
        'tabindex': -1
      });

    $(`#${$target_anchor.attr('aria-controls')}`)
      .removeClass(`${this.options.panelActiveClass}`)
      .attr({ 'aria-hidden': 'true' })
  }

  /**
   * Collapses the active Tab.
   * @fires Tabs#collapse
   * @function
   */
  _collapse() {
    var $activeTab = this.$element.find(`.${this.options.linkClass}.${this.options.linkActiveClass}`);

    if ($activeTab.length) {
      this._collapseTab($activeTab);

      /**
      * Fires when the plugin has successfully collapsed tabs.
      * @event Tabs#collapse
      */
      this.$element.trigger('collapse.zf.tabs', [$activeTab]);
    }
  }

  /**
   * Public method for selecting a content pane to display.
   * @param {jQuery | String} elem - jQuery object or string of the id of the pane to display.
   * @param {boolean} historyHandled - browser has already handled a history update
   * @function
   */
  selectTab(elem, historyHandled) {
    var idStr, hashIdStr;

    if (typeof elem === 'object') {
      idStr = elem[0].id;
    } else {
      idStr = elem;
    }

    if (idStr.indexOf('#') < 0) {
      hashIdStr = `#${idStr}`;
    } else {
      hashIdStr = idStr;
      idStr = idStr.slice(1);
    }

    var $target = this.$tabTitles.has(`[href$="${hashIdStr}"],[data-tabs-target="${idStr}"]`).first();

    this._handleTabChange($target, historyHandled);
  };

  /**
   * Sets the height of each panel to the height of the tallest panel.
   * If enabled in options, gets called on media query change.
   * If loading content via external source, can be called directly or with _reflow.
   * If enabled with `data-match-height="true"`, tabs sets to equal height
   * @function
   * @private
   */
  _setHeight() {
    var max = 0,
        _this = this; // Lock down the `this` value for the root tabs object

    this.$tabContent
      .find(`.${this.options.panelClass}`)
      .css('height', '')
      .each(function() {

        var panel = $(this),
            isActive = panel.hasClass(`${_this.options.panelActiveClass}`); // get the options from the parent instead of trying to get them from the child

        if (!isActive) {
          panel.css({'visibility': 'hidden', 'display': 'block'});
        }

        var temp = this.getBoundingClientRect().height;

        if (!isActive) {
          panel.css({
            'visibility': '',
            'display': ''
          });
        }

        max = temp > max ? temp : max;
      })
      .css('height', `${max}px`);
  }

  /**
   * Destroys an instance of tabs.
   * @fires Tabs#destroyed
   */
  _destroy() {
    this.$element
      .find(`.${this.options.linkClass}`)
      .off('.zf.tabs').hide().end()
      .find(`.${this.options.panelClass}`)
      .hide();

    if (this.options.matchHeight) {
      if (this._setHeightMqHandler != null) {
         $(window).off('changed.zf.mediaquery', this._setHeightMqHandler);
      }
    }

    if (this.options.deepLink) {
      $(window).off('hashchange', this._checkDeepLink);
    }

    if (this.onLoadListener) {
      $(window).off(this.onLoadListener);
    }
  }
}

Tabs.defaults = {
  /**
   * Link the location hash to the active pane.
   * Set the location hash when the active pane changes, and open the corresponding pane when the location changes.
   * @option
   * @type {boolean}
   * @default false
   */
  deepLink: false,

  /**
   * If `deepLink` is enabled, adjust the deep link scroll to make sure the top of the tab panel is visible
   * @option
   * @type {boolean}
   * @default false
   */
  deepLinkSmudge: false,

  /**
   * If `deepLinkSmudge` is enabled, animation time (ms) for the deep link adjustment
   * @option
   * @type {number}
   * @default 300
   */
  deepLinkSmudgeDelay: 300,

  /**
   * If `deepLink` is enabled, update the browser history with the open tab
   * @option
   * @type {boolean}
   * @default false
   */
  updateHistory: false,

  /**
   * Allows the window to scroll to content of active pane on load.
   * Not recommended if more than one tab panel per page.
   * @option
   * @type {boolean}
   * @default false
   */
  autoFocus: false,

  /**
   * Allows keyboard input to 'wrap' around the tab links.
   * @option
   * @type {boolean}
   * @default true
   */
  wrapOnKeys: true,

  /**
   * Allows the tab content panes to match heights if set to true.
   * @option
   * @type {boolean}
   * @default false
   */
  matchHeight: false,

  /**
   * Allows active tabs to collapse when clicked.
   * @option
   * @type {boolean}
   * @default false
   */
  activeCollapse: false,

  /**
   * Class applied to `li`'s in tab link list.
   * @option
   * @type {string}
   * @default 'tabs-title'
   */
  linkClass: 'tabs-title',

  /**
   * Class applied to the active `li` in tab link list.
   * @option
   * @type {string}
   * @default 'is-active'
   */
  linkActiveClass: 'is-active',

  /**
   * Class applied to the content containers.
   * @option
   * @type {string}
   * @default 'tabs-panel'
   */
  panelClass: 'tabs-panel',

  /**
   * Class applied to the active content container.
   * @option
   * @type {string}
   * @default 'is-active'
   */
  panelActiveClass: 'is-active'
};

export {Tabs};
<|endoftext|>


<commit_before>const pixelRatio = window.devicePixelRatio || 1

function removeClass(classToRemove) {
  nodes = document.getElementsByClassName(classToRemove)
  if (!nodes[0]) return

  nodes[0].classList.remove(classToRemove)
  if (nodes[0]) removeClass(classToRemove)
}

function formatCSSCode(unicode) {
  unicode = unicode.toString(16)

  if (unicode.length > 4) {
    return '\' + ("000000" + unicode.toUpperCase()).substr(-6)
  }
  else {
    return 'content: \'\' + ("0000" + unicode.toUpperCase()).substr(-4) + '\';'
  }
}

function formatHTMLCode(unicode) {
  return '&#' + unicode + ';'
}

function enableHighDPICanvas(canvas) {
  if (pixelRatio === 1) return

  const oldWidth = canvas.width
  const oldHeight = canvas.height
  canvas.width = oldWidth * pixelRatio
  canvas.height = oldHeight * pixelRatio
  canvas.style.width = oldWidth + 'px'
  canvas.style.height = oldHeight + 'px'
  canvas.getContext('2d').scale(pixelRatio, pixelRatio)
}

function highlightSelectedGlyph(glyphIndex) {
  removeClass('active')
  document.getElementById('g' + glyphIndex).parentNode.classList.add('active')
}

function displaySelectedGlyphInfo(glyph) {
  const glyphInfoContainer = document.getElementById('glyph-info')

  if (glyph.unicodes.length == 0) {
    glyphInfoContainer.innerHTML = `

Glyph Name

${glyph.name}

HTML Code

Undefined

CSS Code

Undefined

    `
  }
  else {
    glyphInfoContainer.innerHTML = `

Glyph Name

${glyph.name}

HTML Code

${glyph.unicodes.map(formatHTMLCode).join(', ')}

CSS Code

${glyph.unicodes.map(formatCSSCode).join(', ')}

    `
  }
}

function displaySelectedGlyph(font, glyph) {
  const canvas = document.getElementById('glyph-detail-canvas')
  canvas.width = canvas.parentNode.offsetWidth
  canvas.height = canvas.width * 0.8
  enableHighDPICanvas(canvas)
  const ctx = canvas.getContext('2d')
  const {xmin, fontBaseline, fontSize} = getGlyphPosition(canvas, glyph, font)

  // Draw baseline
  ctx.fillStyle = '#F9C4C4'
  ctx.fillRect(32, fontBaseline, (canvas.width / pixelRatio) - 64, 1)

  glyph.draw(ctx, xmin, fontBaseline, fontSize)

  displaySelectedGlyphInfo(glyph)
  highlightSelectedGlyph(glyph.index)
  history.replaceState({}, document.title, '?i=' + glyph.index)
}

function getGlyphPosition(canvas, glyph, font) {
  const w = canvas.width / pixelRatio
  const h = canvas.height / pixelRatio
  const maxHeight = font.tables.head.yMax - font.tables.head.yMin
  const maxWidth = font.tables.head.xMax - font.tables.head.xMin

  const fontScale = Math.min(w / maxWidth, h / maxHeight)
  const fontSize = fontScale * font.unitsPerEm
  const fontBaseline = h * font.tables.head.yMax / maxHeight;
  const glyphWidth = glyph.advanceWidth * fontScale
  const xmin = (w - glyphWidth) / 2

  return { xmin, fontBaseline, fontSize }
}

function writeGlyph(font, glyphCanvas, glyphIndex) {
  if (glyphIndex >= font.numGlyphs) return;

  glyphCanvas.id = 'g' + glyphIndex

  const glyph = font.glyphs.get(glyphIndex)
  const {xmin, fontBaseline, fontSize} = getGlyphPosition(glyphCanvas, glyph, font)

  const ctx = glyphCanvas.getContext('2d')
  glyph.draw(ctx, xmin, fontBaseline, fontSize)
}

function highlightPagination(pageNum) {
  removeClass('selected-page')
  document.getElementById('p' + pageNum).className = 'selected-page'
}

function clearGlyphCanvases() {
  removeClass('active')
  const glyphCanvases = document.getElementsByClassName('glyph')

  for (let i = 0; i < glyphCanvases.length; i++) {
    glyphCanvases[i].getContext('2d').clearRect(0, 0, glyphCanvases[i].width, glyphCanvases[i].height);
  }
}

function displaySelectedGlyphPage(font, glyphsPerPage, pageNum) {
  clearGlyphCanvases()
  highlightPagination(pageNum)

  const firstGlyphIndex = pageNum * glyphsPerPage
  const glyphCanvases = document.getElementsByClassName('glyph')

  for (let i = 0; i < glyphsPerPage; i++) {
    writeGlyph(font, glyphCanvases[i], firstGlyphIndex + i)
  }
}

function getNumCols() {
  if (window.innerWidth > 940) {
    return 16
  }
  else if (window.innerWidth <= 940 && window.innerWidth > 700) {
    return 10
  }
  else if (window.innerWidth <= 700) {
    return 6
  }
}

function createGlyphCanvasContainer(font, glyphIndex) {
  const glyphCanvasContainer = document.createElement('div')
  glyphCanvasContainer.classList.add('glyph-container')

  glyphCanvasContainer.addEventListener('click', (e) => {
    displaySelectedGlyph(font, font.glyphs.get(e.target.id.substr(1)))
  }, false)

  document.getElementById('glyph-grid').appendChild(glyphCanvasContainer)

  return glyphCanvasContainer
}

function createGlyphCanvas(font, glyphIndex) {
  const glyphCanvasContainer = createGlyphCanvasContainer(font, glyphIndex)
  const canvasWidth = document.getElementById('glyph-grid').offsetWidth / getNumCols() - 1

  const glyphCanvas = document.createElement('canvas')
  glyphCanvas.width = canvasWidth
  glyphCanvas.height = canvasWidth * 1.2
  glyphCanvas.className = 'glyph'

  enableHighDPICanvas(glyphCanvas)
  glyphCanvasContainer.appendChild(glyphCanvas)

  return glyphCanvas
}

function displayGlyphGrid(font, glyphsPerPage) {
  for (let i = 0; i < glyphsPerPage; i++) {
    createGlyphCanvas(font, i)
  }
}

function displayPagination(font, glyphsPerPage) {
  const numPages = Math.ceil(font.numGlyphs / glyphsPerPage)
  var pagination = document.createDocumentFragment()

  for (let i = 0; i < numPages; i++) {
    const pageLink = document.createElement('a');
    pageLink.href = '#'
    pageLink.textContent = i + 1
    pageLink.id = 'p' + i

    pageLink.addEventListener('click', (e) => {
      e.preventDefault()
      displaySelectedGlyphPage(font, glyphsPerPage, e.target.id.substr(1))
    }, false)

    pagination.appendChild(pageLink)
  }

  document.getElementById('glyph-pagination').appendChild(pagination)
}

function getInitialGlyphIndex(font) {
  const parsedUrl = new URL(window.location.href)
  let glyphIndex = parsedUrl.searchParams.get('i')

  if (!glyphIndex) { glyphIndex = 5 }
  if (glyphIndex >= font.numGlyphs) { glyphIndex = font.numGlyphs - 1 }
  if (glyphIndex < 0 ) { glyphIndex = 0 }

  return glyphIndex
}

function getGlyphsPerPage() {
  if (window.innerWidth > 940) {
    return 128
  }
  else if (window.innerWidth <= 940 && window.innerWidth > 700) {
    return 130
  }
  else if (window.innerWidth <= 700) {
    return 54
  }
}

function glyphInspector(fontFile) {
  opentype.load(fontFile, function(err, font) {
    if (err) {
      console.log('Font could not be loaded: ' + err)
    }
    else {
      const glyphsPerPage = getGlyphsPerPage()
      const initialGlyphIndex = getInitialGlyphIndex(font)
      const initialGlyphPage = Math.floor(initialGlyphIndex / glyphsPerPage)

      displayPagination(font, glyphsPerPage)
      displayGlyphGrid(font, glyphsPerPage)
      displaySelectedGlyphPage(font, glyphsPerPage, initialGlyphPage)
      displaySelectedGlyph(font, font.glyphs.get(initialGlyphIndex))
    }
  })
}
<commit_msg>Further refactoring of glyph inspector JS<commit_after>const pixelRatio = window.devicePixelRatio || 1

function removeClass(classToRemove) {
  nodes = document.getElementsByClassName(classToRemove)
  if (!nodes[0]) return

  nodes[0].classList.remove(classToRemove)
  if (nodes[0]) removeClass(classToRemove)
}

function formatCSSCode(unicode) {
  unicode = unicode.toString(16)

  if (unicode.length > 4) {
    return '\' + ("000000" + unicode.toUpperCase()).substr(-6)
  }
  else {
    return 'content: \'\' + ("0000" + unicode.toUpperCase()).substr(-4) + '\';'
  }
}

function formatHTMLCode(unicode) {
  return '&#' + unicode + ';'
}

function enableHighDPICanvas(canvas) {
  if (pixelRatio === 1) return

  const oldWidth = canvas.width
  const oldHeight = canvas.height
  canvas.width = oldWidth * pixelRatio
  canvas.height = oldHeight * pixelRatio
  canvas.style.width = oldWidth + 'px'
  canvas.style.height = oldHeight + 'px'
  canvas.getContext('2d').scale(pixelRatio, pixelRatio)
}

function highlightSelectedGlyph(glyphIndex) {
  removeClass('active')
  document.getElementById('g' + glyphIndex).parentNode.classList.add('active')
}

function displaySelectedGlyphInfo(glyph) {
  document.getElementById('glyph-info').innerHTML = `

Glyph Name

${glyph.name}

HTML Code

${glyph.unicodes.map(formatHTMLCode).join(', ') || 'Not available' }

CSS Code

${glyph.unicodes.map(formatCSSCode).join(', ') || 'Not available' }

  `
}

function displaySelectedGlyph(font, glyph) {
  displaySelectedGlyphInfo(glyph)
  highlightSelectedGlyph(glyph.index)
  history.replaceState({}, document.title, '?i=' + glyph.index)

  const canvas = document.getElementById('glyph-detail-canvas')
  canvas.width = canvas.parentNode.offsetWidth
  canvas.height = canvas.width * 0.8
  enableHighDPICanvas(canvas)
  const ctx = canvas.getContext('2d')
  const { xmin, fontBaseline, fontSize } = getGlyphPosition(font, canvas, glyph)

  // Draw baseline
  ctx.fillStyle = '#F9C4C4'
  ctx.fillRect(32, fontBaseline, (canvas.width / pixelRatio) - 64, 1)

  glyph.draw(ctx, xmin, fontBaseline, fontSize)
}

function getGlyphPosition(font, canvas, glyph) {
  const w = canvas.width / pixelRatio
  const h = canvas.height / pixelRatio
  const maxHeight = font.tables.head.yMax - font.tables.head.yMin
  const maxWidth = font.tables.head.xMax - font.tables.head.xMin

  const fontScale = Math.min(w / maxWidth, h / maxHeight)
  const fontSize = fontScale * font.unitsPerEm
  const fontBaseline = h * font.tables.head.yMax / maxHeight;
  const glyphWidth = glyph.advanceWidth * fontScale
  const xmin = (w - glyphWidth) / 2

  return { xmin, fontBaseline, fontSize }
}

function writeGlyph(font, glyphCanvas, glyphIndex) {
  if (glyphIndex >= font.numGlyphs) return;

  glyphCanvas.id = 'g' + glyphIndex

  const glyph = font.glyphs.get(glyphIndex)
  const { xmin, fontBaseline, fontSize } = getGlyphPosition(font, glyphCanvas, glyph)

  const ctx = glyphCanvas.getContext('2d')
  glyph.draw(ctx, xmin, fontBaseline, fontSize)
}

function highlightPagination(pageNum) {
  removeClass('selected-page')
  document.getElementById('p' + pageNum).className = 'selected-page'
}

function clearGlyphCanvases() {
  removeClass('active')
  const glyphCanvases = document.getElementsByClassName('glyph')

  for (let i = 0; i < glyphCanvases.length; i++) {
    glyphCanvases[i].getContext('2d').clearRect(0, 0, glyphCanvases[i].width, glyphCanvases[i].height);
  }
}

function displaySelectedGlyphPage(font, glyphsPerPage, pageNum) {
  clearGlyphCanvases()
  highlightPagination(pageNum)

  const firstGlyphIndex = pageNum * glyphsPerPage
  const glyphCanvases = document.getElementsByClassName('glyph')

  for (let i = 0; i < glyphsPerPage; i++) {
    writeGlyph(font, glyphCanvases[i], firstGlyphIndex + i)
  }
}

function getNumCols() {
  if (window.innerWidth > 940) return 16
  else if (window.innerWidth <= 940 && window.innerWidth > 700) return 10
  else if (window.innerWidth <= 700) return 6
}

function createGlyphCanvasContainer(font) {
  const glyphCanvasContainer = document.createElement('div')
  glyphCanvasContainer.classList.add('glyph-container')

  glyphCanvasContainer.addEventListener('click', (e) => {
    displaySelectedGlyph(font, font.glyphs.get(e.target.id.substr(1)))
  }, false)

  document.getElementById('glyph-grid').appendChild(glyphCanvasContainer)

  return glyphCanvasContainer
}

function createGlyphCanvas(font) {
  const glyphCanvasContainer = createGlyphCanvasContainer(font)
  const canvasWidth = document.getElementById('glyph-grid').offsetWidth / getNumCols() - 1

  const glyphCanvas = document.createElement('canvas')
  glyphCanvas.width = canvasWidth
  glyphCanvas.height = canvasWidth * 1.2
  glyphCanvas.className = 'glyph'

  enableHighDPICanvas(glyphCanvas)
  glyphCanvasContainer.appendChild(glyphCanvas)

  return glyphCanvas
}

function displayGlyphGrid(font, glyphsPerPage) {
  for (let i = 0; i < glyphsPerPage; i++) {
    createGlyphCanvas(font)
  }
}

function displayPagination(font, glyphsPerPage) {
  const numPages = Math.ceil(font.numGlyphs / glyphsPerPage)
  var pagination = document.createDocumentFragment()

  for (let i = 0; i < numPages; i++) {
    const pageLink = document.createElement('a');
    pageLink.href = '#'
    pageLink.textContent = i + 1
    pageLink.id = 'p' + i

    pageLink.addEventListener('click', (e) => {
      e.preventDefault()
      displaySelectedGlyphPage(font, glyphsPerPage, e.target.id.substr(1))
    }, false)

    pagination.appendChild(pageLink)
  }

  document.getElementById('glyph-pagination').appendChild(pagination)
}

function getInitialGlyph(font) {
  let glyphIndex = new URL(window.location.href).searchParams.get('i')

  if (!glyphIndex) { glyphIndex = 5 }
  if (glyphIndex >= font.numGlyphs) { glyphIndex = font.numGlyphs - 1 }
  if (glyphIndex < 0 ) { glyphIndex = 0 }

  return font.glyphs.get(glyphIndex)
}

function getGlyphsPerPage() {
  if (window.innerWidth > 940) return 128
  else if (window.innerWidth <= 940 && window.innerWidth > 700) return 130
  else if (window.innerWidth <= 700) return 54
}

function glyphInspector(fontFile) {
  opentype.load(fontFile, function(err, font) {
    if (err) {
      console.log('Font could not be loaded: ' + err)
    }
    else {
      const glyphsPerPage = getGlyphsPerPage()
      const initialGlyph = getInitialGlyph(font)
      const initialGlyphPage = Math.floor(initialGlyph.index / glyphsPerPage)

      displayPagination(font, glyphsPerPage)
      displayGlyphGrid(font, glyphsPerPage)
      displaySelectedGlyphPage(font, glyphsPerPage, initialGlyphPage)
      displaySelectedGlyph(font, initialGlyph)
    }
  })
}
<|endoftext|>


<commit_before>/* ==========================================================================
   Complete cookie reader/writer
   From https://developer.mozilla.org/en-US/docs/DOM/document.cookie

   Usage:
   KO.cookies.setItem(name, value[, end[, path[, domain[, secure]]]])
   KO.cookies.getItem(name)
   KO.cookies.removeItem(name[, path])
   KO.cookies.hasItem(name)
   KO.cookies.keys()
   ========================================================================== */
;(function (KO) {
	KO.cookies = {
		getItem: function(sKey) {
			if (!sKey || !this.hasItem(sKey)) { return null; }
			return unescape(document.cookie.replace(new RegExp('(?:^|.*;\\s*)' + escape(sKey).replace(/[\-\.\+\*]/g, '\\$&') + '\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*'), '$1'));
		},
		setItem: function(sKey, sValue, vEnd, sPath, sDomain, bSecure) {
			if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return; }
			var sExpires = '';
			if (vEnd) {
				switch (vEnd.constructor) {
					case Number:
						sExpires = vEnd === Infinity ? '; expires=Tue, 19 Jan 2038 03:14:07 GMT' : '; max-age=' + vEnd;
						break;
					case String:
						sExpires = '; expires=' + vEnd;
						break;
					case Date:
						sExpires = '; expires=' + vEnd.toGMTString();
						break;
				}
			}
			document.cookie = escape(sKey) + '=' + escape(sValue) + sExpires + (sDomain ? '; domain=' + sDomain : '') + (sPath ? '; path=' + sPath : '') + (bSecure ? '; secure' : '');
		},
		removeItem: function(sKey, sPath) {
			if (!sKey || !this.hasItem(sKey)) { return; }
			document.cookie = escape(sKey) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT' + (sPath ? '; path=' + sPath : '');
		},
		hasItem: function(sKey) {
			return (new RegExp('(?:^|;\\s*)' + escape(sKey).replace(/[\-\.\+\*]/g, '\\$&') + '\\s*\\=')).test(document.cookie);
		},
		// Optional method: you can safely remove it!
		keys: function() {
			var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, '').split(/\s*(?:\=[^;]*)?;\s*/);
			for (var nIdx = 0; nIdx < aKeys.length; nIdx++) { aKeys[nIdx] = unescape(aKeys[nIdx]); }
			return aKeys;
		}
	};
})(window.KO = window.KO || {});
<commit_msg>remove cookies helper because we are now using a bower package for them<commit_after><|endoftext|>


<commit_before>$(document).ready(function(){

	// add highscore entries to table
	function loadHighscore(){
		$.getJSON("commit.php",function(result){
    		$.each(result, function(i){
      			$('.score-board table').html('
' + result[i].nick + '	' + result[i].time + '	' + result[i].score + '
');
    		});
  		});
	}

	setInterval(loadHighscore, 30000);

	loadHighscore();
});<commit_msg>fixing bug with getting highscores<commit_after>$(document).ready(function(){

	// add highscore entries to table
	function loadHighscore(){
		$.getJSON("commit.php",function(result){
			$('.score-board table').empty();
    		$.each(result, function(i){
      			$('.score-board table').append('
' + result[i].nick + '	' + result[i].time + '	' + result[i].score + '
');
    		});
  		});
	}

	setInterval(loadHighscore, 30000);

	loadHighscore();
});<|endoftext|>


<commit_before>// the following code is based on these two methods:
// *** http://bost.ocks.org/mike/chart/
// *** http://bost.ocks.org/mike/chart/time-series-chart.js

// ********************************************************************************
//  USAGE:
//    // instantiate chart with closure
//      mychart = geom_point();
//
//    // build config object
//      var config = {
//        data: [{x: 10, y: 3, type: 0.1, enrl_cnt: 5},{x: 4, y: 2, type: 0.2, enrl_cnt: 10},{x: 8, y: 9, type: 0.5, enrl_cnt: 15}],
//        aes: {
//          x: { value: 'x', type: 'linear' },
//          y: { value: 'y', type: 'linear' },
//          fill: {
//            value: 'type',   // variable to map to fill colour
//            type: 'discrete'   // discrete or continuous scale?
//          },
//          size: {
//            value: 'enrl_cnt',
//            type: 'linear',
//            range: [5, 20]
//          },
//          alpha: {
//            value: 'enrl_cnt',
//            type: 'linear',
//            range: [.4, 1]
//          }
//        },
//      };
//
//    // get a selection, attach config object to selection, and call mychart
//      d3.select('#chart1').datum(config).call(mychart);
// ********************************************************************************

function geom_point() {
  // default values
  var width = 1000,
      height = 600,
      margin = { top: 30, right: 20, bottom: 30, left: 50 },
      legend = { margin: 100, padding: 20 },
      scaleX = d3.scale.linear(),
      scaleY = d3.scale.linear(),
      scaleSize = null,
      scaleAlpha = null,
      scaleFill = d3.scale.category20(),
      axisX = d3.svg.axis().scale(scaleX).orient("bottom"),
      axisY = d3.svg.axis().scale(scaleY).orient("left"),
      data = [],
      defaultAes = {
        fill: {type: 'static', value: 'black'},
        size: {type: 'static', value: 5},
        alpha: {type: 'static', value: 1}
      },
      // default unmapped aesthetics
      aes = null;
      ;

  ;

  function my(selection) {
    selection.each(function(d, i) {
      var nonDiscreteVariables = [];    // to hold array of variables that need to be converted to numeric values

      // attach config to
      data = d.data;
      aes = Object.create(defaultAes);

      // override default aesthetic mappings
      for (prop in d.aes) {
        aes[prop] = d.aes[prop];
        if (['linear'].some(function(e) { return (e === d.aes[prop].type); })) nonDiscreteVariables.push(d.aes[prop].value);
      }

      // make x and y linear
      data = data.map(function(d) {
        for (prop in d) {
          if (prop === aes.x.value || prop === aes.y.value || nonDiscreteVariables.some( function(e) { return (e === prop); } )) {  // implement checks for other linear scales
            d[prop] = +d[prop];
          }
        }

        return d;
      });

      // console.log(data);

      //update x scale
      scaleX.domain(d3.extent(data, function(d) { return d[aes.x.value]; }))
      .range([0, width - margin.left - margin.right - legend.margin - legend.padding])
      .nice();

      //update y scale
      scaleY.domain(d3.extent(data, function(d) { return d[aes.y.value]; }))
      .range([height - margin.top - margin.bottom, 0])
      .nice();

      //update color scale
      if (aes.fill.type === 'linear') {
        scaleFill = d3.scale.quantize()
        .domain(d3.extent(data, function(d) { return d[aes.fill.value]; }))
        .range(colorbrewer.RdBu[11]);
      }
        // if not explicity configured as continuous, make scale discrete
        else if (aes.fill.type === 'discrete') {
          scaleFill = d3.scale.category20().domain(d3.extent(data, function(d) { return d[aes.fill.value]; }));
        }
        else {
          scaleFill = function() {return aes.fill.value; };
        }

      //update size scale
      if (aes.size.type === 'linear') {
        scaleSize = d3.scale.linear().domain(d3.extent(data, function(d) { return d[aes.size.value]; }))
        .range(aes.size.range);
      }
      else {
        scaleSize = function() {return aes.size.value; };
      }

      //update alpha scale
      if (aes.alpha.type === 'linear') {
        scaleAlpha = d3.scale.linear().domain(d3.extent(data, function(d) { return d[aes.alpha.value]; }))
        .range(aes.alpha.range);
      }
      else {
        scaleAlpha = function() {return aes.alpha.value; };
      }

      // select the SVG element, if it exists
      var svg = d3.select(this).selectAll('svg').data([d]);

      // otherwise create the skeletal chart
      var gEnter = svg.enter().append('svg').append('g');   // outer g element which contains both chart and legend
      var chartAreaEnter = gEnter.append('g').attr('class', 'chart-area');   // chart g
      chartAreaEnter.append('g').attr('class', 'x axis');
      chartAreaEnter.append('g').attr('class', 'y axis');
      var legendAreaEnter = gEnter.append('g').attr('class', 'legend-area');   // legend g

      // update outer dimensions
      svg.attr('height', height)
        .attr('width', width);

      // update inner dimensions
      var g = svg.select('g').attr('transform', translateString(margin.left, margin.top));
      var legendArea = svg.select('.legend-area').attr('transform', translateString(width - margin.left - margin.right - legend.margin - legend.padding, 0));

      // update points
      var dots = g.selectAll('.dot').data(data);

      // update existing points
      dots.transition().duration(1500)
        .attr('cx', function(d) {return scaleX(d[aes.x.value]); })
        .attr('cy', function(d, i) {return scaleY(d[aes.y.value]); })
        .attr('r', function(d) {return scaleSize( (aes.size) ? d[aes.size.value] : null); })
        .attr('fill-opacity', function(d) {return scaleAlpha( (aes.alpha) ? d[aes.alpha.value] : null); })
        .style('fill', function(d) { return scaleFill( (aes.fill) ? d[aes.fill.value] : null); })
      ;

      // insert new points
      dots.enter()
      .append('circle')
      .attr('class', 'dot')
      .attr('r', 0)
      .attr('fill-opacity', function(d) {return scaleAlpha( (aes.alpha) ? d[aes.alpha.value] : null); })
      .style('fill', function(d) { return scaleFill((aes.fill) ? d[aes.fill.value] : null); })
      // .on('mouseover', jgd3.tip.show)
      // .on('mouseout', jgd3.tip.hide)
        .attr('cx', function(d) {return scaleX(d[aes.x.value]); })
        .attr('cy', function(d, i) {return scaleY(d[aes.y.value]); })
      .transition().duration(1500)
      .attr('r', function(d) {return scaleSize( (aes.size) ? d[aes.size.value] : null); })
      ;

      // remove deleted points
      dots.exit().transition().duration(1500).remove();

      // update x axis
      g.select('.x.axis')
      .attr('transform', translateString(0, scaleY.range()[0]))
      .call(axisX)
      ;

      //update y axis
      g.select('.y.axis')
      .call(axisY)
      ;

    });
  }

  // getters and setters
  my.width = function(value) {
    if (!arguments.length) return width;
    width = value;
    return my;
  };

  my.height = function(value) {
    if (!arguments.length) return height;
    height = value;
    return my;
  };

  my.scaleX = function(value) {
    if (!arguments.length) return scaleX;
    scaleX = value;
    return scaleX;
  }

  my.scaleY = function(value) {
    if (!arguments.length) return scaleY;
    scaleY = value;
    return scaleY;
  }

  my.scaleFill = function(value) {
    if (!arguments.length) return scaleFill;
    scaleFill = value;
    return scaleFill;
  }

  my.scaleSize = function(value) {
    if (!arguments.length) return scaleSize;
    scaleSize = value;
    return scaleSize;
  }

  my.defaultAes = function() {
    return defaultAes;
  }

  my.aes = function(aesthetic) {
    return aes;
  }

  my.data = function() {
    return data;
  }

  return my;

}

<commit_msg>tooltip working with default x and y html<commit_after>// the following code is based on these two methods:
// *** http://bost.ocks.org/mike/chart/
// *** http://bost.ocks.org/mike/chart/time-series-chart.js

// ********************************************************************************
//  USAGE:
//    // instantiate chart with closure
//      mychart = geom_point();
//
//    // build config object
//      var config = {
//        data: [{x: 10, y: 3, type: 0.1, enrl_cnt: 5},{x: 4, y: 2, type: 0.2, enrl_cnt: 10},{x: 8, y: 9, type: 0.5, enrl_cnt: 15}],
//        aes: {
//          x: { value: 'x', type: 'linear' },
//          y: { value: 'y', type: 'linear' },
//          fill: {
//            value: 'type',   // variable to map to fill colour
//            type: 'discrete'   // discrete or continuous scale?
//          },
//          size: {
//            value: 'enrl_cnt',
//            type: 'linear',
//            range: [5, 20]
//          },
//          alpha: {
//            value: 'enrl_cnt',
//            type: 'linear',
//            range: [.4, 1]
//          }
//        },
//      };
//
//    // get a selection, attach config object to selection, and call mychart
//      d3.select('#chart1').datum(config).call(mychart);
// ********************************************************************************

function geom_point() {
  // default values
  var width = 1000,
      height = 600,
      margin = { top: 30, right: 20, bottom: 30, left: 50 },
      legend = { margin: 100, padding: 20 },
      scaleX = d3.scale.linear(),
      scaleY = d3.scale.linear(),
      scaleSize = null,
      scaleAlpha = null,
      scaleFill = d3.scale.category20(),
      axisX = d3.svg.axis().scale(scaleX).orient("bottom"),
      axisY = d3.svg.axis().scale(scaleY).orient("left"),
      data = [],
      attachedSelection = undefined;
      defaultAes = {
        fill: {type: 'static', value: 'black'},
        size: {type: 'static', value: 5},
        alpha: {type: 'static', value: 1},
        tip: {type: 'static', value: []}      // {type: 'mapped', value: [field1, field2, etc..]
      },
      // variable to hold aesthetics specific to instantiated chart
      aes = {},
      tip = d3.tip().attr('class', 'd3-tip').direction('n').offset([-10, 0]);

  ;

  function round(num, precision) {
    if (arguments.length === 0) {
      throw 'Must supply at least one numeric argument.  Usage:  round(number, [precision]).  If precision is omitted it defaults to precision of 0.';
    }

    if (arguments.length === 1) {
      precision = 0;
    }
    return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision);
  }

  function my(selection) {
    // if no arguments supplied, and no existing selection, raise error
    if (arguments.length === 0 && typeof attachedSelection === 'undefined') {
      throw 'No selection defined.  Call function with selection as a parameter';
    }
    // else save selection in variable
    else if (arguments.length) {
      attachedSelection = selection;
    }

    //draw chart
    attachedSelection.each(function(d, i) {
      var nonDiscreteVariables = [];    // to hold array of variables that need to be converted to numeric values

      // attach config to
      data = d.data;
      aes = Object.create(defaultAes);

      // override default aesthetic mappings
      for (prop in d.aes) {
        aes[prop] = d.aes[prop];
        if (['linear'].some(function(e) { return (e === d.aes[prop].type); })) nonDiscreteVariables.push(d.aes[prop].value);
      }

      // make x and y linear
      data = data.map(function(d) {
        for (prop in d) {
          if (prop === aes.x.value || prop === aes.y.value || nonDiscreteVariables.some( function(e) { return (e === prop); } )) {  // implement checks for other linear scales
            d[prop] = +d[prop];
          }
        }

        return d;
      });

      //update tooltip html function
      tip.html(function(d, i) {
        if (aes.tip.type === 'static') {
          return '(' + round(d[aes.x.value], 2) + ', ' + round(d[aes.y.value], 2) + ')';
        }
      });

      // console.log(data);

      //update x scale
      scaleX.domain(d3.extent(data, function(d) { return d[aes.x.value]; }))
      .range([0, width - margin.left - margin.right - legend.margin - legend.padding])
      .nice();

      //update y scale
      scaleY.domain(d3.extent(data, function(d) { return d[aes.y.value]; }))
      .range([height - margin.top - margin.bottom, 0])
      .nice();

      //update color scale
      if (aes.fill.type === 'linear') {
        scaleFill = d3.scale.quantize()
        .domain(d3.extent(data, function(d) { return d[aes.fill.value]; }))
        .range(colorbrewer.RdBu[11]);
      }
      // if not explicity configured as continuous, make scale discrete
      else if (aes.fill.type === 'discrete') {
        scaleFill = d3.scale.category20().domain(d3.extent(data, function(d) { return d[aes.fill.value]; }));
      }
      else {
        scaleFill = function() {return aes.fill.value; };
      }

      //update size scale
      if (aes.size.type === 'linear') {
        scaleSize = d3.scale.linear().domain(d3.extent(data, function(d) { return d[aes.size.value]; }))
        .range(aes.size.range);
      }
      else {
        scaleSize = function() {return aes.size.value; };
      }

      //update alpha scale
      if (aes.alpha.type === 'linear') {
        scaleAlpha = d3.scale.linear().domain(d3.extent(data, function(d) { return d[aes.alpha.value]; }))
        .range(aes.alpha.range);
      }
      else {
        scaleAlpha = function() {return aes.alpha.value; };
      }

      // select the SVG element, if it exists
      var svg = d3.select(this).selectAll('svg').data([d]);

      // otherwise create the skeletal chart
      var gEnter = svg.enter().append('svg').append('g');   // outer g element which contains both chart and legend
      var chartAreaEnter = gEnter.append('g').attr('class', 'chart-area');   // chart g
      chartAreaEnter.append('g').attr('class', 'x axis');
      chartAreaEnter.append('g').attr('class', 'y axis');
      var legendAreaEnter = gEnter.append('g').attr('class', 'legend-area');   // legend g

      // update outer dimensions
      svg.attr('height', height)
        .attr('width', width);

      // update inner dimensions
      var g = svg.select('g').attr('transform', translateString(margin.left, margin.top));
      var legendArea = svg.select('.legend-area').attr('transform', translateString(width - margin.left - margin.right - legend.margin - legend.padding, 0));

      // attach tooltip to svg
      g.call(tip);

      // update points
      var dots = g.selectAll('.dot').data(data);

      // update existing points
      dots
      .on('mouseover', tip.show)
      .on('mouseout', tip.hide)
      .transition().duration(1500)
      .attr('cx', function(d) {return scaleX(d[aes.x.value]); })
      .attr('cy', function(d, i) {return scaleY(d[aes.y.value]); })
      .attr('r', function(d) {return scaleSize( (aes.size) ? d[aes.size.value] : null); })
      .attr('fill-opacity', function(d) {return scaleAlpha( (aes.alpha) ? d[aes.alpha.value] : null); })
      .style('fill', function(d) { return scaleFill( (aes.fill) ? d[aes.fill.value] : null); })
      ;

      // insert new points
      dots.enter()
      .append('circle')
      .attr('class', 'dot')
      .attr('r', 0)
      .attr('fill-opacity', function(d) {return scaleAlpha( (aes.alpha) ? d[aes.alpha.value] : null); })
      .style('fill', function(d) { return scaleFill((aes.fill) ? d[aes.fill.value] : null); })
      // .on('mouseover', jgd3.tip.show)
      // .on('mouseout', jgd3.tip.hide)
      .attr('cx', function(d) {return scaleX(d[aes.x.value]); })
      .attr('cy', function(d, i) {return scaleY(d[aes.y.value]); })
      .on('mouseover', tip.show)
      .on('mouseout', tip.hide)
      .transition().duration(1500)
      .attr('r', function(d) {return scaleSize( (aes.size) ? d[aes.size.value] : null); })
      ;

      // remove deleted points
      dots.exit().transition().duration(1500).remove();

      // update x axis
      g.select('.x.axis')
      .attr('transform', translateString(0, scaleY.range()[0]))
      .call(axisX)
      ;

      //update y axis
      g.select('.y.axis')
      .call(axisY)
      ;

    });
  }

  // getters and setters
  my.width = function(value) {
    if (!arguments.length) return width;
    width = value;
    return my;
  };

  my.height = function(value) {
    if (!arguments.length) return height;
    height = value;
    return my;
  };

  my.scaleX = function(value) {
    if (!arguments.length) return scaleX;
    scaleX = value;
    return scaleX;
  }

  my.scaleY = function(value) {
    if (!arguments.length) return scaleY;
    scaleY = value;
    return scaleY;
  }

  my.scaleFill = function(value) {
    if (!arguments.length) return scaleFill;
    scaleFill = value;
    return scaleFill;
  }

  my.scaleSize = function(value) {
    if (!arguments.length) return scaleSize;
    scaleSize = value;
    return scaleSize;
  }

  my.defaultAes = function() {
    return defaultAes;
  }

  my.aes = function(aesthetic) {
    return aes;
  }

  my.data = function() {
    return data;
  }

  return my;

}

<|endoftext|>


<commit_before>(function(exports) {
  // var ObservableDOM = require("frb/dom"); // add support for content editable
  // var Bindings = require("frb/bindings");
  var SortedSet = UniqueSet = exports.FieldDB ? exports.FieldDB.Collection : require("fielddb").FieldDB.Collection;
  var CORS = exports.FieldDB ? exports.FieldDB.CORS : require("fielddb/api/CORS").CORS;
  var Q = exports.FieldDB ? exports.FieldDB.Q : require("q");
  var NonContentWords = exports.NonContentWords || require('./NonContentWords').NonContentWords;
  var LexemeFrequency = exports.LexemeFrequency || require('./LexemeFrequency').LexemeFrequency;
  var MorphemeSegmenter = exports.MorphemeSegmenter || require('./MorphemeSegmenter').MorphemeSegmenter;

  var maxLexiconSize = 1000;

  var LexiconNode = function(options) {
    for (var property in options) {
      if (options.hasOwnProperty(property)) {
        this[property] = options[property];
      }
    }
    this.igtBeforeCleaning = JSON.parse(JSON.stringify(options.igt));
    Object.call(this);
  };

  LexiconNode.prototype = Object.create(Object.prototype, {
    constructor: {
      value: LexiconNode
    },
    equals: {
      value: function(b) {
        var a = this;
        var equal = false;
        var fieldIndex,
          field,
          tmpArray;

        if (!a.igt || !b.igt) {
          equal = false;
          return equal;
        }

        for (fieldIndex in this.expandedIGTFields) {
          field = this.expandedIGTFields[fieldIndex];
          if (a.igt.hasOwnProperty(field)) {
            // console.log(a);
            if (a.igt[field] === b.igt[field]) {
              equal = true;
            }
          }
        }
        // If the nodes are "equal" then make sure they have the same combineFieldsIfEqual
        if (equal) {
          for (fieldIndex in this.combineFieldsIfEqual) {
            field = this.combineFieldsIfEqual[fieldIndex];
            if (a.hasOwnProperty(field)) {
              tmpArray = [];
              a[field] = a[field].concat(b[field]);
              /*jshint loopfunc: true */
              a[field].map(function(item) {
                if (tmpArray.indexOf(item) === -1) {
                  tmpArray.push(item);
                }
              });
              a[field] = tmpArray;
              b[field] = tmpArray;
            }
          }
          if (a.confidence && b.confidence) {
            a.confidence = (parseFloat(a.confidence, 10) + parseFloat(b.confidence, 10)) / 2;
            b.confidence = a.confidence;
          }
          if (a.allomorphs || b.allomorphs) {
            var allomorphs = a.allomorphs ? a.allomorphs : "";
            allomorphs = allomorphs + allomorphs ? ", " : "";
            allomorphs = allomorphs + b.allomorphs ? b.allomorphs : "";
            a.allomorphs = allomorphs;
            b.allomorphs = allomorphs;
          }
        }
        return equal;
      }
    },
    expandedIGTFields: {
      value: ["morphemes", "gloss", "allomorphy", "phonetic", "orthography"]
    },
    combineFieldsIfEqual: {
      value: ["datumids", "utteranceContext"]
    },
    sortBy: {
      value: "morphemes",
      writable: true
    },
    compare: {
      value: function(b) {
        var a = this;
        var result = 0;
        if (!b || !b.igt || !b.igt[this.sortBy]) {
          return -1;
        }
        if (!a || !a.igt || !a.igt[this.sortBy]) {
          return 1;
        }
        if ((typeof(a.igt[this.sortBy]) === 'number') && (typeof(b.igt[this.sortBy]) === 'number')) {
          result = a.igt[this.sortBy] - b.igt[this.sortBy];
        } else if ((typeof(a.igt[this.sortBy]) === 'string') && (typeof(b.igt[this.sortBy]) === 'string')) {
          if (a.igt[this.sortBy] < b.igt[this.sortBy]) {
            result = -1;
          } else if (a.igt[this.sortBy] > b.igt[this.sortBy]) {
            result = 1;
          } else {
            result = 0;
          }
        } else if (typeof(a.igt[this.sortBy]) === 'string') {
          result = 1;
        } else {
          result = -1;
        }
        return result;
      }
    },
    clean: {
      value: function() {
        console.log("Preparing datum with this lexical entry to be cleaned...");
        var deffered = Q.defer();
        this.cleanedData = [];
        var promises = [];
        var self = this;
        this.proposedChanges = [];

        var successFunction = function(doc) {
          console.log(doc);
          doc.datumFields.map(function(datumField) {
            if (self.igtBeforeCleaning[datumField.label] !== self.igt[datumField.label] && (new RegExp(self.igtBeforeCleaning[datumField.label], "i")).test(datumField.mask)) {
              var change = {
                before: datumField.mask + ""
              };
              // TODO this makes things lower case... because part of the map reduce seems to be doing that...
              datumField.mask = datumField.mask.replace(new RegExp(self.igtBeforeCleaning[datumField.label], "ig"), self.igt[datumField.label]);
              datumField.value = datumField.mask;
              change.after = datumField.mask;
              self.proposedChanges.push(change);
              return datumField;
            }
          });
          self.cleanedData.push(doc);
        };
        var failFunction = function(reason) {
          console.log(reason);
        };

        for (var idIndex = 0; idIndex < this.datumids.length; idIndex++) {
          console.log(this.datumids.length[idIndex]);
          promises[idIndex] = CORS.makeCORSRequest({
            method: 'GET',
            dataType: 'json',
            url: this.url + "/" + this.datumids[idIndex]
          });
          promises[idIndex].then(successFunction).fail(failFunction);
        }
        Q.allSettled(promises).then(function(results) {
          deffered.resolve(self.proposedChanges);
        });
        return deffered.promise;
      }
    },
    save: {
      value: function() {
        var deffered = Q.defer(),
          promises = [];

        console.log("Saving cleaned datum...");
        while (this.cleanedData.length > 0) {
          var cleanedDatum = this.cleanedData.pop();
          promises.push(CORS.makeCORSRequest({
            method: 'PUT',
            dataType: 'json',
            data: cleanedDatum,
            url: this.url + "/" + cleanedDatum._id
          }));
        }
        Q.allSettled(promises).then(function(results) {
          console.log("Saving results: ", results);
          deffered.resolve(results);
        });
        return deffered.promise;
      }
    }
  });

  var Lexicon = function(values, equals, compare, getDefault) {
    console.log("\tConstructing Lexicon... ");
    // SortedSet.apply(this, [values, equals, compare, getDefault]);
    SortedSet.apply(this, Array.prototype.slice.call(arguments, 1));
    // if (!compare) {
    //   this.contentCompare = this.__proto__.igtCompare;
    // }
    // if (!equals) {
    //   this.contentEquals = this.__proto__.igtEqual;
    // }
  };

  Lexicon.prototype = Object.create(SortedSet.prototype, {
    constructor: {
      value: Lexicon
    },
    sortBy: {
      value: "morphemes"
    },

    toJSON: {
      value: function() {
        return JSON.stringify(this.toObject(), null, 2);
      }
    },
    getLexicalEntries: {
      value: function(lexicalEntryToMatch) {
        var deffered = Q.defer(),
          matches = [],
          self = this;

        if (!lexicalEntryToMatch) {
          deffered.resolve(matches);
        } else {
          this.filter(function(value, key, object, depth) {
            console.log(key + " of " + self.length);
            if (typeof lexicalEntryToMatch.equals === "function") {
              if (lexicalEntryToMatch.equals(value)) {
                matches.push(value);
                console.log(value);
              }
            } else {
              var howWellDoesThisMatch = 0;
              lexicalEntryToMatch = lexicalEntryToMatch.trim();
              for (var attr in value.igt) {
                if (value.igt.hasOwnProperty(attr) && value.igt[attr] === lexicalEntryToMatch) {
                  howWellDoesThisMatch = howWellDoesThisMatch + 1;
                }
              }
              if (howWellDoesThisMatch > 0) {
                matches.push(value);
                console.log(value);
              }
            }
            if (key === self.length - 1) {
              deffered.resolve(matches);
            }
          }, this);
        }
        return deffered.promise;
      }
    },

    guessContextSensitiveGlosses: {
      value: function(datum) {

        if (!datum.morphemes) {
          console.warn("There was no morphemes line to guess the gloss from...");
          return datum;
        }
        var glossGroups = [];
        var matchingNodes = [];
        var morphemeToFind = "";
        var morphemeGroup = datum.morphemes.split(/ +/);
        var matchingfunction = function(node) {
          if (node.igt.morphemes === morphemeToFind) {
            console.log(node);
            matchingNodes.push(node);
          }
        };
        for (var group in morphemeGroup) {
          var morphemes = morphemeGroup[group].split("-");
          var glosses = [];
          for (var m in morphemes) {
            if (!morphemes.hasOwnProperty(m)) {
              continue;
            }
            matchingNodes = [];
            morphemeToFind = morphemes[m];
            this.filter(matchingfunction);

            var gloss = "?"; // If there's no matching gloss, use question marks
            if (matchingNodes && matchingNodes.length > 0) {
              // Take the first gloss for this morpheme
              console.log("Glosses which match: " + morphemes[m], matchingNodes);
              try {
                gloss = matchingNodes[0].igt.gloss;
              } catch (e) {
                console.log(matchingNodes);
              }
            }
            glosses.push(gloss);
          }

          glossGroups.push(glosses.join("-"));
        }
        datum.glossAlternates = datum.glossAlternates ? datum.glossAlternates.concat(glossGroups) : glossGroups;
        datum.gloss = glossGroups.join(" ");
        // Replace the gloss line with the guessed glosses
        return datum;
      }
    },

    /**
     * Takes as a parameters an array of this.precedenceRelations which came from CouchDB precedence rule query.
     * Example Rule: {"key":{"x":"@","relation":"preceeds","y":"aqtu","context":"aqtu-nay-wa-n"},"value":2}
     */
    generatePrecedenceForceDirectedRulesJsonForD3: {
      value: function(dontConnectWordBoundaries) {
        /*
         * Cycle through the precedence rules, convert them into graph edges with the morpheme index in the morpheme array as the source/target values
         */
        var morphemeLinks = [];
        var morphemes = {};

        /*
         * Create the JSON required by D3
         */
        var precedenceGraph = {
          links: morphemeLinks,
          nodes: morphemes
        };
        this.precedenceGraph = precedenceGraph;

        return precedenceGraph;
      }
    }
  });

  var LexiconFactory = function(options) {
    // var lex = new Lexicon(null, Lexicon.prototype.igtEqual, Lexicon.prototype.igtCompare);
    var lex = new Lexicon();
    lex.precedenceRelations = new UniqueSet();
    lex.references = new UniqueSet();
    if (options.precedenceRelations && options.precedenceRelations.length > 0) {
      for (var i in options.precedenceRelations) {
        try {
          //Add source target and value to the link
          delete options.precedenceRelations[i].key.previous.utterance;
          delete options.precedenceRelations[i].key.subsequent.utterance;
          options.precedenceRelations[i].key.utteranceContext = options.precedenceRelations[i].key.context.utterance;
          options.precedenceRelations[i].key.datumid = options.precedenceRelations[i].id;

          // Put the previous and subsequent morphemes into the morpheme nodes
          // lex.add(options.precedenceRelations[i].key.context.utterance, new LexiconNode({
          //   igt: options.precedenceRelations[i].key.previous
          // }));
          lex.add(new LexiconNode({
            igt: options.precedenceRelations[i].key.previous,
            datumids: [options.precedenceRelations[i].key.context.id],
            utteranceContext: [options.precedenceRelations[i].key.context.utterance],
            url: options.url
          }));

          // lex.add(options.precedenceRelations[i].key.context.utterance, new LexiconNode({
          //   igt: options.precedenceRelations[i].key.previous
          // }));
          lex.add(new LexiconNode({
            igt: options.precedenceRelations[i].key.subsequent,
            datumids: [options.precedenceRelations[i].key.context.id],
            utteranceContext: [options.precedenceRelations[i].key.context.utterance],
            url: options.url
          }));
          lex.references.add(options.precedenceRelations[i].key.context.id);

          //To avoid loops
          if (options.precedenceRelations[i].key.subsequent.morphemes.indexOf("@") === -1) {
            lex.precedenceRelations.add(options.precedenceRelations[i].key);
          }

        } catch (e) {
          console.warn(e);
        }

      }
    }

    if (options.orthography) {
      if (options.nonContentWordsArray) {
        options.userSpecifiedNonContentWords = true;
        if (Object.prototype.toString.call(options.nonContentWordsArray) === '[object Array]' && options.nonContentWordsArray.length === 0) {
          options.userSpecifiedNonContentWords = false;
          console.log("User sent an empty array of non content words, attempting to automatically detect them");
        }
        // else if (options.nonContentWordsArray.trim && !options.nonContentWordsArray.trim()) {
        //   options.userSpecifiedNonContentWords = false;
        // }
      }
      NonContentWords.processNonContentWords(options);

      for (var wordIndex in options.wordFrequencies) {
        if (!options.wordFrequencies.hasOwnProperty(wordIndex)) {
          continue;
        }
        var word = options.wordFrequencies[wordIndex].orthography;
        var count = options.wordFrequencies[wordIndex].count;
        var categories = options.wordFrequencies[wordIndex].categories;
        if (lex.length > maxLexiconSize) {
          continue;
        }
        lex.add(new LexiconNode({
          igt: {
            orthography: word,
            utterance: word,
            morphemes: word,
            gloss: word,
          },
          categories: categories,
          datumids: [options._id],
          count: count,
          utteranceContext: [word],
          url: options.url
        }));

      }
    }

    for (var property in options) {
      if (options.hasOwnProperty(property)) {
        lex[property] = options[property];
      }
    }
    return lex;
  };
  Lexicon.NonContentWords = NonContentWords;
  Lexicon.LexemeFrequency = LexemeFrequency;
  Lexicon.LexiconNode = LexiconNode;
  Lexicon.LexiconFactory = LexiconFactory;
  Lexicon.MorphemeSegmenter = MorphemeSegmenter;

  exports.Lexicon = Lexicon;
  try {
    global.Lexicon = Lexicon;
    global.LexiconFactory = LexiconFactory;
  } catch (e) {
    console.log(e);
  }

  exports.LexiconFactory = LexiconFactory;

  // }(typeof exports === 'object' && exports || this));
})(typeof exports === 'undefined' ? this : exports);<commit_msg>using fielddb collection instead of collection<commit_after>(function(exports) {
  // var ObservableDOM = require("frb/dom"); // add support for content editable
  // var Bindings = require("frb/bindings");
  var SortedSet = exports.FieldDB ? exports.FieldDB.Collection : require("fielddb/api/Collection").Collection;
  var UniqueSet = SortedSet;
  var CORS = exports.FieldDB ? exports.FieldDB.CORS : require("fielddb/api/CORS").CORS;
  var Q = exports.FieldDB ? exports.FieldDB.Q : require("q");
  var NonContentWords = exports.NonContentWords || require('./NonContentWords').NonContentWords;
  var LexemeFrequency = exports.LexemeFrequency || require('./LexemeFrequency').LexemeFrequency;
  var MorphemeSegmenter = exports.MorphemeSegmenter || require('./MorphemeSegmenter').MorphemeSegmenter;

  var maxLexiconSize = 1000;

  var LexiconNode = function(options) {
    for (var property in options) {
      if (options.hasOwnProperty(property)) {
        this[property] = options[property];
      }
    }
    this.igtBeforeCleaning = JSON.parse(JSON.stringify(options.igt));
    Object.call(this);
  };

  LexiconNode.prototype = Object.create(Object.prototype, {
    constructor: {
      value: LexiconNode
    },
    id: {
      get: function() {
        var uniqueness = "";
        if (this.igt) {
          uniqueness = uniqueness + this.igt.morphemes ? this.igt.morphemes : "";
          uniqueness = uniqueness + this.igt.gloss ? this.igt.gloss : "";
        }
        return uniqueness;
      }
    },
    equals: {
      value: function(b) {
        var a = this;
        var equal = false;
        var fieldIndex,
          field,
          tmpArray;

        if (!a.igt || !b.igt) {
          equal = false;
          return equal;
        }

        for (fieldIndex in this.expandedIGTFields) {
          field = this.expandedIGTFields[fieldIndex];
          if (a.igt.hasOwnProperty(field)) {
            // console.log(a);
            if (a.igt[field] === b.igt[field]) {
              equal = true;
            }
          }
        }
        // If the nodes are "equal" then make sure they have the same combineFieldsIfEqual
        if (equal) {
          for (fieldIndex in this.combineFieldsIfEqual) {
            field = this.combineFieldsIfEqual[fieldIndex];
            if (a.hasOwnProperty(field)) {
              tmpArray = [];
              a[field] = a[field].concat(b[field]);
              /*jshint loopfunc: true */
              a[field].map(function(item) {
                if (tmpArray.indexOf(item) === -1) {
                  tmpArray.push(item);
                }
              });
              a[field] = tmpArray;
              b[field] = tmpArray;
            }
          }
          if (a.confidence && b.confidence) {
            a.confidence = (parseFloat(a.confidence, 10) + parseFloat(b.confidence, 10)) / 2;
            b.confidence = a.confidence;
          }
          if (a.allomorphs || b.allomorphs) {
            var allomorphs = a.allomorphs ? a.allomorphs : "";
            allomorphs = allomorphs + allomorphs ? ", " : "";
            allomorphs = allomorphs + b.allomorphs ? b.allomorphs : "";
            a.allomorphs = allomorphs;
            b.allomorphs = allomorphs;
          }
        }
        return equal;
      }
    },
    expandedIGTFields: {
      value: ["morphemes", "gloss", "allomorphy", "phonetic", "orthography"]
    },
    combineFieldsIfEqual: {
      value: ["datumids", "utteranceContext"]
    },
    sortBy: {
      value: "morphemes",
      writable: true
    },
    compare: {
      value: function(b) {
        var a = this;
        var result = 0;
        if (!b || !b.igt || !b.igt[this.sortBy]) {
          return -1;
        }
        if (!a || !a.igt || !a.igt[this.sortBy]) {
          return 1;
        }
        if ((typeof(a.igt[this.sortBy]) === 'number') && (typeof(b.igt[this.sortBy]) === 'number')) {
          result = a.igt[this.sortBy] - b.igt[this.sortBy];
        } else if ((typeof(a.igt[this.sortBy]) === 'string') && (typeof(b.igt[this.sortBy]) === 'string')) {
          if (a.igt[this.sortBy] < b.igt[this.sortBy]) {
            result = -1;
          } else if (a.igt[this.sortBy] > b.igt[this.sortBy]) {
            result = 1;
          } else {
            result = 0;
          }
        } else if (typeof(a.igt[this.sortBy]) === 'string') {
          result = 1;
        } else {
          result = -1;
        }
        return result;
      }
    },
    clean: {
      value: function() {
        console.log("Preparing datum with this lexical entry to be cleaned...");
        var deffered = Q.defer();
        this.cleanedData = [];
        var promises = [];
        var self = this;
        this.proposedChanges = [];

        var successFunction = function(doc) {
          console.log(doc);
          doc.datumFields.map(function(datumField) {
            if (self.igtBeforeCleaning[datumField.label] !== self.igt[datumField.label] && (new RegExp(self.igtBeforeCleaning[datumField.label], "i")).test(datumField.mask)) {
              var change = {
                before: datumField.mask + ""
              };
              // TODO this makes things lower case... because part of the map reduce seems to be doing that...
              datumField.mask = datumField.mask.replace(new RegExp(self.igtBeforeCleaning[datumField.label], "ig"), self.igt[datumField.label]);
              datumField.value = datumField.mask;
              change.after = datumField.mask;
              self.proposedChanges.push(change);
              return datumField;
            }
          });
          self.cleanedData.push(doc);
        };
        var failFunction = function(reason) {
          console.log(reason);
        };

        for (var idIndex = 0; idIndex < this.datumids.length; idIndex++) {
          console.log(this.datumids.length[idIndex]);
          promises[idIndex] = CORS.makeCORSRequest({
            method: 'GET',
            dataType: 'json',
            url: this.url + "/" + this.datumids[idIndex]
          });
          promises[idIndex].then(successFunction).fail(failFunction);
        }
        Q.allSettled(promises).then(function(results) {
          deffered.resolve(self.proposedChanges);
        });
        return deffered.promise;
      }
    },
    save: {
      value: function() {
        var deffered = Q.defer(),
          promises = [];

        console.log("Saving cleaned datum...");
        while (this.cleanedData.length > 0) {
          var cleanedDatum = this.cleanedData.pop();
          promises.push(CORS.makeCORSRequest({
            method: 'PUT',
            dataType: 'json',
            data: cleanedDatum,
            url: this.url + "/" + cleanedDatum._id
          }));
        }
        Q.allSettled(promises).then(function(results) {
          console.log("Saving results: ", results);
          deffered.resolve(results);
        });
        return deffered.promise;
      }
    }
  });

  var Lexicon = function(values, equals, compare, getDefault) {
    console.log("\tConstructing Lexicon... ");
    // SortedSet.apply(this, [values, equals, compare, getDefault]);
    SortedSet.apply(this, Array.prototype.slice.call(arguments, 1));
    // if (!compare) {
    //   this.contentCompare = this.__proto__.igtCompare;
    // }
    // if (!equals) {
    //   this.contentEquals = this.__proto__.igtEqual;
    // }
  };

  Lexicon.prototype = Object.create(SortedSet.prototype, {
    constructor: {
      value: Lexicon
    },
    INTERNAL_MODELS: {
      value: {
        item: LexiconNode
      }
    },
    sortBy: {
      value: "morphemes"
    },

    // toJSON: {
    //   value: function() {
    //     return JSON.stringify(this.toObject(), null, 2);
    //   }
    // },
    getLexicalEntries: {
      value: function(lexicalEntryToMatch) {
        var deffered = Q.defer(),
          matches = [],
          self = this;

        if (!lexicalEntryToMatch) {
          deffered.resolve(matches);
        } else {
          var results = this.find(lexicalEntryToMatch);
          results.map(function(value){
            console.log(value.id + " of " + self.length);
            if (typeof lexicalEntryToMatch.equals === "function") {
              if (lexicalEntryToMatch.equals(value)) {
                matches.push(value);
                console.log(value);
              }
            } else {
              var howWellDoesThisMatch = 0;
              lexicalEntryToMatch = lexicalEntryToMatch.trim();
              for (var attr in value.igt) {
                if (value.igt.hasOwnProperty(attr) && value.igt[attr] === lexicalEntryToMatch) {
                  howWellDoesThisMatch = howWellDoesThisMatch + 1;
                }
              }
              if (howWellDoesThisMatch > 0) {
                matches.push(value);
                console.log(value);
              }
            }
          });
          deffered.resolve(matches);
        }
        return deffered.promise;
      }
    },

    guessContextSensitiveGlosses: {
      value: function(datum) {

        if (!datum.morphemes) {
          console.warn("There was no morphemes line to guess the gloss from...");
          return datum;
        }
        var glossGroups = [];
        var matchingNodes = [];
        var morphemeToFind = "";
        var morphemeGroup = datum.morphemes.split(/ +/);
        var matchingfunction = function(node) {
          if (node.igt.morphemes === morphemeToFind) {
            console.log(node);
            matchingNodes.push(node);
          }
        };
        for (var group in morphemeGroup) {
          var morphemes = morphemeGroup[group].split("-");
          var glosses = [];
          for (var m in morphemes) {
            if (!morphemes.hasOwnProperty(m)) {
              continue;
            }
            matchingNodes = [];
            morphemeToFind = morphemes[m];
            this.filter(matchingfunction);

            var gloss = "?"; // If there's no matching gloss, use question marks
            if (matchingNodes && matchingNodes.length > 0) {
              // Take the first gloss for this morpheme
              console.log("Glosses which match: " + morphemes[m], matchingNodes);
              try {
                gloss = matchingNodes[0].igt.gloss;
              } catch (e) {
                console.log(matchingNodes);
              }
            }
            glosses.push(gloss);
          }

          glossGroups.push(glosses.join("-"));
        }
        datum.glossAlternates = datum.glossAlternates ? datum.glossAlternates.concat(glossGroups) : glossGroups;
        datum.gloss = glossGroups.join(" ");
        // Replace the gloss line with the guessed glosses
        return datum;
      }
    },

    /**
     * Takes as a parameters an array of this.precedenceRelations which came from CouchDB precedence rule query.
     * Example Rule: {"key":{"x":"@","relation":"preceeds","y":"aqtu","context":"aqtu-nay-wa-n"},"value":2}
     */
    generatePrecedenceForceDirectedRulesJsonForD3: {
      value: function(dontConnectWordBoundaries) {
        /*
         * Cycle through the precedence rules, convert them into graph edges with the morpheme index in the morpheme array as the source/target values
         */
        var morphemeLinks = [];
        var morphemes = {};

        /*
         * Create the JSON required by D3
         */
        var precedenceGraph = {
          links: morphemeLinks,
          nodes: morphemes
        };
        this.precedenceGraph = precedenceGraph;

        return precedenceGraph;
      }
    }
  });

  var LexiconFactory = function(options) {
    // var lex = new Lexicon(null, Lexicon.prototype.igtEqual, Lexicon.prototype.igtCompare);
    var lex = new Lexicon();
    lex.precedenceRelations = new UniqueSet();
    lex.references = new UniqueSet();
    if (options.precedenceRelations && options.precedenceRelations.length > 0) {
      for (var i in options.precedenceRelations) {
        try {
          //Add source target and value to the link
          delete options.precedenceRelations[i].key.previous.utterance;
          delete options.precedenceRelations[i].key.subsequent.utterance;
          options.precedenceRelations[i].key.utteranceContext = options.precedenceRelations[i].key.context.utterance;
          options.precedenceRelations[i].key.datumid = options.precedenceRelations[i].id;

          // Put the previous and subsequent morphemes into the morpheme nodes
          // lex.add(options.precedenceRelations[i].key.context.utterance, new LexiconNode({
          //   igt: options.precedenceRelations[i].key.previous
          // }));
          lex.add(new LexiconNode({
            igt: options.precedenceRelations[i].key.previous,
            datumids: [options.precedenceRelations[i].key.context.id],
            utteranceContext: [options.precedenceRelations[i].key.context.utterance],
            url: options.url
          }));

          // lex.add(options.precedenceRelations[i].key.context.utterance, new LexiconNode({
          //   igt: options.precedenceRelations[i].key.previous
          // }));
          lex.add(new LexiconNode({
            igt: options.precedenceRelations[i].key.subsequent,
            datumids: [options.precedenceRelations[i].key.context.id],
            utteranceContext: [options.precedenceRelations[i].key.context.utterance],
            url: options.url
          }));
          lex.references.add(options.precedenceRelations[i].key.context.id);

          //To avoid loops
          if (options.precedenceRelations[i].key.subsequent.morphemes.indexOf("@") === -1) {
            lex.precedenceRelations.add(options.precedenceRelations[i].key);
          }

        } catch (e) {
          console.warn(e);
        }

      }
    }

    if (options.orthography) {
      if (options.nonContentWordsArray) {
        options.userSpecifiedNonContentWords = true;
        if (Object.prototype.toString.call(options.nonContentWordsArray) === '[object Array]' && options.nonContentWordsArray.length === 0) {
          options.userSpecifiedNonContentWords = false;
          console.log("User sent an empty array of non content words, attempting to automatically detect them");
        }
        // else if (options.nonContentWordsArray.trim && !options.nonContentWordsArray.trim()) {
        //   options.userSpecifiedNonContentWords = false;
        // }
      }
      NonContentWords.processNonContentWords(options);

      for (var wordIndex in options.wordFrequencies) {
        if (!options.wordFrequencies.hasOwnProperty(wordIndex)) {
          continue;
        }
        var word = options.wordFrequencies[wordIndex].orthography;
        var count = options.wordFrequencies[wordIndex].count;
        var categories = options.wordFrequencies[wordIndex].categories;
        if (lex.length > maxLexiconSize) {
          continue;
        }
        lex.add(new LexiconNode({
          igt: {
            orthography: word,
            utterance: word,
            morphemes: word,
            gloss: word,
          },
          categories: categories,
          datumids: [options._id],
          count: count,
          utteranceContext: [word],
          url: options.url
        }));

      }
    }

    for (var property in options) {
      if (options.hasOwnProperty(property)) {
        lex[property] = options[property];
      }
    }
    return lex;
  };
  Lexicon.NonContentWords = NonContentWords;
  Lexicon.LexemeFrequency = LexemeFrequency;
  Lexicon.LexiconNode = LexiconNode;
  Lexicon.LexiconFactory = LexiconFactory;
  Lexicon.MorphemeSegmenter = MorphemeSegmenter;

  exports.Lexicon = Lexicon;
  try {
    global.Lexicon = Lexicon;
    global.LexiconFactory = LexiconFactory;
  } catch (e) {
    console.log(e);
  }

  exports.LexiconFactory = LexiconFactory;

  // }(typeof exports === 'object' && exports || this));
})(typeof exports === 'undefined' ? this : exports);<|endoftext|>