<commit_before>/**
 * Copyright 2017 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {Services} from '../../../src/services';
import {Toast} from './toast';
import {
  copyTextToClipboard,
  isCopyingToClipboardSupported,
} from '../../../src/clipboard';
import {dev, user} from '../../../src/log';
import {dict} from './../../../src/utils/object';
import {isObject} from '../../../src/types';
import {listen} from '../../../src/event-helper';
import {renderAsElement, renderSimpleTemplate} from './simple-template';
import {scopedQuerySelector, scopedQuerySelectorAll} from '../../../src/dom';
import {px, setImportantStyles} from '../../../src/style';
import {throttle} from '../../../src/utils/rate-limit';

/**
 * Maps share provider type to visible name.
 * If the name only needs to be capitalized (e.g. `facebook` to `Facebook`) it
 * does not need to be included here.
 * @const {!JsonObject}
 */
const SHARE_PROVIDER_NAME = dict({
  'gplus': 'Google+',
  'linkedin': 'LinkedIn',
  'system': 'More',
  'whatsapp': 'WhatsApp',
});

/**
 * Default left/right padding for share buttons.
 * @private @const {number}
 */
const DEFAULT_BUTTON_PADDING = 16;

/**
 * Minimum left/right padding for share buttons.
 * @private @const {number}
 */
const MIN_BUTTON_PADDING = 10;

/** @private @const {!./simple-template.ElementDef} */
const TEMPLATE = {
  tag: 'div',
  attrs: dict({'class': 'i-amphtml-story-share-widget'}),
  children: [{
    tag: 'ul',
    attrs: dict({'class': 'i-amphtml-story-share-list'}),
    children: [
      {
        tag: 'li',
        attrs: dict({'class': 'i-amphtml-story-share-system'}),
      },
    ],
  }],
};

/** @private @const {!./simple-template.ElementDef} */
const SHARE_ITEM_TEMPLATE = {
  tag: 'li',
  attrs: dict({'class': 'i-amphtml-story-share-item'}),
};

/** @private @const {!./simple-template.ElementDef} */
const LINK_SHARE_ITEM_TEMPLATE = {
  tag: 'div',
  attrs: dict({
    'class':
        'i-amphtml-story-share-icon i-amphtml-story-share-icon-link',
  }),
  text: 'Get Link', // TODO(alanorozco): i18n
};

/** @private @const {string} */
const SCROLLABLE_CLASSNAME = 'i-amphtml-story-share-widget-scrollable';

/**
 * @param {!JsonObject=} opt_params
 * @return {!JsonObject}
 */
function buildProviderParams(opt_params) {
  const attrs = dict();

  if (opt_params) {
    Object.keys(opt_params || {}).forEach(field => {
      attrs[`data-param-${field}`] = opt_params[field];
    });
  }

  return attrs;
}

/**
 * @param {!Document} doc
 * @param {string} shareType
 * @param {!JsonObject=} opt_params
 * @return {!Node}
 */
function buildProvider(doc, shareType, opt_params) {
  return renderSimpleTemplate(doc,
      /** @type {!Array<!./simple-template.ElementDef>} */ ([
        {
          tag: 'amp-social-share',
          attrs: /** @type {!JsonObject} */ (Object.assign(
              dict({
                'width': 48,
                'height': 66,
                'class': 'i-amphtml-story-share-icon',
                'type': shareType,
              }),
              buildProviderParams(opt_params))),
          text: SHARE_PROVIDER_NAME[shareType] || shareType,
        },
      ]));
}

/**
 * @param {!Document} doc
 * @param {string} url
 * @return {!Element}
 */
function buildCopySuccessfulToast(doc, url) {
  return renderAsElement(doc, /** @type {!./simple-template.ElementDef} */ ({
    tag: 'div',
    attrs: dict({'class': 'i-amphtml-story-copy-successful'}),
    children: [
      {
        tag: 'div',
        text: 'Link copied!', // TODO(alanorozco): i18n
      },
      {
        tag: 'div',
        attrs: dict({'class': 'i-amphtml-story-copy-url'}),
        text: url,
      },
    ],
  }));
}

/**
 * Social share widget for story bookend.
 */
export class ShareWidget {
  /** @param {!Window} win */
  constructor(win) {
    /** @private {?../../../src/service/ampdoc-impl.AmpDoc} */
    this.ampdoc_ = null;

    /** @private @const {!Window} */
    this.win_ = win;

    /** @private {?Element} */
    this.root_ = null;
  }

  /** @param {!Window} win */
  static create(win) {
    return new ShareWidget(win);
  }

  /**
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {!Element}
   */
  build(ampdoc) {
    dev().assert(!this.root_, 'Already built.');

    this.ampdoc_ = ampdoc;

    this.root_ = renderAsElement(this.win_.document, TEMPLATE);

    this.maybeAddLinkShareButton_();
    this.maybeAddSystemShareButton_();

    return this.root_;
  }

  /** @private */
  maybeAddLinkShareButton_() {
    if (!isCopyingToClipboardSupported(this.win_.document)) {
      return;
    }

    const linkShareButton =
        renderAsElement(this.win_.document, LINK_SHARE_ITEM_TEMPLATE);

    this.add_(linkShareButton);

    // TODO(alanorozco): Listen for proper tap event (i.e. fastclick)
    listen(linkShareButton, 'click', e => {
      e.preventDefault();
      this.copyUrlToClipboard_();
    });
  }

  /** @private */
  // TODO(alanorozco): i18n for toast.
  copyUrlToClipboard_() {
    const url = Services.documentInfoForDoc(
        /** @type {!../../../src/service/ampdoc-impl.AmpDoc} */ (
          dev().assert(this.ampdoc_))).canonicalUrl;

    if (!copyTextToClipboard(this.win_.document, url)) {
      Toast.show(this.win_, 'Could not copy link to clipboard :(');
      return;
    }

    Toast.show(this.win_, buildCopySuccessfulToast(this.win_.document, url));
  }

  /** @private */
  maybeAddSystemShareButton_() {
    if (!this.isSystemShareSupported_()) {
      // `amp-social-share` will hide `system` buttons when not supported, but
      // we also need to avoid adding it for rendering reasons.
      return;
    }

    const container = scopedQuerySelector(
        dev().assertElement(this.root_),
        '.i-amphtml-story-share-system');

    container.appendChild(buildProvider(this.win_.document, 'system'));
  }

  /** @private */
  // NOTE(alanorozco): This is a duplicate of the logic in the
  // `amp-social-share` component.
  isSystemShareSupported_() {
    const viewer = Services.viewerForDoc(
        /** @type {!../../../src/service/ampdoc-impl.AmpDoc} */ (
          dev().assert(this.ampdoc_)));

    const platform = Services.platformFor(this.win_);

    // Chrome exports navigator.share in WebView but does not implement it.
    // See https://bugs.chromium.org/p/chromium/issues/detail?id=765923
    const isChromeWebview = viewer.isWebviewEmbedded() && platform.isChrome();

    return ('share' in navigator) && !isChromeWebview;
  }

  /**
   * @param {!Object<string, (!JsonObject|boolean)>} providers
   * @public
   */
  // TODO(alanorozco): Set story metadata in share config
  setProviders(providers) {
    this.loadRequiredExtensions_();

    Object.keys(providers).forEach(type => {
      if (type == 'system') {
        user().warn('AMP-STORY',
            '`system` is not a valid share provider type. Native sharing is ' +
            'enabled by default and cannot be turned off.',
            type);
        return;
      }

      if (isObject(providers[type])) {
        this.add_(buildProvider(this.win_.document, type,
            /** @type {!JsonObject} */ (providers[type])));
        return;
      }

      // Bookend config API requires real boolean, not just truthy
      if (providers[type] === true) {
        this.add_(buildProvider(this.win_.document, type));
        return;
      }

      user().warn('AMP-STORY',
          'Invalid amp-story bookend share configuration for %s. ' +
          'Value must be `true` or a params object.',
          type);
    });
  }

  /** @private */
  loadRequiredExtensions_() {
    const ampdoc = /** @type {!../../../src/service/ampdoc-impl.AmpDoc} */ (
      dev().assert(this.ampdoc_));

    Services.extensionsFor(this.win_)
        .installExtensionForDoc(ampdoc, 'amp-social-share');
  }

  /**
   * @param {!Node} node
   * @private
   */
  add_(node) {
    const list = dev().assert(this.root_).firstElementChild;
    const item = renderAsElement(this.win_.document, SHARE_ITEM_TEMPLATE);

    item.appendChild(node);

    // `lastElementChild` is the system share button container, which should
    // always be last in list
    list.insertBefore(item, list.lastElementChild);
  }
}

/**
 * Social share widget for story bookend with a scrollable layout.
 * This class is coupled to the DOM structure for ShareWidget, but that's ok.
 */
export class ScrollableShareWidget {
  /** @param {!Window} win */
  constructor(win) {
    /** @private @const {!Window} */
    this.win_ = win;

    /** @private @const {!../../../src/service/vsync-impl.Vsync} */
    this.vsync_ = Services.vsyncFor(win);

    /** @private @const {!ShareWidget} */
    this.mixin_ = ShareWidget.create(win);

    /**
     * Container width is being tracked to prevent unnecessary layout
     * calculations.
     * @private {?number}
     */
    this.containerWidth_ = null;

    /** @private {?Element} */
    this.root_ = null;
  }

  /** @param {!Window} win */
  static create(win) {
    return new ScrollableShareWidget(win);
  }

  /**
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {!Element}
   */
  build(ampdoc) {
    this.root_ = this.mixin_.build(ampdoc);

    this.root_.classList.add(SCROLLABLE_CLASSNAME);

    Services.viewportForDoc(ampdoc).onResize(
        // we don't require a lot of smoothness here, so we throttle
        throttle(this.win_, () => this.applyButtonPadding_(), 100));

    return this.root_;
  }

  /**
   * Calculates padding between buttons so that the result is that there's
   * always one item visually "cut off" for scroll affordance.
   * @private
   */
  applyButtonPadding_() {
    const items = this.getVisibleItems_();

    if (!items.length) {
      return;
    }

    this.vsync_.run({
      measure: state => {
        const containerWidth = this.root_./*OK*/clientWidth;

        if (containerWidth == this.containerWidth_) {
          // Don't recalculate if width has not changed (i.e. onscreen keyboard)
          state.noop = true;
          return;
        }

        const icon = dev().assert(items[0].firstElementChild);

        const leftMargin = icon./*OK*/offsetLeft - this.root_./*OK*/offsetLeft;
        const iconWidth = icon./*OK*/offsetWidth;

        // Total width that the buttons will occupy with minimum padding.
        const totalItemWidth =
            (iconWidth * items.length + 2 * MIN_BUTTON_PADDING *
                (items.length - 1));

        // If buttons don't fit within the available area, calculate padding so
        // that there will be an element cut-off.
        if (totalItemWidth > (containerWidth - leftMargin * 2)) {
          const availableWidth = containerWidth - leftMargin - iconWidth / 2;
          const amountVisible =
              Math.floor(availableWidth / (iconWidth + MIN_BUTTON_PADDING * 2));

          state.padding = 0.5 * (availableWidth / amountVisible - iconWidth);
        } else {
          // Otherwise, calculate padding in from MIN_PADDING to DEFAULT_PADDING
          // so that all elements fit and take as much area as possible.
          const totalPadding =
              ((containerWidth - leftMargin * 2) - iconWidth * items.length) /
              (items.length - 1);

          state.padding = Math.min(DEFAULT_BUTTON_PADDING, 0.5 * totalPadding);
        }

        this.containerWidth_ = containerWidth;
      },
      mutate: state => {
        if (state.noop) {
          return;
        }
        items.forEach((el, i) => {
          if (i != 0) {
            setImportantStyles(el, {'padding-left': px(state.padding)});
          }
          if (i != items.length - 1) {
            setImportantStyles(el, {'padding-right': px(state.padding)});
          }
        });
      },
    }, {});
  }

  /**
   * @return {!Array<!Element>}
   * @private
   */
  getVisibleItems_() {
    return Array.prototype.filter.call(
        scopedQuerySelectorAll(dev().assertElement(this.root_), 'li'),
        el => !!el.firstElementChild);
  }

  /**
   * @param {!Object<string, (!JsonObject|boolean)>} providers
   * @public
   */
  setProviders(providers) {
    this.mixin_.setProviders(providers);
    this.applyButtonPadding_();
  }
}
<commit_msg>Load amp-social-share when system share is supported. (#12812)<commit_after>/**
 * Copyright 2017 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {Services} from '../../../src/services';
import {Toast} from './toast';
import {
  copyTextToClipboard,
  isCopyingToClipboardSupported,
} from '../../../src/clipboard';
import {dev, user} from '../../../src/log';
import {dict} from './../../../src/utils/object';
import {isObject} from '../../../src/types';
import {listen} from '../../../src/event-helper';
import {renderAsElement, renderSimpleTemplate} from './simple-template';
import {scopedQuerySelector, scopedQuerySelectorAll} from '../../../src/dom';
import {px, setImportantStyles} from '../../../src/style';
import {throttle} from '../../../src/utils/rate-limit';

/**
 * Maps share provider type to visible name.
 * If the name only needs to be capitalized (e.g. `facebook` to `Facebook`) it
 * does not need to be included here.
 * @const {!JsonObject}
 */
const SHARE_PROVIDER_NAME = dict({
  'gplus': 'Google+',
  'linkedin': 'LinkedIn',
  'system': 'More',
  'whatsapp': 'WhatsApp',
});

/**
 * Default left/right padding for share buttons.
 * @private @const {number}
 */
const DEFAULT_BUTTON_PADDING = 16;

/**
 * Minimum left/right padding for share buttons.
 * @private @const {number}
 */
const MIN_BUTTON_PADDING = 10;

/** @private @const {!./simple-template.ElementDef} */
const TEMPLATE = {
  tag: 'div',
  attrs: dict({'class': 'i-amphtml-story-share-widget'}),
  children: [{
    tag: 'ul',
    attrs: dict({'class': 'i-amphtml-story-share-list'}),
    children: [
      {
        tag: 'li',
        attrs: dict({'class': 'i-amphtml-story-share-system'}),
      },
    ],
  }],
};

/** @private @const {!./simple-template.ElementDef} */
const SHARE_ITEM_TEMPLATE = {
  tag: 'li',
  attrs: dict({'class': 'i-amphtml-story-share-item'}),
};

/** @private @const {!./simple-template.ElementDef} */
const LINK_SHARE_ITEM_TEMPLATE = {
  tag: 'div',
  attrs: dict({
    'class':
        'i-amphtml-story-share-icon i-amphtml-story-share-icon-link',
  }),
  text: 'Get Link', // TODO(alanorozco): i18n
};

/** @private @const {string} */
const SCROLLABLE_CLASSNAME = 'i-amphtml-story-share-widget-scrollable';

/**
 * @param {!JsonObject=} opt_params
 * @return {!JsonObject}
 */
function buildProviderParams(opt_params) {
  const attrs = dict();

  if (opt_params) {
    Object.keys(opt_params || {}).forEach(field => {
      attrs[`data-param-${field}`] = opt_params[field];
    });
  }

  return attrs;
}

/**
 * @param {!Document} doc
 * @param {string} shareType
 * @param {!JsonObject=} opt_params
 * @return {!Node}
 */
function buildProvider(doc, shareType, opt_params) {
  return renderSimpleTemplate(doc,
      /** @type {!Array<!./simple-template.ElementDef>} */ ([
        {
          tag: 'amp-social-share',
          attrs: /** @type {!JsonObject} */ (Object.assign(
              dict({
                'width': 48,
                'height': 66,
                'class': 'i-amphtml-story-share-icon',
                'type': shareType,
              }),
              buildProviderParams(opt_params))),
          text: SHARE_PROVIDER_NAME[shareType] || shareType,
        },
      ]));
}

/**
 * @param {!Document} doc
 * @param {string} url
 * @return {!Element}
 */
function buildCopySuccessfulToast(doc, url) {
  return renderAsElement(doc, /** @type {!./simple-template.ElementDef} */ ({
    tag: 'div',
    attrs: dict({'class': 'i-amphtml-story-copy-successful'}),
    children: [
      {
        tag: 'div',
        text: 'Link copied!', // TODO(alanorozco): i18n
      },
      {
        tag: 'div',
        attrs: dict({'class': 'i-amphtml-story-copy-url'}),
        text: url,
      },
    ],
  }));
}

/**
 * Social share widget for story bookend.
 */
export class ShareWidget {
  /** @param {!Window} win */
  constructor(win) {
    /** @private {?../../../src/service/ampdoc-impl.AmpDoc} */
    this.ampdoc_ = null;

    /** @private @const {!Window} */
    this.win_ = win;

    /** @private {?Element} */
    this.root_ = null;
  }

  /** @param {!Window} win */
  static create(win) {
    return new ShareWidget(win);
  }

  /**
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {!Element}
   */
  build(ampdoc) {
    dev().assert(!this.root_, 'Already built.');

    this.ampdoc_ = ampdoc;

    this.root_ = renderAsElement(this.win_.document, TEMPLATE);

    this.maybeAddLinkShareButton_();
    this.maybeAddSystemShareButton_();

    return this.root_;
  }

  /** @private */
  maybeAddLinkShareButton_() {
    if (!isCopyingToClipboardSupported(this.win_.document)) {
      return;
    }

    const linkShareButton =
        renderAsElement(this.win_.document, LINK_SHARE_ITEM_TEMPLATE);

    this.add_(linkShareButton);

    // TODO(alanorozco): Listen for proper tap event (i.e. fastclick)
    listen(linkShareButton, 'click', e => {
      e.preventDefault();
      this.copyUrlToClipboard_();
    });
  }

  /** @private */
  // TODO(alanorozco): i18n for toast.
  copyUrlToClipboard_() {
    const url = Services.documentInfoForDoc(
        /** @type {!../../../src/service/ampdoc-impl.AmpDoc} */ (
          dev().assert(this.ampdoc_))).canonicalUrl;

    if (!copyTextToClipboard(this.win_.document, url)) {
      Toast.show(this.win_, 'Could not copy link to clipboard :(');
      return;
    }

    Toast.show(this.win_, buildCopySuccessfulToast(this.win_.document, url));
  }

  /** @private */
  maybeAddSystemShareButton_() {
    if (!this.isSystemShareSupported_()) {
      // `amp-social-share` will hide `system` buttons when not supported, but
      // we also need to avoid adding it for rendering reasons.
      return;
    }

    const container = scopedQuerySelector(
        dev().assertElement(this.root_),
        '.i-amphtml-story-share-system');

    this.loadRequiredExtensions_();

    container.appendChild(buildProvider(this.win_.document, 'system'));
  }

  /** @private */
  // NOTE(alanorozco): This is a duplicate of the logic in the
  // `amp-social-share` component.
  isSystemShareSupported_() {
    const viewer = Services.viewerForDoc(
        /** @type {!../../../src/service/ampdoc-impl.AmpDoc} */ (
          dev().assert(this.ampdoc_)));

    const platform = Services.platformFor(this.win_);

    // Chrome exports navigator.share in WebView but does not implement it.
    // See https://bugs.chromium.org/p/chromium/issues/detail?id=765923
    const isChromeWebview = viewer.isWebviewEmbedded() && platform.isChrome();

    return ('share' in navigator) && !isChromeWebview;
  }

  /**
   * @param {!Object<string, (!JsonObject|boolean)>} providers
   * @public
   */
  // TODO(alanorozco): Set story metadata in share config
  setProviders(providers) {
    this.loadRequiredExtensions_();

    Object.keys(providers).forEach(type => {
      if (type == 'system') {
        user().warn('AMP-STORY',
            '`system` is not a valid share provider type. Native sharing is ' +
            'enabled by default and cannot be turned off.',
            type);
        return;
      }

      if (isObject(providers[type])) {
        this.add_(buildProvider(this.win_.document, type,
            /** @type {!JsonObject} */ (providers[type])));
        return;
      }

      // Bookend config API requires real boolean, not just truthy
      if (providers[type] === true) {
        this.add_(buildProvider(this.win_.document, type));
        return;
      }

      user().warn('AMP-STORY',
          'Invalid amp-story bookend share configuration for %s. ' +
          'Value must be `true` or a params object.',
          type);
    });
  }

  /** @private */
  loadRequiredExtensions_() {
    const ampdoc = /** @type {!../../../src/service/ampdoc-impl.AmpDoc} */ (
      dev().assert(this.ampdoc_));

    Services.extensionsFor(this.win_)
        .installExtensionForDoc(ampdoc, 'amp-social-share');
  }

  /**
   * @param {!Node} node
   * @private
   */
  add_(node) {
    const list = dev().assert(this.root_).firstElementChild;
    const item = renderAsElement(this.win_.document, SHARE_ITEM_TEMPLATE);

    item.appendChild(node);

    // `lastElementChild` is the system share button container, which should
    // always be last in list
    list.insertBefore(item, list.lastElementChild);
  }
}

/**
 * Social share widget for story bookend with a scrollable layout.
 * This class is coupled to the DOM structure for ShareWidget, but that's ok.
 */
export class ScrollableShareWidget {
  /** @param {!Window} win */
  constructor(win) {
    /** @private @const {!Window} */
    this.win_ = win;

    /** @private @const {!../../../src/service/vsync-impl.Vsync} */
    this.vsync_ = Services.vsyncFor(win);

    /** @private @const {!ShareWidget} */
    this.mixin_ = ShareWidget.create(win);

    /**
     * Container width is being tracked to prevent unnecessary layout
     * calculations.
     * @private {?number}
     */
    this.containerWidth_ = null;

    /** @private {?Element} */
    this.root_ = null;
  }

  /** @param {!Window} win */
  static create(win) {
    return new ScrollableShareWidget(win);
  }

  /**
   * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
   * @return {!Element}
   */
  build(ampdoc) {
    this.root_ = this.mixin_.build(ampdoc);

    this.root_.classList.add(SCROLLABLE_CLASSNAME);

    Services.viewportForDoc(ampdoc).onResize(
        // we don't require a lot of smoothness here, so we throttle
        throttle(this.win_, () => this.applyButtonPadding_(), 100));

    return this.root_;
  }

  /**
   * Calculates padding between buttons so that the result is that there's
   * always one item visually "cut off" for scroll affordance.
   * @private
   */
  applyButtonPadding_() {
    const items = this.getVisibleItems_();

    if (!items.length) {
      return;
    }

    this.vsync_.run({
      measure: state => {
        const containerWidth = this.root_./*OK*/clientWidth;

        if (containerWidth == this.containerWidth_) {
          // Don't recalculate if width has not changed (i.e. onscreen keyboard)
          state.noop = true;
          return;
        }

        const icon = dev().assert(items[0].firstElementChild);

        const leftMargin = icon./*OK*/offsetLeft - this.root_./*OK*/offsetLeft;
        const iconWidth = icon./*OK*/offsetWidth;

        // Total width that the buttons will occupy with minimum padding.
        const totalItemWidth =
            (iconWidth * items.length + 2 * MIN_BUTTON_PADDING *
                (items.length - 1));

        // If buttons don't fit within the available area, calculate padding so
        // that there will be an element cut-off.
        if (totalItemWidth > (containerWidth - leftMargin * 2)) {
          const availableWidth = containerWidth - leftMargin - iconWidth / 2;
          const amountVisible =
              Math.floor(availableWidth / (iconWidth + MIN_BUTTON_PADDING * 2));

          state.padding = 0.5 * (availableWidth / amountVisible - iconWidth);
        } else {
          // Otherwise, calculate padding in from MIN_PADDING to DEFAULT_PADDING
          // so that all elements fit and take as much area as possible.
          const totalPadding =
              ((containerWidth - leftMargin * 2) - iconWidth * items.length) /
              (items.length - 1);

          state.padding = Math.min(DEFAULT_BUTTON_PADDING, 0.5 * totalPadding);
        }

        this.containerWidth_ = containerWidth;
      },
      mutate: state => {
        if (state.noop) {
          return;
        }
        items.forEach((el, i) => {
          if (i != 0) {
            setImportantStyles(el, {'padding-left': px(state.padding)});
          }
          if (i != items.length - 1) {
            setImportantStyles(el, {'padding-right': px(state.padding)});
          }
        });
      },
    }, {});
  }

  /**
   * @return {!Array<!Element>}
   * @private
   */
  getVisibleItems_() {
    return Array.prototype.filter.call(
        scopedQuerySelectorAll(dev().assertElement(this.root_), 'li'),
        el => !!el.firstElementChild);
  }

  /**
   * @param {!Object<string, (!JsonObject|boolean)>} providers
   * @public
   */
  setProviders(providers) {
    this.mixin_.setProviders(providers);
    this.applyButtonPadding_();
  }
}
<|endoftext|>


<commit_before>/*
 * ngTest.js 0.1.2 08-29-2013
 * copyright (c) 2013 Andrew Luetgers
 * you are free to distribute ngTest.js under the MIT license
 * https://github.com/andrewluetgers/ngTest
 */

var ngTest = (function(root) {

	function ngTest(obj, depth, debug) {

		var code = "";
		function list() {
			return Array.prototype.slice.call(arguments, 0);
		}

		var log = ("dump" in root) ? dump : function() {console.log.apply(console, list(arguments));}
		debug = debug || depth === true;
		depth = _.isNumber(depth) ? ++depth : 0;

		// parse the describe/test blocks
		_.each(obj, function(spec, des) {
			var type = isDescribe(des) ? "describe" : "it",
				describeMode = type == "describe";

			code +=						type + "('"+des+"', ";

			if (_.isFunction(spec)) {
				// we have a traditional describe or it function
				code += 				spec + ");";

			} else if (describeMode && _.isArray(spec)) {
				code += 				"function() {";

				// we have an array of possible actions depending on type
				// so lets compile lists of each then code-gen
				var vars = [],
					deps = [],
					mods = [],
					before = [],
					funcs = [],
					after = [],
					tests = [],
					compile = false;

				// compile phase --------------
				_.each(spec, function(val) {
					if (_.isString(val)) {
						// string = a module to load or dependencies to inject
						// lets compile them into lists and in either case we need a before each function
						// supports modules that have the same name as a dependency
						// e.g. titleService, we demarcate modules with a ':' suffix
						// we can indicate the module and the dependency via the prefix "+" or ":+"
						var name = val.replace(/\+$/, "");
						name = name.replace(/:$/, "");

						if (isModuleAndDep(val)) {
							deps.push(name);
							mods.push(name);
						} else if (isVariable(val)) {
							vars.push(val.replace(/=$/, ""));
						} else {
							isModule(val) ? mods.push(name) : deps.push(name);
						}

					} else if (_.isFunction(val)) {
						// function = beforeEach, afterEach or some inline function

						// are we needing the compileDirective util?
						compile = (!compile && (val+"").match("compileWithScope")) ? true : compile;

						if (isNamed(val)) {
							if (isBefore(val)) {
								before.push(val);
							} else if (isAfter(val)) {
								after.push(val);
							} else {
								// named functions not prefixed
								// with before or after get in-lined
								funcs.push(val);
							}
						} else if (!before.length) {
							before.push(val);
						} else if (!after.length) {
							after.push(val);
						}

					} else if (_.isPlainObject(val)) {
						// objects = recur with depth
						tests.push(ngTest(val, depth, debug));
					}
				});

				debug && log(mods, deps, before, after, tests);

				// code gen phase --------------------------------------------------------------------

				// in-line our named functions that are not prefixed with before or after
				funcs.length && (code += "\n\n" + funcs.join("\n\n"));

				// load modules ------------------
				// will generating for something like this
				//	beforeEach(function() {
				//		module('foo');
				//		module('bar');
				//		module('filters');
				//	});

				// if ngExampleApp is loading the test include the ngExampleApp module
				if ("ngExampleApp" in root) {
					mods.unshift("ngExampleApp");
				}

				var modCode = "";
				if (mods.length) {
					modCode = 			"beforeEach(function() {";
					_.each(mods, function(mod) {
						// if running under the ngExampleApp loader skip things that look like templates
						if("ngExampleApp" in root && isTemplate(mod)) {
							modCode += 		" /*\"module('"+mod+"');\" eliminated for compatibility with ngExampleApp loader */";
						} else {
						modCode += 		"module('"+mod+"');";
						}
					});
					code += 			modCode + "});";
				}

				// add in the compileWithScope util deps if needed ------------------

				// we use compile in the nested code?
				compile = compile || _.any(tests, function(code) {
					return !!(code).match("compileWithScope");
				});

				if (compile) {
					// if we need the compileWithScope util lets load up a couple deps for it
					deps.push("$compile", "$rootScope");
					deps = _.uniq(deps);
				}

				// compileWithScope util function added after deps

				// add in our vars ------------------
				if (vars.length) {
					code += 			"var "+ vars +";";
				}

				// inject deps via closure ref ------------------
				// will generate something like this
				// 		var momentAgoFilter;
				//		beforeEach(inject(function(_momentAgoFilter_) {
				//			momentAgoFilter = _momentAgoFilter_;
				//		}));

				var depCode = "";
				if (deps.length) {
					code += 			"var "+ deps +";";
					var injectArgs = _.map(deps, function(d) {
						var dAlt = 		"_"+d+"_";
						depCode += 		d + " = " + dAlt + ";";
						return dAlt;
					});

					code += 			"beforeEach(inject(function("+injectArgs+") {";
					code += 			depCode + "}));";  // end inject code
				}

				// add in the compileWithScope util if needed ------------------
				function compileWithScope(spec) {

					var ret = {};

					// create a scope
					ret.scope = $rootScope.$new();

					// copy provided scope vals to our new scope
					if (spec.scope) {
						angular.extend(ret.scope, spec.scope);
					}

					// get the jqLite or jQuery element
					ret.el = angular.element(spec.html);

					// compile the element into a function to
					// process the view.
					ret.compiled = $compile(ret.el);

					// run the compiled view.
					ret.compiled(ret.scope);

					// call digest on the scope!
					ret.scope.$digest();

					return ret;
				}

				code += compile ? " " +compileWithScope : "/* compileWithScope util not required */";

				// add our deferred injection support
				function beforeInjections_() {
					_.each(testInjections.before, function(fn) {
						if (_.isFunction(fn)) {
							inject(fn);
						}
					});
				}

				function afterInjections_() {
					_.each(testInjections.after, function(fn) {
						if (_.isFunction(fn)) {
							inject(fn);
						}
					});
				}

				if ("testInjections" in root) {
					if (_.isArray(testInjections.before) && testInjections.before.length) {
						before.unshift(beforeInjections_);
					}
					if (_.isArray(testInjections.after) && testInjections.after.length) {
						after.unshift(afterInjections_);
					}
				}

				// add our befores and afters ------------------
				_.each(before, function(fn) {
					code += 			"beforeEach(" + fn + ");";
				});

				_.each(after, function(fn) {
					code += 			"afterEach(" + fn + ");";
				});

				// add all the tests and nested describes ------------------
				_.each(tests, function(testCode) {
					code += 			testCode;
				});

				// all done bitches ------------------
				code += 				"});"; // end describe fn

			} else {
				log("Bad spec!", spec);
				throw new TypeError("expected array or function but saw " + typeof spec);
			}

		});

		if (depth === 0) {
			debug && log("final code", code, depth);
			var fn = new Function(code);
			fn.call(root);
		} else {
			debug && log("returning code", code, depth);
			return code;
		}
	}

	function isTest(str) {
		return stringStartsWith(str.toLowerCase(), "should");
	}

	function isDescribe(str) {
		return !isTest(str);
	}

	function isModule(str) {
		return stringEndsWith(str, ":");
	}

	function isModuleAndDep(str) {
		return stringEndsWith(str, "+");
	}

	function isVariable(str) {
		return stringEndsWith(str, "=");
	}

	function isTemplate(str) {
		return str.match(/template|\.tpl\.html/i);
	}

	function isBefore(fn) {
		return stringStartsWith(fn + "", "function before");
	}

	function isAfter(fn) {
		return stringStartsWith(fn + "", "function after");
	}

	function isNamed(fn) {
		return stringStartsWith(fn + "", "function ");
	}

	function stringStartsWith(haystack, needle) {
		return haystack.substr(0, needle.length) == needle;
	}

	function stringEndsWith(haystack, needle) {
		return haystack.substr(-needle.length) == needle;
	}

	return ngTest;

}(this));<commit_msg>- bumped version<commit_after>/*
 * ngTest.js 0.1.3 08-29-2013
 * copyright (c) 2013 Andrew Luetgers
 * you are free to distribute ngTest.js under the MIT license
 * https://github.com/andrewluetgers/ngTest
 */

var ngTest = (function(root) {

	function ngTest(obj, depth, debug) {

		var code = "";
		function list() {
			return Array.prototype.slice.call(arguments, 0);
		}

		var log = ("dump" in root) ? dump : function() {console.log.apply(console, list(arguments));}
		debug = debug || depth === true;
		depth = _.isNumber(depth) ? ++depth : 0;

		// parse the describe/test blocks
		_.each(obj, function(spec, des) {
			var type = isDescribe(des) ? "describe" : "it",
				describeMode = type == "describe";

			code +=						type + "('"+des+"', ";

			if (_.isFunction(spec)) {
				// we have a traditional describe or it function
				code += 				spec + ");";

			} else if (describeMode && _.isArray(spec)) {
				code += 				"function() {";

				// we have an array of possible actions depending on type
				// so lets compile lists of each then code-gen
				var vars = [],
					deps = [],
					mods = [],
					before = [],
					funcs = [],
					after = [],
					tests = [],
					compile = false;

				// compile phase --------------
				_.each(spec, function(val) {
					if (_.isString(val)) {
						// string = a module to load or dependencies to inject
						// lets compile them into lists and in either case we need a before each function
						// supports modules that have the same name as a dependency
						// e.g. titleService, we demarcate modules with a ':' suffix
						// we can indicate the module and the dependency via the prefix "+" or ":+"
						var name = val.replace(/\+$/, "");
						name = name.replace(/:$/, "");

						if (isModuleAndDep(val)) {
							deps.push(name);
							mods.push(name);
						} else if (isVariable(val)) {
							vars.push(val.replace(/=$/, ""));
						} else {
							isModule(val) ? mods.push(name) : deps.push(name);
						}

					} else if (_.isFunction(val)) {
						// function = beforeEach, afterEach or some inline function

						// are we needing the compileDirective util?
						compile = (!compile && (val+"").match("compileWithScope")) ? true : compile;

						if (isNamed(val)) {
							if (isBefore(val)) {
								before.push(val);
							} else if (isAfter(val)) {
								after.push(val);
							} else {
								// named functions not prefixed
								// with before or after get in-lined
								funcs.push(val);
							}
						} else if (!before.length) {
							before.push(val);
						} else if (!after.length) {
							after.push(val);
						}

					} else if (_.isPlainObject(val)) {
						// objects = recur with depth
						tests.push(ngTest(val, depth, debug));
					}
				});

				debug && log(mods, deps, before, after, tests);

				// code gen phase --------------------------------------------------------------------

				// in-line our named functions that are not prefixed with before or after
				funcs.length && (code += "\n\n" + funcs.join("\n\n"));

				// load modules ------------------
				// will generating for something like this
				//	beforeEach(function() {
				//		module('foo');
				//		module('bar');
				//		module('filters');
				//	});

				// if ngExampleApp is loading the test include the ngExampleApp module
				if ("ngExampleApp" in root) {
					mods.unshift("ngExampleApp");
				}

				var modCode = "";
				if (mods.length) {
					modCode = 			"beforeEach(function() {";
					_.each(mods, function(mod) {
						// if running under the ngExampleApp loader skip things that look like templates
						if("ngExampleApp" in root && isTemplate(mod)) {
							modCode += 		" /*\"module('"+mod+"');\" eliminated for compatibility with ngExampleApp loader */";
						} else {
						modCode += 		"module('"+mod+"');";
						}
					});
					code += 			modCode + "});";
				}

				// add in the compileWithScope util deps if needed ------------------

				// we use compile in the nested code?
				compile = compile || _.any(tests, function(code) {
					return !!(code).match("compileWithScope");
				});

				if (compile) {
					// if we need the compileWithScope util lets load up a couple deps for it
					deps.push("$compile", "$rootScope");
					deps = _.uniq(deps);
				}

				// compileWithScope util function added after deps

				// add in our vars ------------------
				if (vars.length) {
					code += 			"var "+ vars +";";
				}

				// inject deps via closure ref ------------------
				// will generate something like this
				// 		var momentAgoFilter;
				//		beforeEach(inject(function(_momentAgoFilter_) {
				//			momentAgoFilter = _momentAgoFilter_;
				//		}));

				var depCode = "";
				if (deps.length) {
					code += 			"var "+ deps +";";
					var injectArgs = _.map(deps, function(d) {
						var dAlt = 		"_"+d+"_";
						depCode += 		d + " = " + dAlt + ";";
						return dAlt;
					});

					code += 			"beforeEach(inject(function("+injectArgs+") {";
					code += 			depCode + "}));";  // end inject code
				}

				// add in the compileWithScope util if needed ------------------
				function compileWithScope(spec) {

					var ret = {};

					// create a scope
					ret.scope = $rootScope.$new();

					// copy provided scope vals to our new scope
					if (spec.scope) {
						angular.extend(ret.scope, spec.scope);
					}

					// get the jqLite or jQuery element
					ret.el = angular.element(spec.html);

					// compile the element into a function to
					// process the view.
					ret.compiled = $compile(ret.el);

					// run the compiled view.
					ret.compiled(ret.scope);

					// call digest on the scope!
					ret.scope.$digest();

					return ret;
				}

				code += compile ? " " +compileWithScope : "/* compileWithScope util not required */";

				// add our deferred injection support
				function beforeInjections_() {
					_.each(testInjections.before, function(fn) {
						if (_.isFunction(fn)) {
							inject(fn);
						}
					});
				}

				function afterInjections_() {
					_.each(testInjections.after, function(fn) {
						if (_.isFunction(fn)) {
							inject(fn);
						}
					});
				}

				if ("testInjections" in root) {
					if (_.isArray(testInjections.before) && testInjections.before.length) {
						before.unshift(beforeInjections_);
					}
					if (_.isArray(testInjections.after) && testInjections.after.length) {
						after.unshift(afterInjections_);
					}
				}

				// add our befores and afters ------------------
				_.each(before, function(fn) {
					code += 			"beforeEach(" + fn + ");";
				});

				_.each(after, function(fn) {
					code += 			"afterEach(" + fn + ");";
				});

				// add all the tests and nested describes ------------------
				_.each(tests, function(testCode) {
					code += 			testCode;
				});

				// all done bitches ------------------
				code += 				"});"; // end describe fn

			} else {
				log("Bad spec!", spec);
				throw new TypeError("expected array or function but saw " + typeof spec);
			}

		});

		if (depth === 0) {
			debug && log("final code", code, depth);
			var fn = new Function(code);
			fn.call(root);
		} else {
			debug && log("returning code", code, depth);
			return code;
		}
	}

	function isTest(str) {
		return stringStartsWith(str.toLowerCase(), "should");
	}

	function isDescribe(str) {
		return !isTest(str);
	}

	function isModule(str) {
		return stringEndsWith(str, ":");
	}

	function isModuleAndDep(str) {
		return stringEndsWith(str, "+");
	}

	function isVariable(str) {
		return stringEndsWith(str, "=");
	}

	function isTemplate(str) {
		return str.match(/template|\.tpl\.html/i);
	}

	function isBefore(fn) {
		return stringStartsWith(fn + "", "function before");
	}

	function isAfter(fn) {
		return stringStartsWith(fn + "", "function after");
	}

	function isNamed(fn) {
		return stringStartsWith(fn + "", "function ");
	}

	function stringStartsWith(haystack, needle) {
		return haystack.substr(0, needle.length) == needle;
	}

	function stringEndsWith(haystack, needle) {
		return haystack.substr(-needle.length) == needle;
	}

	return ngTest;

}(this));<|endoftext|>


<commit_before>var glob = require('glob');
var path = require('path');
var _ = require('lodash');

module.exports = function(app, config) {
  var seedNodules = [], routes = {};

  var rootConfig = {
    // directories to look for nodules in, minus exclude pattern - looks in nodules directory by default
    dirs: [
      { path: path.join(process.cwd(), 'nodules'), exclude: null }
    ],

    // default debug function if none is supplied
    customDebug: function(identifier) {
      if (defaultConfig.debugToConsole)
        return function(msg) { console.log(identifier+': '+msg); };
      else
        return function(msg) { };
    },

    // set this to true if you have not defined a customDebugger but want to temporality see debugging output
    debugToConsole: false,

    noduleDefaults: {
      // array of (or function which returns array of) middleware functions which will be called in order for each nodule on each express request
      middlewares: [],

      // Note: you can use a function which returns an array of middlewares
      //       to assign different groups of middleware based on certain nodule properties when app inits
      //       but be careful because most nodule properties can be mutated at request time - whereas middleware chains are set up at app init time

      // function example:
      // middlewares: function(nodule) {
      //  if (nodule.routeVerb === 'post') return formMiddleWareArray;  // predefined middleware array just for post requests
      //  else return middleWareArray;                                  // standard middleware array used for everything else
      // },

      // NOTE: the three route config params below must be specified in the nodule init method, they cannot be mutated at request-time

      // REQUIRED, must be unique within express app, can be String or RegExp or an array of either to handle multiple routes
      route: null,

      // get/post/put/delete
      routeVerb: 'get',

      // use to load routes before or after main group (use negative #s for to load route first - like z-index)
      routeIndex: 0,
    },
  };

  var defaultConfig = _.merge(_.cloneDeep(rootConfig), config);
  var debug = defaultConfig.customDebug('nodulejs');
  debug('debug initialized');

  // find all nodules and init all routes first so they can be sorted based on routeIndex
  defaultConfig.dirs.forEach(function(dir) { loadNodules(dir.path, dir.exclude); });

  registerRoutes();

  return {
    defaultConfig: defaultConfig
  };

  // finds nodules in supplied dir, minus exclude patterns, and invokes initNodule method on them
  function loadNodules(dir, exclude) {
    debug('loadNodules called - dir: ' + dir + ', exclude: ' + exclude);
    var root = dir || process.cwd(); // TOOD - should this be process.cwd() + '/app' ?
    glob.sync('./**/*.js', { cwd: root })
      .filter(doesntMatch.apply(this, exclude))
      .forEach(function(file) { initNodule(path.join(root, file)); });
  }

  // creates seedNodules for each found nodule (seedNodules are cloned at the beginning of each request and added to the req object)
  function initNodule(filepath) {
    var nodule = require(filepath)(app);
    var seedNodule = _.merge(_.cloneDeep(defaultConfig.noduleDefaults), nodule); // merge nodule properties onto default nodule
    seedNodule.path = path.dirname(filepath);
    seedNodule.name = path.basename(filepath, '.js');
    seedNodule.debug = defaultConfig.customDebug(seedNodule.name);

    // nodules can have multiple routes
    var routeArray = (typeof seedNodule.route === 'string') || (seedNodule.route instanceof RegExp) ? [seedNodule.route] : seedNodule.route;
    _.each(routeArray, function(routePath) {
      seedNodules[routePath] = seedNodule; // routes must me unique

      // middlewares can be an array of functions, or function that returns an array of functions
      var middlewares = typeof seedNodule.middlewares === 'function' ? seedNodule.middlewares(seedNodule) : seedNodule.middlewares;

      if (!routes[seedNodule.routeIndex]) routes[seedNodule.routeIndex] = [];
      routes[seedNodule.routeIndex].push({path:routePath, verb:seedNodule.routeVerb, middlewares:middlewares});
    });
  }

  // register express routes (call app.get()/app.post() etc.) in order based on nodule.routeIndex (default = 0, can be negative)
  function registerRoutes() {
    var sortedRouteKeys = _.sortBy(_.keys(routes), function(num){ return 1*num; });
    _.each(sortedRouteKeys, function(key) {
      _.each(routes[key], function(route) {
        debug('registering route: ' + route.verb + ' ' + route.path);
        app[route.verb].apply(app, [route.path, initRequest].concat(route.middlewares));
      });
    });
  }

  // first step in middleware chain - clone applicable seedNodule and attach cloned instance to each incoming request
  function initRequest(req, res, next) {
    req.nodule = _.cloneDeep(seedNodules[req.route.path]);
    next();
  }
};

function doesntMatch() {
  var matchers = Array.prototype.slice.call(arguments, 0);
  return function(str) {
    var matches = !matchers.some(function(matcher) {
      return str.indexOf(matcher) !== -1;
    });
    return matches;
  };
}<commit_msg>Change merge to assign to avoid merging middleware arrays, apiCalls array, etc<commit_after>var glob = require('glob');
var path = require('path');
var _ = require('lodash');

module.exports = function(app, config) {
  var seedNodules = [], routes = {};

  var rootConfig = {
    // directories to look for nodules in, minus exclude pattern - looks in nodules directory by default
    dirs: [
      { path: path.join(process.cwd(), 'nodules'), exclude: null }
    ],

    // default debug function if none is supplied
    customDebug: function(identifier) {
      if (defaultConfig.debugToConsole)
        return function(msg) { console.log(identifier+': '+msg); };
      else
        return function(msg) { };
    },

    // set this to true if you have not defined a customDebugger but want to temporality see debugging output
    debugToConsole: false,

    noduleDefaults: {
      // array of (or function which returns array of) middleware functions which will be called in order for each nodule on each express request
      middlewares: [],

      // Note: you can use a function which returns an array of middlewares
      //       to assign different groups of middleware based on certain nodule properties when app inits
      //       but be careful because most nodule properties can be mutated at request time - whereas middleware chains are set up at app init time

      // function example:
      // middlewares: function(nodule) {
      //  if (nodule.routeVerb === 'post') return formMiddleWareArray;  // predefined middleware array just for post requests
      //  else return middleWareArray;                                  // standard middleware array used for everything else
      // },

      // NOTE: the three route config params below must be specified in the nodule init method, they cannot be mutated at request-time

      // REQUIRED, must be unique within express app, can be String or RegExp or an array of either to handle multiple routes
      route: null,

      // get/post/put/delete
      routeVerb: 'get',

      // use to load routes before or after main group (use negative #s for to load route first - like z-index)
      routeIndex: 0,
    },
  };

  var defaultConfig = _.merge(_.cloneDeep(rootConfig), config);
  var debug = defaultConfig.customDebug('nodulejs');
  debug('debug initialized');

  // find all nodules and init all routes first so they can be sorted based on routeIndex
  defaultConfig.dirs.forEach(function(dir) { loadNodules(dir.path, dir.exclude); });

  registerRoutes();

  return {
    defaultConfig: defaultConfig
  };

  // finds nodules in supplied dir, minus exclude patterns, and invokes initNodule method on them
  function loadNodules(dir, exclude) {
    debug('loadNodules called - dir: ' + dir + ', exclude: ' + exclude);
    var root = dir || process.cwd(); // TOOD - should this be process.cwd() + '/app' ?
    glob.sync('./**/*.js', { cwd: root })
      .filter(doesntMatch.apply(this, exclude))
      .forEach(function(file) { initNodule(path.join(root, file)); });
  }

  // creates seedNodules for each found nodule (seedNodules are cloned at the beginning of each request and added to the req object)
  function initNodule(filepath) {
    var nodule = require(filepath)(app);
    var seedNodule = _.assign(_.cloneDeep(defaultConfig.noduleDefaults), nodule); // merge nodule properties onto default nodule
    seedNodule.path = path.dirname(filepath);
    seedNodule.name = path.basename(filepath, '.js');
    seedNodule.debug = defaultConfig.customDebug(seedNodule.name);

    // nodules can have multiple routes
    var routeArray = (typeof seedNodule.route === 'string') || (seedNodule.route instanceof RegExp) ? [seedNodule.route] : seedNodule.route;
    _.each(routeArray, function(routePath) {
      seedNodules[routePath] = seedNodule; // routes must me unique

      // middlewares can be an array of functions, or function that returns an array of functions
      var middlewares = typeof seedNodule.middlewares === 'function' ? seedNodule.middlewares(seedNodule) : seedNodule.middlewares;

      if (!routes[seedNodule.routeIndex]) routes[seedNodule.routeIndex] = [];
      routes[seedNodule.routeIndex].push({path:routePath, verb:seedNodule.routeVerb, middlewares:middlewares});
    });
  }

  // register express routes (call app.get()/app.post() etc.) in order based on nodule.routeIndex (default = 0, can be negative)
  function registerRoutes() {
    var sortedRouteKeys = _.sortBy(_.keys(routes), function(num){ return 1*num; });
    _.each(sortedRouteKeys, function(key) {
      _.each(routes[key], function(route) {
        debug('registering route: ' + route.verb + ' ' + route.path);
        app[route.verb].apply(app, [route.path, initRequest].concat(route.middlewares));
      });
    });
  }

  // first step in middleware chain - clone applicable seedNodule and attach cloned instance to each incoming request
  function initRequest(req, res, next) {
    req.nodule = _.cloneDeep(seedNodules[req.route.path]);
    next();
  }
};

function doesntMatch() {
  var matchers = Array.prototype.slice.call(arguments, 0);
  return function(str) {
    var matches = !matchers.some(function(matcher) {
      return str.indexOf(matcher) !== -1;
    });
    return matches;
  };
}<|endoftext|>


<commit_before>define(['Modernizr', 'domPrefixes'], function( Modernizr, domPrefixes ) {
  Modernizr.addTest('fullscreen',function() {
    for (var i = 0; i < domPrefixes.length; i++) {
      if( document[domPrefixes[i].toLowerCase() + 'CancelFullScreen'])
        return true;
    }
    return !!document['cancelFullScreen'] || false;
  });

  // http://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/ControllingMediaWithJavaScript/ControllingMediaWithJavaScript.html#//apple_ref/doc/uid/TP40009523-CH3-SW20
  // https://developer.mozilla.org/en/API/Fullscreen
});
<commit_msg>Update detect for fullscreen api. Fixes #739<commit_after>define(['Modernizr', 'domPrefixes'], function( Modernizr, domPrefixes ) {
  // http://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/ControllingMediaWithJavaScript/ControllingMediaWithJavaScript.html#//apple_ref/doc/uid/TP40009523-CH3-SW20
  // https://developer.mozilla.org/en/API/Fullscreen
  // github.com/Modernizr/Modernizr/issues/739
  Modernizr.addTest('fullscreen', !!(Modernizr.prefixed("exitFullscreen", document, false) || Modernizr.prefixed("cancelFullScreen", document, false)));
});
<|endoftext|>


<commit_before>(function() {
  'use strict';

  var _controllers = {};

  window.Ractive.controller = function(name, controller) {
    _controllers[name] = _controllers[name] || [];

    if (typeof controller == 'function') {
      _controllers[name].push(controller);
    }
  };

  function _callControllers(controllers, component, data, el, config, i, callback) {
    if (i < controllers.length) {
      controllers[i](component, data, el, config, function done() {
        _callControllers(controllers, component, data, el, config, ++i, callback);
      });
    }
    else if (callback) {
      callback();
    }
  }

  window.Ractive.fireController = function(name, component, data, el, config, callback, tries) {
    tries = (tries || 0) + 1;

    if (_controllers[name]) {
      _callControllers(_controllers[name], component, data, el, config, 0, callback);
    }
    else if (tries < 500) {
      setTimeout(function() {
        window.Ractive.fireController(name, component, data, el, config, callback, tries);
      }, 10);
    }
    else if (callback) {
      callback();
    }
  };

})();
<commit_msg>fix(controller): add force no "tries" with false value<commit_after>(function() {
  'use strict';

  var _controllers = {};

  window.Ractive.controller = function(name, controller) {
    _controllers[name] = _controllers[name] || [];

    if (typeof controller == 'function') {
      _controllers[name].push(controller);
    }
  };

  function _callControllers(controllers, component, data, el, config, i, callback) {
    if (i < controllers.length) {
      controllers[i](component, data, el, config, function done() {
        _callControllers(controllers, component, data, el, config, ++i, callback);
      });
    }
    else if (callback) {
      callback();
    }
  }

  window.Ractive.fireController = function(name, component, data, el, config, callback, tries) {
    tries = tries === false ? tries : (tries || 0) + 1;

    if (_controllers[name]) {
      _callControllers(_controllers[name], component, data, el, config, 0, callback);
    }
    else if (tries !== false && tries < 500) {
      setTimeout(function() {
        window.Ractive.fireController(name, component, data, el, config, callback, tries);
      }, 10);
    }
    else if (callback) {
      callback();
    }
  };

})();
<|endoftext|>


<commit_before>var _ = require('lodash');
var FieldType = require('../Type');
var util = require('util');
var utils = require('keystone-utils');

/**
 * Select FieldType Constructor
 * @extends Field
 * @api public
 */
function select (list, path, options) {
	this.ui = options.ui || 'select';
	this.numeric = options.numeric ? true : false;
	this._nativeType = (options.numeric) ? Number : String;
	this._underscoreMethods = ['format', 'pluck'];
	this._properties = ['ops', 'numeric'];
	if (typeof options.options === 'string') {
		options.options = options.options.split(',');
	}
	if (!Array.isArray(options.options)) {
		throw new Error('Select fields require an options array.');
	}
	this.ops = options.options.map(function (i) {
		var op = _.isString(i) ? { value: i.trim(), label: utils.keyToLabel(i) } : i;
		if (!_.isObject(op)) {
			op = { label: '' + i, value: '' + i };
		}
		if (options.numeric && !_.isNumber(op.value)) {
			op.value = Number(op.value);
		}
		return op;
	});
	// undefined options.emptyOption defaults to true
	if (options.emptyOption === undefined) {
		options.emptyOption = true;
	}
	// ensure this.emptyOption is a boolean
	this.emptyOption = options.emptyOption ? true : false;
	// cached maps for options, labels and values
	this.map = utils.optionsMap(this.ops);
	this.labels = utils.optionsMap(this.ops, 'label');
	this.values = _.map(this.ops, 'value');
	select.super_.call(this, list, path, options);
}
util.inherits(select, FieldType);

/**
 * Registers the field on the List's Mongoose Schema.
 *
 * Adds a virtual for accessing the label of the selected value,
 * and statics to the Schema for converting a value to a label,
 * and retrieving all of the defined options.
 */
select.prototype.addToSchema = function () {
	var field = this;
	var schema = this.list.schema;
	this.paths = {
		data: this.options.dataPath || this._path.append('Data'),
		label: this.options.labelPath || this._path.append('Label'),
		options: this.options.optionsPath || this._path.append('Options'),
		map: this.options.optionsMapPath || this._path.append('OptionsMap'),
	};
	schema.path(this.path, _.defaults({
		type: this._nativeType,
		enum: this.values,
		set: function (val) {
			return (val === '' || val === null || val === false) ? undefined : val;
		},
	}, this.options));
	schema.virtual(this.paths.data).get(function () {
		return field.map[this.get(field.path)];
	});
	schema.virtual(this.paths.label).get(function () {
		return field.labels[this.get(field.path)];
	});
	schema.virtual(this.paths.options).get(function () {
		return field.ops;
	});
	schema.virtual(this.paths.map).get(function () {
		return field.map;
	});
	this.bindUnderscoreMethods();
};

/**
 * Returns a key value from the selected option
 */
select.prototype.pluck = function (item, property, _default) {
	var option = item.get(this.paths.data);
	return (option) ? option[property] : _default;
};

/**
 * Retrieves a shallow clone of the options array
 */
select.prototype.cloneOps = function () {
	return _.map(this.ops, _.clone);
};

/**
 * Retrieves a shallow clone of the options map
 */
select.prototype.cloneMap = function () {
	return utils.optionsMap(this.ops, true);
};

/**
 * Add filters to a query
 */
select.prototype.addFilterToQuery = function (filter, query) {
	query = query || {};
	if (!Array.isArray(filter.value)) {
		if (filter.value) {
			filter.value = [filter.value];
		} else {
			filter.value = [];
		}
	}
	if (filter.value.length) {
		if (filter.value.length > 1) {
			query[this.path] = (filter.inverted) ? { $nin: filter.value } : { $in: filter.value };
		} else {
			query[this.path] = (filter.inverted) ? { $ne: filter.value } : filter.value;
		}
	} else {
		query[this.path] = (filter.inverted) ? { $nin: ['', null] } : { $in: ['', null] };
	}
	return query;
};

/**
 * Asynchronously confirms that the provided value is valid
 */
select.prototype.validateInput = function (data, callback) {
	var value = this.getValueFromData(data);
	if (typeof value === 'string' && this.numeric) {
		value = utils.number(value);
	}
	var result = value === undefined || (value in this.map) ? true : false;
	utils.defer(callback, result);
};

/**
 * Asynchronously confirms that the provided value is present
 */
select.prototype.validateRequiredInput = function (item, data, callback) {
	var value = this.getValueFromData(data);
	var result = value !== undefined || (value === undefined && item.get(this.path)) ? true : false;
	utils.defer(callback, result);
};

/**
 * Validates that a valid option has been provided in a data object
 *
 * Deprecated
 */
select.prototype.inputIsValid = function (data, required, item) {
	if (data[this.path]) {
		return (data[this.path] in this.map) ? true : false;
	} else {
		return (!required || (!(this.path in data) && item && item.get(this.path))) ? true : false;
	}
};

/**
 * Formats the field value
 */
select.prototype.format = function (item) {
	return this.labels[item.get(this.path)] || '';
};

/* Export Field Type */
module.exports = select;
<commit_msg>Fixing issues filtering on a single value for Select fields<commit_after>var _ = require('lodash');
var FieldType = require('../Type');
var util = require('util');
var utils = require('keystone-utils');

/**
 * Select FieldType Constructor
 * @extends Field
 * @api public
 */
function select (list, path, options) {
	this.ui = options.ui || 'select';
	this.numeric = options.numeric ? true : false;
	this._nativeType = (options.numeric) ? Number : String;
	this._underscoreMethods = ['format', 'pluck'];
	this._properties = ['ops', 'numeric'];
	if (typeof options.options === 'string') {
		options.options = options.options.split(',');
	}
	if (!Array.isArray(options.options)) {
		throw new Error('Select fields require an options array.');
	}
	this.ops = options.options.map(function (i) {
		var op = _.isString(i) ? { value: i.trim(), label: utils.keyToLabel(i) } : i;
		if (!_.isObject(op)) {
			op = { label: '' + i, value: '' + i };
		}
		if (options.numeric && !_.isNumber(op.value)) {
			op.value = Number(op.value);
		}
		return op;
	});
	// undefined options.emptyOption defaults to true
	if (options.emptyOption === undefined) {
		options.emptyOption = true;
	}
	// ensure this.emptyOption is a boolean
	this.emptyOption = options.emptyOption ? true : false;
	// cached maps for options, labels and values
	this.map = utils.optionsMap(this.ops);
	this.labels = utils.optionsMap(this.ops, 'label');
	this.values = _.map(this.ops, 'value');
	select.super_.call(this, list, path, options);
}
util.inherits(select, FieldType);

/**
 * Registers the field on the List's Mongoose Schema.
 *
 * Adds a virtual for accessing the label of the selected value,
 * and statics to the Schema for converting a value to a label,
 * and retrieving all of the defined options.
 */
select.prototype.addToSchema = function () {
	var field = this;
	var schema = this.list.schema;
	this.paths = {
		data: this.options.dataPath || this._path.append('Data'),
		label: this.options.labelPath || this._path.append('Label'),
		options: this.options.optionsPath || this._path.append('Options'),
		map: this.options.optionsMapPath || this._path.append('OptionsMap'),
	};
	schema.path(this.path, _.defaults({
		type: this._nativeType,
		enum: this.values,
		set: function (val) {
			return (val === '' || val === null || val === false) ? undefined : val;
		},
	}, this.options));
	schema.virtual(this.paths.data).get(function () {
		return field.map[this.get(field.path)];
	});
	schema.virtual(this.paths.label).get(function () {
		return field.labels[this.get(field.path)];
	});
	schema.virtual(this.paths.options).get(function () {
		return field.ops;
	});
	schema.virtual(this.paths.map).get(function () {
		return field.map;
	});
	this.bindUnderscoreMethods();
};

/**
 * Returns a key value from the selected option
 */
select.prototype.pluck = function (item, property, _default) {
	var option = item.get(this.paths.data);
	return (option) ? option[property] : _default;
};

/**
 * Retrieves a shallow clone of the options array
 */
select.prototype.cloneOps = function () {
	return _.map(this.ops, _.clone);
};

/**
 * Retrieves a shallow clone of the options map
 */
select.prototype.cloneMap = function () {
	return utils.optionsMap(this.ops, true);
};

/**
 * Add filters to a query
 */
select.prototype.addFilterToQuery = function (filter, query) {
	query = query || {};
	if (!Array.isArray(filter.value)) {
		if (filter.value) {
			filter.value = [filter.value];
		} else {
			filter.value = [];
		}
	}
	if (filter.value.length > 1) {
		query[this.path] = (filter.inverted) ? { $nin: filter.value } : { $in: filter.value };
	} else if (filter.value.length === 1) {
		query[this.path] = (filter.inverted) ? { $ne: filter.value[0] } : filter.value[0];
	} else {
		query[this.path] = (filter.inverted) ? { $nin: ['', null] } : { $in: ['', null] };
	}
	return query;
};

/**
 * Asynchronously confirms that the provided value is valid
 */
select.prototype.validateInput = function (data, callback) {
	var value = this.getValueFromData(data);
	if (typeof value === 'string' && this.numeric) {
		value = utils.number(value);
	}
	var result = value === undefined || (value in this.map) ? true : false;
	utils.defer(callback, result);
};

/**
 * Asynchronously confirms that the provided value is present
 */
select.prototype.validateRequiredInput = function (item, data, callback) {
	var value = this.getValueFromData(data);
	var result = value !== undefined || (value === undefined && item.get(this.path)) ? true : false;
	utils.defer(callback, result);
};

/**
 * Validates that a valid option has been provided in a data object
 *
 * Deprecated
 */
select.prototype.inputIsValid = function (data, required, item) {
	if (data[this.path]) {
		return (data[this.path] in this.map) ? true : false;
	} else {
		return (!required || (!(this.path in data) && item && item.get(this.path))) ? true : false;
	}
};

/**
 * Formats the field value
 */
select.prototype.format = function (item) {
	return this.labels[item.get(this.path)] || '';
};

/* Export Field Type */
module.exports = select;
<|endoftext|>


<commit_before>//There should always be a query variable present: from that, it should be possible to derive anything else we'll ever need, and any changes can update it directly.
//This is the cardinal rule of the architecture here: absolutely any state must DRAW FROM and UPDATE the query variable if it pertains to any higher-level architecture
//about what's being displayed.
//Violating this rule, ever, will pretty much instantly make the code un-maintable. I promise.

var query = {};

var defaultQuery = {
    "method":"return_json",
    "words_collation":"Case_Sensitive",
    "database":"ChronAm",
    "search_limits":{
        "date_year":{"$lte":1922,"$gte":1850},
        "word":["Apache"]
    },
    "aesthetic":{'x':'lng','y':'lat',"size":"WordCount","color":"WordsPerMillion"},
    "plotType":"map"
};

if (window.location.host=="melville.seas.harvard.edu") {
    defaultQuery = {
        "method":"return_json",
        "words_collation":"Case_Sensitive",
        "groups":["year","classification"],
        "database":"presidio",
        "counttype":["WordCount","TotalWords","WordsPerMillion"],
        "search_limits":{
            "year":{"$lte":1922,"$gte":1850},
            "word":["chart"]
        },
        "aesthetic":{"x":"yearchunk","y":"classification","color":"WordsPerMillion"},
        "plotType":"heatMap"
    }
}

if (window.location.host=="localhost:8080") {
    console.log("yo, localhost!")
    defaultQuery = {
        "method":"return_json",
        "words_collation":"Case_Sensitive",
        "groups":["publication_date_year","publication_date_month_year"],
        "database":"halftimes",
        "counttype":["WordCount","TotalWords","WordsPerMillion"],
        "search_limits":{
            "word":["war"]
        },
        "plotType":"heatMap"
    }
}

var lastPlotted="None"

//Graphical Elements
var w = window.innerWidth
var h = window.innerHeight

//Things for everywhere
var svg = d3.select("#svg")
    .attr('width',window.innerWidth)
    .attr('height',window.innerHeight*.9)

var width;

//These are the things to delete when a new chart is refreshed.
//They contain the various aspects of the actual plot

var bottomLevel = svg.append("g").attr("id", "#bottomLevel")
var maplevel = svg.append("g").attr("id", "#maplevel")
var paperdiv = svg
    .append("g")
    .attr("id","#paperdiv")

//These datums (that's the plural of datum, you know) are there to
//make the item draggable using the "drag" function.

paperdiv.datum({"x":0,"y":0})

//var legend = svg.append('g').attr('id','legend');
//var colorLegend = legend.append('g').attr('id','colorLegend').attr('transform','translate(' + w/25+ ','+h/7+')');

var title = svg.append('g').attr('id','title').attr('transform','translate(' + w*.4+ ',' + 25  +')');

// Things for the background map

var projection = {"mapname":"none"}
var stateItems;

// Variables for the grid charts
var testing, x,y;

// Prepare the paper points.
// These variables should be renamed for clarity: 'paper' refers to newspapers, for legacy reasons.
var paperdata = [];

var colorLegendPointer,
updatePointer,
sizeAxis,
titleText,
legendScale = d3.scale.linear()
;

var nameSubstitutions = {
    "WordsPerMillion":"Uses per Million Words",
    "WordCount":"# of matches",
    "TextPercent":"% of texts",
    "TotalWords":"Total # of words",
    "TextCount":"# of Texts",
    "TotalTexts":"Total # of Texts"
}

var quantitativeVariables = [
    {"variable":"WordsPerMillion","label":"Uses per Million Words"},
    {"variable": "WordCount","label":"# of matches"},
    {"variable":"TextPercent","label":"% of texts"},
    {"variable":"TotalWords","label":"Total # of words"},
    {"variable":"TextCount","label":"# of Texts"},
    {"variable":"TotalTexts","label":"Total # of Texts"},
    {"variable":"WordsRatio","label":"Ratio of group A to B"},
    {"variable":"SumWords","label":"Total in both sets"}
]

for (item in quantitativeVariables) {
    nameSubstitutions[item.variable] = item.label
}

//Pull query from hash location iff supplied
if(window.location.hash) {
    var hash = window.location.hash.substring(1);
    decoded = decodeURIComponent(hash)
    query =  JSON.parse(decoded)
} else {
    query = defaultQuery
}

if (query['scaleType']==undefined) {
    query['scaleType'] = "linear"
}

if (!('aesthetic' in query)) {
    query['aesthetic']= {
        "color":"WordsPerMillion",
        "size":"WordCount",
        "filterByTop":"WordCount"
    }
}

updateAxisOptionBoxes()

// And for now I'm just having that query live in a text box. We can use the real Bookworm query entries instead, but no use reinventing that wheel here.

var APIbox = d3.select('#APIbox')
    .property('value', JSON.stringify(query))
    .attr('style', 'width: 95%;')
    .on('keyup',function(){})

//Well, one re-invention: a word box at the top that automatically updates the text box, and vice-versa.

d3.selectAll("[bindTo]")
    .on('change',function() {
        console.log("change registered")
        queryAligner.updateQuery(d3.select(this))
    })
    .on('keyup',function() {
        console.log("keyup registered")
        queryAligner.updateQuery(d3.select(this))
    })

queryAligner.updateQuery()

d3.select("body").append("button")
    .text('Redraw Plot')
    .on('click',function(){
        currentPlot = myPlot()
        currentPlot()
    })

d3.select("#AdvancedOptions")
//Make advanced Options into a toggler.
    .on("click",function() {
        display = d3.select(".debugging")
        display = (display.style("display"))

        if (display=="none") {

            d3.selectAll(".debugging")
                .style("display","inline")
            d3
                .select(this)
                .text("Hide Advanced")
        }
        if (display=="inline") {
            d3.selectAll(".debugging").style("display","none")
            d3.select(this).text("Show Advanced")
        }
    })

d3.select("#ExportData")
//Make advanced Options into a toggler.
    .on("click",function() {
        function post_to_url(path, params, method) {
            //Function From http://stackoverflow.com/questions/133925/javascript-post-request-like-a-form-submit
            method = method || "post"; // Set method to post by default, if not specified.

            // The rest of this code assumes you are not using a library.
            // It can be made less wordy if you use one.
            var form = document.createElement("form");
            form.setAttribute("method", method);
            form.setAttribute("action", path);

            for(var key in params) {
                if(params.hasOwnProperty(key)) {
                    var hiddenField = document.createElement("input");
                    hiddenField.setAttribute("type", "hidden");
                    hiddenField.setAttribute("name", key);
                    hiddenField.setAttribute("value", params[key]);

                    form.appendChild(hiddenField);
                }
            }
            console.log(params)
            document.body.appendChild(form);
            form.submit();
        }

        localquery = JSON.parse(JSON.stringify(query))
        localquery['method'] = "return_tsv"
        post_to_url("/cgi-bin/dbbindings.py",   {"queryTerms":JSON.stringify(localquery)})
    })

var lastOptions = $('<div />');

//The types of maps are just coded in.
mapOptions = [
    {"text":'USA','value':"USA"},
    {'text':'World','value':"World"},
    {'text':'Europe','value':"Europe"},
    {'text':'Asia','value':"Asia"}
]

mapSelector = d3.select("#lastOptions").append('select').attr('id',"mapChoice").attr('class',"chartSpecific mapChart")
pointSelector = mapSelector.selectAll('option').data(mapOptions)

pointSelector.enter()
    .append('option')
    .attr('value',function(d){return d.value})
    .text(function(d) {return(d.text)})

scaleTypes = [{'text':'Linear','value':"linear"},
              {"text":'Logarithmic','value':"log"}]

scaleSelector = d3.select("#lastOptions")
    .append('select')
    .attr('id',"scaleType")

scaleOptions = scaleSelector.selectAll('option').data(scaleTypes)

scaleOptions.enter()
    .append('option')
    .attr('value',function(d){return d.value})
    .text(function(d) {return(d.text)})

scaleSelector.on("change",function(d){
    //myPlot()()
    currentPlot()
})

var options = $('<div />');

//executeButtons.appendTo($('body'));

d3.select("body").on("keypress",function(e){
    console.log(e)
    if(d3.event.keyCode == 13){
        plotting = myPlot();
        plotting()
    }
});

//$('body').keypress(function(e){
//    if(e.which == 13){
//        plotting = myPlot();
//        plotting()
//    }
//});

//I like this pallette--I think we usually need two tones to really discriminate,
//even though dataviz wisdom seems to say that's not kosher.

greenToRed = ["#D61818","#FFAE63","#FFFFBD","#B5E384"].reverse()
RdYlGn = ['rgb(26,152,80)','rgb(255,255,191)','rgb(215,48,39)']
RdYlGn = greenToRed
PuOr = ['rgb(84,39,136)','rgb(153,142,195)','rgb(216,218,235)','rgb(247,247,247)','rgb(254,224,182)','rgb(230,97,1)']
RdYlGn = ["#D61818","#FFAE63","#FFFFBD","#B5E384"].reverse()
RdYlGn = colorbrewer["RdYlGn"][5].slice(0,4).reverse()

//RdYlGn = greenToRed

//define some default scales
nwords = d3.scale.sqrt().range([0,100]);
var sizescale = nwords
var colorscale = d3.scale.log().range(greenToRed);

//Have to keep the data the same for subsequent calls, but this will transform them
plotTransformers = {};
dataTypes = {};

var legendData = [];

var currentPlot=myPlot()
currentPlot()
d3.selectAll(".debugging").style('display','none')
<commit_msg>Removing extraneous code for log scale selector<commit_after>//There should always be a query variable present: from that, it should be possible to derive anything else we'll ever need, and any changes can update it directly.
//This is the cardinal rule of the architecture here: absolutely any state must DRAW FROM and UPDATE the query variable if it pertains to any higher-level architecture
//about what's being displayed.
//Violating this rule, ever, will pretty much instantly make the code un-maintable. I promise.

var query = {};

var defaultQuery = {
    "method":"return_json",
    "words_collation":"Case_Sensitive",
    "database":"ChronAm",
    "search_limits":{
        "date_year":{"$lte":1922,"$gte":1850},
        "word":["Apache"]
    },
    "aesthetic":{'x':'lng','y':'lat',"size":"WordCount","color":"WordsPerMillion"},
    "plotType":"map"
};

if (window.location.host=="melville.seas.harvard.edu") {
    defaultQuery = {
        "method":"return_json",
        "words_collation":"Case_Sensitive",
        "groups":["year","classification"],
        "database":"presidio",
        "counttype":["WordCount","TotalWords","WordsPerMillion"],
        "search_limits":{
            "year":{"$lte":1922,"$gte":1850},
            "word":["chart"]
        },
        "aesthetic":{"x":"yearchunk","y":"classification","color":"WordsPerMillion"},
        "plotType":"heatMap"
    }
}

if (window.location.host=="localhost:8080") {
    console.log("yo, localhost!")
    defaultQuery = {
        "method":"return_json",
        "words_collation":"Case_Sensitive",
        "groups":["publication_date_year","publication_date_month_year"],
        "database":"halftimes",
        "counttype":["WordCount","TotalWords","WordsPerMillion"],
        "search_limits":{
            "word":["war"]
        },
        "plotType":"heatMap"
    }
}

var lastPlotted="None"

//Graphical Elements
var w = window.innerWidth
var h = window.innerHeight

//Things for everywhere
var svg = d3.select("#svg")
    .attr('width',window.innerWidth)
    .attr('height',window.innerHeight*.9)

var width;

//These are the things to delete when a new chart is refreshed.
//They contain the various aspects of the actual plot

var bottomLevel = svg.append("g").attr("id", "#bottomLevel")
var maplevel = svg.append("g").attr("id", "#maplevel")
var paperdiv = svg
    .append("g")
    .attr("id","#paperdiv")

//These datums (that's the plural of datum, you know) are there to
//make the item draggable using the "drag" function.

paperdiv.datum({"x":0,"y":0})

//var legend = svg.append('g').attr('id','legend');
//var colorLegend = legend.append('g').attr('id','colorLegend').attr('transform','translate(' + w/25+ ','+h/7+')');

var title = svg.append('g').attr('id','title').attr('transform','translate(' + w*.4+ ',' + 25  +')');

// Things for the background map

var projection = {"mapname":"none"}
var stateItems;

// Variables for the grid charts
var testing, x,y;

// Prepare the paper points.
// These variables should be renamed for clarity: 'paper' refers to newspapers, for legacy reasons.
var paperdata = [];

var colorLegendPointer,
updatePointer,
sizeAxis,
titleText,
legendScale = d3.scale.linear()
;

var nameSubstitutions = {
    "WordsPerMillion":"Uses per Million Words",
    "WordCount":"# of matches",
    "TextPercent":"% of texts",
    "TotalWords":"Total # of words",
    "TextCount":"# of Texts",
    "TotalTexts":"Total # of Texts"
}

var quantitativeVariables = [
    {"variable":"WordsPerMillion","label":"Uses per Million Words"},
    {"variable": "WordCount","label":"# of matches"},
    {"variable":"TextPercent","label":"% of texts"},
    {"variable":"TotalWords","label":"Total # of words"},
    {"variable":"TextCount","label":"# of Texts"},
    {"variable":"TotalTexts","label":"Total # of Texts"},
    {"variable":"WordsRatio","label":"Ratio of group A to B"},
    {"variable":"SumWords","label":"Total in both sets"}
]

for (item in quantitativeVariables) {
    nameSubstitutions[item.variable] = item.label
}

//Pull query from hash location iff supplied
if(window.location.hash) {
    var hash = window.location.hash.substring(1);
    decoded = decodeURIComponent(hash)
    query =  JSON.parse(decoded)
} else {
    query = defaultQuery
}

if (query['scaleType']==undefined) {
    query['scaleType'] = "linear"
}

if (!('aesthetic' in query)) {
    query['aesthetic']= {
        "color":"WordsPerMillion",
        "size":"WordCount",
        "filterByTop":"WordCount"
    }
}

updateAxisOptionBoxes()

// And for now I'm just having that query live in a text box. We can use the real Bookworm query entries instead, but no use reinventing that wheel here.

var APIbox = d3.select('#APIbox')
    .property('value', JSON.stringify(query))
    .attr('style', 'width: 95%;')
    .on('keyup',function(){})

//Well, one re-invention: a word box at the top that automatically updates the text box, and vice-versa.

d3.selectAll("[bindTo]")
    .on('change',function() {
        console.log("change registered")
        queryAligner.updateQuery(d3.select(this))
    })
    .on('keyup',function() {
        console.log("keyup registered")
        queryAligner.updateQuery(d3.select(this))
    })

queryAligner.updateQuery()

d3.select("body").append("button")
    .text('Redraw Plot')
    .on('click',function(){
        currentPlot = myPlot()
        currentPlot()
    })

d3.select("#AdvancedOptions")
//Make advanced Options into a toggler.
    .on("click",function() {
        display = d3.select(".debugging")
        display = (display.style("display"))

        if (display=="none") {

            d3.selectAll(".debugging")
                .style("display","inline")
            d3
                .select(this)
                .text("Hide Advanced")
        }
        if (display=="inline") {
            d3.selectAll(".debugging").style("display","none")
            d3.select(this).text("Show Advanced")
        }
    })

d3.select("#ExportData")
//Make advanced Options into a toggler.
    .on("click",function() {
        function post_to_url(path, params, method) {
            //Function From http://stackoverflow.com/questions/133925/javascript-post-request-like-a-form-submit
            method = method || "post"; // Set method to post by default, if not specified.

            // The rest of this code assumes you are not using a library.
            // It can be made less wordy if you use one.
            var form = document.createElement("form");
            form.setAttribute("method", method);
            form.setAttribute("action", path);

            for(var key in params) {
                if(params.hasOwnProperty(key)) {
                    var hiddenField = document.createElement("input");
                    hiddenField.setAttribute("type", "hidden");
                    hiddenField.setAttribute("name", key);
                    hiddenField.setAttribute("value", params[key]);

                    form.appendChild(hiddenField);
                }
            }
            console.log(params)
            document.body.appendChild(form);
            form.submit();
        }

        localquery = JSON.parse(JSON.stringify(query))
        localquery['method'] = "return_tsv"
        post_to_url("/cgi-bin/dbbindings.py",   {"queryTerms":JSON.stringify(localquery)})
    })

var lastOptions = $('<div />');

//The types of maps are just coded in.
mapOptions = [
    {"text":'USA','value':"USA"},
    {'text':'World','value':"World"},
    {'text':'Europe','value':"Europe"},
    {'text':'Asia','value':"Asia"}
]

mapSelector = d3.select("#lastOptions").append('select').attr('id',"mapChoice").attr('class',"chartSpecific mapChart")
pointSelector = mapSelector.selectAll('option').data(mapOptions)

pointSelector.enter()
    .append('option')
    .attr('value',function(d){return d.value})
    .text(function(d) {return(d.text)})

var options = $('<div />');

//executeButtons.appendTo($('body'));

d3.select("body").on("keypress",function(e){
    console.log(e)
    if(d3.event.keyCode == 13){
        plotting = myPlot();
        plotting()
    }
});

//$('body').keypress(function(e){
//    if(e.which == 13){
//        plotting = myPlot();
//        plotting()
//    }
//});

//I like this pallette--I think we usually need two tones to really discriminate,
//even though dataviz wisdom seems to say that's not kosher.

greenToRed = ["#D61818","#FFAE63","#FFFFBD","#B5E384"].reverse()
RdYlGn = ['rgb(26,152,80)','rgb(255,255,191)','rgb(215,48,39)']
RdYlGn = greenToRed
PuOr = ['rgb(84,39,136)','rgb(153,142,195)','rgb(216,218,235)','rgb(247,247,247)','rgb(254,224,182)','rgb(230,97,1)']
RdYlGn = ["#D61818","#FFAE63","#FFFFBD","#B5E384"].reverse()
RdYlGn = colorbrewer["RdYlGn"][5].slice(0,4).reverse()

//RdYlGn = greenToRed

//define some default scales
nwords = d3.scale.sqrt().range([0,100]);
var sizescale = nwords
var colorscale = d3.scale.log().range(greenToRed);

//Have to keep the data the same for subsequent calls, but this will transform them
plotTransformers = {};
dataTypes = {};

var legendData = [];

var currentPlot=myPlot()
currentPlot()
d3.selectAll(".debugging").style('display','none')
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

function lib() { // eslint-disable-line no-unused-vars

    var abbreviations = {
        thousand : 'k',
        million  : 'm',
        billion  : 'b',
        trillion : 't'
    };

    function getMaxFractionDigits (pattern, symbols) {
        var decimalSeparator = symbols && symbols.decimalSeparator ? symbols.decimalSeparator : '.';
        var zero = symbols && symbols.zero ? symbols.zero : '0';
        var patternSplit = pattern.split(decimalSeparator);
        var reg = new RegExp('[^(#'+ zero + ']','g');
        if (patternSplit.length > 1) {
            return patternSplit[1].replace(reg,'').length;
        }
        return 0;
    }

    return {
        formatNumber: function (number, formatter) {
            var numberFormat = this.getNumberFormat(formatter);

            if (!$A.util.isUndefinedOrNull(number) && !isNaN(number)) {
                return numberFormat.format(number);
            }
            return '';
        },
        getNumberFormat: function (formatter) {
            if (typeof formatter === 'string') {
                try {
                    return $A.localizationService.getNumberFormat(formatter);
                } catch (e) {
                    // invalid number format error
                    // use default instead and show a warning on console
                    return $A.localizationService.getDefaultNumberFormat();
                }
            }
            return $A.localizationService.getDefaultNumberFormat();
        },
        unFormatNumber: function (string ) {
            var decimalSeparator = $A.get("$Locale.decimal");
            var currencySymbol   = $A.get("$Locale.currency");

            var stringOriginal = string,
                thousandRegExp,
                millionRegExp,
                billionRegExp,
                trillionRegExp;

            if (this.isNumber(string)) {
                return string;
            }

            if (decimalSeparator !== '.') {
                string = string.replace(/\./g, '').replace(decimalSeparator, '.');
            }

            // see if abbreviations are there so that we can multiply to the correct number
            thousandRegExp = new RegExp('[^a-zA-Z]' + abbreviations.thousand + '(?:\\)|(\\' + currencySymbol + ')?(?:\\))?)?$', 'i');
            millionRegExp  = new RegExp('[^a-zA-Z]' + abbreviations.million  + '(?:\\)|(\\' + currencySymbol + ')?(?:\\))?)?$', 'i');
            billionRegExp  = new RegExp('[^a-zA-Z]' + abbreviations.billion  + '(?:\\)|(\\' + currencySymbol + ')?(?:\\))?)?$', 'i');
            trillionRegExp = new RegExp('[^a-zA-Z]' + abbreviations.trillion + '(?:\\)|(\\' + currencySymbol + ')?(?:\\))?)?$', 'i');
            // do some math to create our number
            return ((stringOriginal.match(thousandRegExp)) ? Math.pow(10, 3) : 1) *
                   ((stringOriginal.match(millionRegExp))  ? Math.pow(10, 6) : 1) *
                   ((stringOriginal.match(billionRegExp))  ? Math.pow(10, 9) : 1) *
                   ((stringOriginal.match(trillionRegExp)) ? Math.pow(10, 12) : 1) *
                   (((string.split('-').length + Math.min(string.split('(').length - 1, string.split(')').length - 1)) % 2) ? 1 : -1) *
                   Number(string.replace(/[^0-9\.]+/g, ''));
        },
        isNumber: function (number) {
            return $A.util.isNumber(number);
        },
        isFormattedNumber: function (string, formatter) {
            var zero              = $A.get("$Locale.zero");
            var decimalSeparator  = $A.get("$Locale.decimal");
            var groupingSeparator = $A.get("$Locale.grouping");
            var maxFractionDigits = getMaxFractionDigits(formatter, { decimalSeparator : decimalSeparator, zero : zero});

            var const1 = '(?!(K|B|M|T|\\' + decimalSeparator + '))';

            // This regexp math with any formatted number or any possible formatted number
            // Match with :
            // never start with letter(k,b,m,t,(decimalSeparator))
            // everything that start with ( space* (+|-) spaces* )
            // any repeat of #{1}(groupingSeparator)#{0,n}
            // follow decimalSeparator #{0,maxFractionDigits} (not required)
            // ended by any shortcut (K|B|M|T)
            // it not case sensitive
            var regString = '^' + const1 + '((\\s*(\\+|\\-)?\\s*)' + const1 + ')?' +
                            '(\\d+(\\' + groupingSeparator + '\\d*)*)*' +
                            '(\\' + decimalSeparator + '\\d*)?' +
                            '(K|B|M|T)?$';

            var reg = new RegExp(regString, 'i');
            return reg.test(string);
        }
    };
}

<commit_msg>fix for EsLint issues<commit_after>/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

function lib() { // eslint-disable-line no-unused-vars

    var abbreviations = {
        thousand : 'k',
        million  : 'm',
        billion  : 'b',
        trillion : 't'
    };

    return {
        formatNumber: function (number, formatter) {
            var numberFormat = this.getNumberFormat(formatter);

            if (!$A.util.isUndefinedOrNull(number) && !isNaN(number)) {
                return numberFormat.format(number);
            }
            return '';
        },
        getNumberFormat: function (formatter) {
            if (typeof formatter === 'string') {
                try {
                    return $A.localizationService.getNumberFormat(formatter);
                } catch (e) {
                    // invalid number format error
                    // use default instead and show a warning on console
                    return $A.localizationService.getDefaultNumberFormat();
                }
            }
            return $A.localizationService.getDefaultNumberFormat();
        },
        unFormatNumber: function (string ) {
            var decimalSeparator = $A.get("$Locale.decimal");
            var currencySymbol   = $A.get("$Locale.currency");

            var stringOriginal = string,
                thousandRegExp,
                millionRegExp,
                billionRegExp,
                trillionRegExp;

            if (this.isNumber(string)) {
                return string;
            }

            if (decimalSeparator !== '.') {
                string = string.replace(/\./g, '').replace(decimalSeparator, '.');
            }

            // see if abbreviations are there so that we can multiply to the correct number
            thousandRegExp = new RegExp('[^a-zA-Z]' + abbreviations.thousand + '(?:\\)|(\\' + currencySymbol + ')?(?:\\))?)?$', 'i');
            millionRegExp  = new RegExp('[^a-zA-Z]' + abbreviations.million  + '(?:\\)|(\\' + currencySymbol + ')?(?:\\))?)?$', 'i');
            billionRegExp  = new RegExp('[^a-zA-Z]' + abbreviations.billion  + '(?:\\)|(\\' + currencySymbol + ')?(?:\\))?)?$', 'i');
            trillionRegExp = new RegExp('[^a-zA-Z]' + abbreviations.trillion + '(?:\\)|(\\' + currencySymbol + ')?(?:\\))?)?$', 'i');
            // do some math to create our number
            return ((stringOriginal.match(thousandRegExp)) ? Math.pow(10, 3) : 1) *
                   ((stringOriginal.match(millionRegExp))  ? Math.pow(10, 6) : 1) *
                   ((stringOriginal.match(billionRegExp))  ? Math.pow(10, 9) : 1) *
                   ((stringOriginal.match(trillionRegExp)) ? Math.pow(10, 12) : 1) *
                   (((string.split('-').length + Math.min(string.split('(').length - 1, string.split(')').length - 1)) % 2) ? 1 : -1) *
                   Number(string.replace(/[^0-9\.]+/g, ''));
        },
        isNumber: function (number) {
            return $A.util.isNumber(number);
        },
        isFormattedNumber: function (string) {
            var decimalSeparator  = $A.get("$Locale.decimal");
            var groupingSeparator = $A.get("$Locale.grouping");

            var const1 = '(?!(K|B|M|T|\\' + decimalSeparator + '))';

            // This regexp math with any formatted number or any possible formatted number
            // Match with :
            // never start with letter(k,b,m,t,(decimalSeparator))
            // everything that start with ( space* (+|-) spaces* )
            // any repeat of #{1}(groupingSeparator)#{0,n}
            // follow decimalSeparator #{0,maxFractionDigits} (not required)
            // ended by any shortcut (K|B|M|T)
            // it not case sensitive
            var regString = '^' + const1 + '((\\s*(\\+|\\-)?\\s*)' + const1 + ')?' +
                            '(\\d+(\\' + groupingSeparator + '\\d*)*)*' +
                            '(\\' + decimalSeparator + '\\d*)?' +
                            '(K|B|M|T)?$';

            var reg = new RegExp(regString, 'i');
            return reg.test(string);
        }
    };
}

<|endoftext|>