<commit_before>import { schedule, run, begin, end, join } from '@ember/runloop';
import { copy } from '@ember/object/internals';
import Component from '@ember/component';
import EmberObject, { computed, set, get } from '@ember/object';
import createComponentCard from 'ember-mobiledoc-editor/utils/create-component-card';
import Editor from 'mobiledoc-kit/editor/editor';
import { MOBILEDOC_VERSION } from 'mobiledoc-kit/renderers/mobiledoc';
import layout from './template';
import isMobile from '../../system/mobile';

const EDITOR_CARD_SUFFIX = '-editor';
export const ADD_CARD_HOOK = 'addComponent';
export const REMOVE_CARD_HOOK = 'removeComponent';
export const ADD_ATOM_HOOK = 'addAtomComponent';
export const REMOVE_ATOM_HOOK = 'removeAtomComponent';

let uuid = 0;

function arrayToMap(array) {
  let map = Object.create(null);
  array.forEach(key => {
    if (key) { // skip undefined/falsy key values
      map[key] = true;
    }
  });
  return map;
}

function lineBreakParser(node, builder, { addMarkerable, nodeFinished }) {
  if (node.nodeType !== 1 || node.tagName !== 'BR') {
    return;
  }
  addMarkerable(builder.createAtom('line-break', '', {}));
  nodeFinished();
}

const EMPTY_MOBILEDOC = {
  "version": MOBILEDOC_VERSION,
  "atoms": [],
  "cards": [],
  "markups": [],
  "sections": [
    [
      1,
      "p",
      []
    ]
  ]
};

export default Component.extend({
  classNames: ['text-editor'],
  tagName: 'article',
  layout,

  isMobile,

  placeholder: null,
  spellcheck: true,
  autofocus: true,

  buttons: computed('markup', function () {
    let buttons = [{
      type: 'strong',
      label: 'Bold',
      icon: 'bold'
    }, {
      type: 'em',
      label: 'Italic',
      icon: 'italic'
    }, {
      type: 'u',
      label: 'Underline',
      icon: 'underline'
    }, {
      type: 'blockquote',
      label: 'Quote',
      icon: 'quote'
    }, {
      type: 'a',
      label: 'Link',
      icon: 'link'
    }, {
      type: 'photo',
      label: 'Add Photo',
      icon: 'photo'
    }, {
      type: 'youtube',
      label: 'Embed Youtube Video',
      icon: 'youtube'
    }];

    // Mobile OSes have robust emoji keyboards
    if (!get(this, 'isMobile')) {
      buttons.push({
        type: 'emoji',
        label: 'Add Emoji',
        icon: 'emoji'
      });
    }

    if (get(this, 'markup')) {
      return get(this, 'markup').map((name) => {
        return buttons.findBy('icon', name);
      });
    }
    return buttons;
  }),

  init() {
    this._super(...arguments);
    set(this, 'componentCards', []);
    set(this, 'componentAtoms', []);

    set(this, 'cards', [
      createComponentCard('photo'),
      createComponentCard('youtube')
    ]);
    set(this, 'atoms', [{
      name: 'line-break',
      type: 'dom',
      render() {
        return document.createElement('br');
      }
    }]);

    this._startedRunLoop = false;
  },

  willRender() {
    // Use a default mobiledoc. If there are no changes, then return early.
    let mobiledoc = get(this, 'mobiledoc');
    if (((this._localMobiledoc && this._localMobiledoc === mobiledoc) ||
         (this._upstreamMobiledoc && this._upstreamMobiledoc === mobiledoc)) &&
        (this._lastDisabled === get(this, 'disabled'))) {
      // No change to mobiledoc, no need to recreate the editor
      return;
    }

    this._lastDisabled = get(this, 'disabled');
    this._upstreamMobiledoc = mobiledoc;
    this._localMobiledoc = null;

    // Teardown any old editor that might be around.
    let editor = get(this, 'editor');
    if (editor) {
      editor.destroy();
    }

    // Create a new editor.
    let componentHooks = {
      [ADD_CARD_HOOK]: ({env, options, payload}, isEditing=false) => {
        let cardId = `card-${uuid++}`;
        let cardName = env.name;
        if (isEditing) {
          cardName = cardName + EDITOR_CARD_SUFFIX;
        }
        let destinationElementId = `mobiledoc-editor-card-${cardId}`;
        let element = document.createElement('div');
        element.id = destinationElementId;

        // The data must be copied to avoid sharing the reference
        payload = copy(payload, true);

        let card = EmberObject.create({
          destinationElementId,
          cardName,
          payload,
          env,
          options,
          editor,
          postModel: env.postModel
        });
        schedule('afterRender', () => {
          get(this, 'componentCards').pushObject(card);
        });
        return { card, element };
      },
      [ADD_ATOM_HOOK]: ({env, options, payload, value}) => {
        let atomId = `atom-${uuid++}`;
        let atomName = env.name;
        let destinationElementId = `mobiledoc-editor-atom-${atomId}`;
        let element = document.createElement('span');
        element.id = destinationElementId;

        // The data must be copied to avoid sharing the reference
        payload = copy(payload, true);

        let atom = EmberObject.create({
          destinationElementId,
          atomName,
          payload,
          value,
          callbacks: env,
          options,
          editor,
          postModel: env.postModel
        });
        schedule('afterRender', () => {
          get(this, 'componentAtoms').pushObject(atom);
        });
        return { atom, element };
      },
      [REMOVE_CARD_HOOK]: (card) => {
        get(this, 'componentCards').removeObject(card);
      },
      [REMOVE_ATOM_HOOK]: (atom) => {
        get(this, 'componentAtoms').removeObject(atom);
      }
    };

    editor = new Editor({
      placeholder: get(this, 'placeholder') || '',
      spellcheck: get(this, 'spellcheck'),
      autofocus: get(this, 'autofocus'),
      cards: get(this, 'cards'),
      atoms: get(this, 'atoms'),
      mobiledoc,
      cardOptions: componentHooks,
      parserPlugins: [lineBreakParser]
    });

    editor.willRender(() => {
      // The editor's render/rerender will happen after this `editor.willRender`,
      // so we explicitly start a runloop here if there is none, so that the
      // add/remove card hooks happen inside a runloop.
      // When pasting text that gets turned into a card, for example,
      // the add card hook would run outside the runloop if we didn't begin a new
      // one now.
      if (!run.currentRunLoop) {
        this._startedRunLoop = true;
        begin();
      }
    });

    editor.didRender(() => {
      // If we had explicitly started a run loop in `editor.willRender`,
      // we must explicitly end it here.
      if (this._startedRunLoop) {
        this._startedRunLoop = false;
        end();
      }
    });

    editor.postDidChange(() => {
      join(() => {
        this.postDidChange(editor);
      });
    });

    editor.inputModeDidChange(() => {
      if (this.isDestroyed) { return; }
      join(() => {
        this.inputModeDidChange(editor);
      });
    });

    editor.registerKeyCommand({
      str: 'SHIFT+ENTER',
      run(editor) {
        editor.insertAtom('line-break', '', {});
      }
    });

    if (get(this, 'disabled')) {
      editor.disableEditing();
    }
    set(this, 'editor', editor);
  },

  didRender() {
    let editor = get(this, 'editor');
    if (!editor.hasRendered) {
      let editorElement = this.$('.mobiledoc-editor__editor')[0];
      this._isRenderingEditor = true;
      editor.render(editorElement);
      this._isRenderingEditor = false;
    }
  },

  willDestroyElement() {
    let editor = get(this, 'editor');
    editor.destroy();
  },

  mobiledoc: computed({
    get() {
      if (get(this, 'value') == null ||
          get(this, 'value') === '') {
        return EMPTY_MOBILEDOC;
      }
      try {
        return JSON.parse(get(this, 'value')) || EMPTY_MOBILEDOC;
      } catch (e) {
        return EMPTY_MOBILEDOC;
      }
    }
  }),

  postDidChange(editor) {
    let serializeVersion = this.get('serializeVersion');
    let updatedMobileDoc = editor.serialize(serializeVersion);
    this._localMobiledoc = updatedMobileDoc;
    get(this, 'onchange')(JSON.stringify(updatedMobileDoc));
  },

  inputModeDidChange(editor) {
    const markupTags = arrayToMap(editor.activeMarkups.map(m => m.tagName));
    // editor.activeSections are leaf sections.
    // Map parent section tag names (e.g. 'p', 'ul', 'ol') so that list buttons
    // are updated.
    let sectionParentTagNames = editor.activeSections.map(s => {
      return s.isNested ? s.parent.tagName : s.tagName;
    });
    const sectionTags = arrayToMap(sectionParentTagNames);

    // Avoid updating this component's properties synchronously while
    // rendering the editor (after rendering the component) because it
    // causes Ember to display deprecation warnings
    if (this._isRenderingEditor) {
      schedule('afterRender', () => {
        this.set('activeMarkupTagNames', markupTags);
        this.set('activeSectionTagNames', sectionTags);
      });
    } else {
      this.set('activeMarkupTagNames', markupTags);
      this.set('activeSectionTagNames', sectionTags);
    }
  },

  addCard(cardName, payload, isEditing=false) {
    get(this, 'editor').insertCard(cardName, payload, isEditing);
  },

  actions: {
    upload(editor, file) {
      let editor = this.get('editor');
      let range = editor.range;
      return get(this, 'onupload')(file).then((photo) => {
        editor.selectRange(range);
        this.addCard('photo', {
          id: get(photo, 'id'),
          url: get(photo, 'url'),
          title: get(photo, 'title'),
          caption: get(photo, 'caption')
        });
      });
    },

    openForm(component) {
      if (this.get('form.component') === component) {
        let range = get(this, 'form.range');
        this.set('form', null);
        this.get('editor').selectRange(range);
        return;
      }

      let editor = this.get('editor');
      if (!editor.hasCursor()) {
        return;
      }
      if (editor.hasActiveMarkup('a')) {
        editor.toggleMarkup('a');
      } else {
        this.set('form', {
          range: editor.range,
          component
        });
      }
    },

    addLink(href) {
      let range = get(this, 'form.range');
      this.set('form', null);
      let editor = this.get('editor');
      editor.selectRange(range);
      editor.toggleMarkup('a', { href });
    },

    toggleMarkup(editor, forProperty) {
      editor.toggleMarkup(forProperty);
    },

    toggleSection(editor, forProperty) {
      editor.toggleSection(forProperty);
    },

    addYoutubeEmbed(url) {
      let { range } = this.get('form');
      this.set('form', null);
      let editor = this.get('editor');
      editor.selectRange(range);
      this.addCard('youtube', {
        url,
        width: 640,
        height: 360,
        autoplay: false
      });
    },

    addEmoji(emoji) {
      let { range } = this.get('form');
      this.set('form', null);
      let editor = this.get('editor');
      editor.selectRange(range);
      editor.insertText(emoji);
    }
  }
});
<commit_msg>don't double define variable<commit_after>import { schedule, run, begin, end, join } from '@ember/runloop';
import { copy } from '@ember/object/internals';
import Component from '@ember/component';
import EmberObject, { computed, set, get } from '@ember/object';
import createComponentCard from 'ember-mobiledoc-editor/utils/create-component-card';
import Editor from 'mobiledoc-kit/editor/editor';
import { MOBILEDOC_VERSION } from 'mobiledoc-kit/renderers/mobiledoc';
import layout from './template';
import isMobile from '../../system/mobile';

const EDITOR_CARD_SUFFIX = '-editor';
export const ADD_CARD_HOOK = 'addComponent';
export const REMOVE_CARD_HOOK = 'removeComponent';
export const ADD_ATOM_HOOK = 'addAtomComponent';
export const REMOVE_ATOM_HOOK = 'removeAtomComponent';

let uuid = 0;

function arrayToMap(array) {
  let map = Object.create(null);
  array.forEach(key => {
    if (key) { // skip undefined/falsy key values
      map[key] = true;
    }
  });
  return map;
}

function lineBreakParser(node, builder, { addMarkerable, nodeFinished }) {
  if (node.nodeType !== 1 || node.tagName !== 'BR') {
    return;
  }
  addMarkerable(builder.createAtom('line-break', '', {}));
  nodeFinished();
}

const EMPTY_MOBILEDOC = {
  "version": MOBILEDOC_VERSION,
  "atoms": [],
  "cards": [],
  "markups": [],
  "sections": [
    [
      1,
      "p",
      []
    ]
  ]
};

export default Component.extend({
  classNames: ['text-editor'],
  tagName: 'article',
  layout,

  isMobile,

  placeholder: null,
  spellcheck: true,
  autofocus: true,

  buttons: computed('markup', function () {
    let buttons = [{
      type: 'strong',
      label: 'Bold',
      icon: 'bold'
    }, {
      type: 'em',
      label: 'Italic',
      icon: 'italic'
    }, {
      type: 'u',
      label: 'Underline',
      icon: 'underline'
    }, {
      type: 'blockquote',
      label: 'Quote',
      icon: 'quote'
    }, {
      type: 'a',
      label: 'Link',
      icon: 'link'
    }, {
      type: 'photo',
      label: 'Add Photo',
      icon: 'photo'
    }, {
      type: 'youtube',
      label: 'Embed Youtube Video',
      icon: 'youtube'
    }];

    // Mobile OSes have robust emoji keyboards
    if (!get(this, 'isMobile')) {
      buttons.push({
        type: 'emoji',
        label: 'Add Emoji',
        icon: 'emoji'
      });
    }

    if (get(this, 'markup')) {
      return get(this, 'markup').map((name) => {
        return buttons.findBy('icon', name);
      });
    }
    return buttons;
  }),

  init() {
    this._super(...arguments);
    set(this, 'componentCards', []);
    set(this, 'componentAtoms', []);

    set(this, 'cards', [
      createComponentCard('photo'),
      createComponentCard('youtube')
    ]);
    set(this, 'atoms', [{
      name: 'line-break',
      type: 'dom',
      render() {
        return document.createElement('br');
      }
    }]);

    this._startedRunLoop = false;
  },

  willRender() {
    // Use a default mobiledoc. If there are no changes, then return early.
    let mobiledoc = get(this, 'mobiledoc');
    if (((this._localMobiledoc && this._localMobiledoc === mobiledoc) ||
         (this._upstreamMobiledoc && this._upstreamMobiledoc === mobiledoc)) &&
        (this._lastDisabled === get(this, 'disabled'))) {
      // No change to mobiledoc, no need to recreate the editor
      return;
    }

    this._lastDisabled = get(this, 'disabled');
    this._upstreamMobiledoc = mobiledoc;
    this._localMobiledoc = null;

    // Teardown any old editor that might be around.
    let editor = get(this, 'editor');
    if (editor) {
      editor.destroy();
    }

    // Create a new editor.
    let componentHooks = {
      [ADD_CARD_HOOK]: ({env, options, payload}, isEditing=false) => {
        let cardId = `card-${uuid++}`;
        let cardName = env.name;
        if (isEditing) {
          cardName = cardName + EDITOR_CARD_SUFFIX;
        }
        let destinationElementId = `mobiledoc-editor-card-${cardId}`;
        let element = document.createElement('div');
        element.id = destinationElementId;

        // The data must be copied to avoid sharing the reference
        payload = copy(payload, true);

        let card = EmberObject.create({
          destinationElementId,
          cardName,
          payload,
          env,
          options,
          editor,
          postModel: env.postModel
        });
        schedule('afterRender', () => {
          get(this, 'componentCards').pushObject(card);
        });
        return { card, element };
      },
      [ADD_ATOM_HOOK]: ({env, options, payload, value}) => {
        let atomId = `atom-${uuid++}`;
        let atomName = env.name;
        let destinationElementId = `mobiledoc-editor-atom-${atomId}`;
        let element = document.createElement('span');
        element.id = destinationElementId;

        // The data must be copied to avoid sharing the reference
        payload = copy(payload, true);

        let atom = EmberObject.create({
          destinationElementId,
          atomName,
          payload,
          value,
          callbacks: env,
          options,
          editor,
          postModel: env.postModel
        });
        schedule('afterRender', () => {
          get(this, 'componentAtoms').pushObject(atom);
        });
        return { atom, element };
      },
      [REMOVE_CARD_HOOK]: (card) => {
        get(this, 'componentCards').removeObject(card);
      },
      [REMOVE_ATOM_HOOK]: (atom) => {
        get(this, 'componentAtoms').removeObject(atom);
      }
    };

    editor = new Editor({
      placeholder: get(this, 'placeholder') || '',
      spellcheck: get(this, 'spellcheck'),
      autofocus: get(this, 'autofocus'),
      cards: get(this, 'cards'),
      atoms: get(this, 'atoms'),
      mobiledoc,
      cardOptions: componentHooks,
      parserPlugins: [lineBreakParser]
    });

    editor.willRender(() => {
      // The editor's render/rerender will happen after this `editor.willRender`,
      // so we explicitly start a runloop here if there is none, so that the
      // add/remove card hooks happen inside a runloop.
      // When pasting text that gets turned into a card, for example,
      // the add card hook would run outside the runloop if we didn't begin a new
      // one now.
      if (!run.currentRunLoop) {
        this._startedRunLoop = true;
        begin();
      }
    });

    editor.didRender(() => {
      // If we had explicitly started a run loop in `editor.willRender`,
      // we must explicitly end it here.
      if (this._startedRunLoop) {
        this._startedRunLoop = false;
        end();
      }
    });

    editor.postDidChange(() => {
      join(() => {
        this.postDidChange(editor);
      });
    });

    editor.inputModeDidChange(() => {
      if (this.isDestroyed) { return; }
      join(() => {
        this.inputModeDidChange(editor);
      });
    });

    editor.registerKeyCommand({
      str: 'SHIFT+ENTER',
      run(editor) {
        editor.insertAtom('line-break', '', {});
      }
    });

    if (get(this, 'disabled')) {
      editor.disableEditing();
    }
    set(this, 'editor', editor);
  },

  didRender() {
    let editor = get(this, 'editor');
    if (!editor.hasRendered) {
      let editorElement = this.$('.mobiledoc-editor__editor')[0];
      this._isRenderingEditor = true;
      editor.render(editorElement);
      this._isRenderingEditor = false;
    }
  },

  willDestroyElement() {
    let editor = get(this, 'editor');
    editor.destroy();
  },

  mobiledoc: computed({
    get() {
      if (get(this, 'value') == null ||
          get(this, 'value') === '') {
        return EMPTY_MOBILEDOC;
      }
      try {
        return JSON.parse(get(this, 'value')) || EMPTY_MOBILEDOC;
      } catch (e) {
        return EMPTY_MOBILEDOC;
      }
    }
  }),

  postDidChange(editor) {
    let serializeVersion = this.get('serializeVersion');
    let updatedMobileDoc = editor.serialize(serializeVersion);
    this._localMobiledoc = updatedMobileDoc;
    get(this, 'onchange')(JSON.stringify(updatedMobileDoc));
  },

  inputModeDidChange(editor) {
    const markupTags = arrayToMap(editor.activeMarkups.map(m => m.tagName));
    // editor.activeSections are leaf sections.
    // Map parent section tag names (e.g. 'p', 'ul', 'ol') so that list buttons
    // are updated.
    let sectionParentTagNames = editor.activeSections.map(s => {
      return s.isNested ? s.parent.tagName : s.tagName;
    });
    const sectionTags = arrayToMap(sectionParentTagNames);

    // Avoid updating this component's properties synchronously while
    // rendering the editor (after rendering the component) because it
    // causes Ember to display deprecation warnings
    if (this._isRenderingEditor) {
      schedule('afterRender', () => {
        this.set('activeMarkupTagNames', markupTags);
        this.set('activeSectionTagNames', sectionTags);
      });
    } else {
      this.set('activeMarkupTagNames', markupTags);
      this.set('activeSectionTagNames', sectionTags);
    }
  },

  addCard(cardName, payload, isEditing=false) {
    get(this, 'editor').insertCard(cardName, payload, isEditing);
  },

  actions: {
    upload(editor, file) {
      let range = editor.range;
      return get(this, 'onupload')(file).then((photo) => {
        editor.selectRange(range);
        this.addCard('photo', {
          id: get(photo, 'id'),
          url: get(photo, 'url'),
          title: get(photo, 'title'),
          caption: get(photo, 'caption')
        });
      });
    },

    openForm(component) {
      if (this.get('form.component') === component) {
        let range = get(this, 'form.range');
        this.set('form', null);
        this.get('editor').selectRange(range);
        return;
      }

      let editor = this.get('editor');
      if (!editor.hasCursor()) {
        return;
      }
      if (editor.hasActiveMarkup('a')) {
        editor.toggleMarkup('a');
      } else {
        this.set('form', {
          range: editor.range,
          component
        });
      }
    },

    addLink(href) {
      let range = get(this, 'form.range');
      this.set('form', null);
      let editor = this.get('editor');
      editor.selectRange(range);
      editor.toggleMarkup('a', { href });
    },

    toggleMarkup(editor, forProperty) {
      editor.toggleMarkup(forProperty);
    },

    toggleSection(editor, forProperty) {
      editor.toggleSection(forProperty);
    },

    addYoutubeEmbed(url) {
      let { range } = this.get('form');
      this.set('form', null);
      let editor = this.get('editor');
      editor.selectRange(range);
      this.addCard('youtube', {
        url,
        width: 640,
        height: 360,
        autoplay: false
      });
    },

    addEmoji(emoji) {
      let { range } = this.get('form');
      this.set('form', null);
      let editor = this.get('editor');
      editor.selectRange(range);
      editor.insertText(emoji);
    }
  }
});
<|endoftext|>


<commit_before>/*
 *  Copyright 2014 TWO SIGMA OPEN SOURCE, LLC
 *
 *  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.
 */
/**
 * Module bk.commonUi
 * This module is the general store of low level UI directives, which should be separated out or
 * potentially found equivalent in 3rd party libraries.
 */

(function() {
  'use strict';
  var module = angular.module('bk.commonUi', []);
  module.directive('onCtrlEnter', function() {
    return {
      link: function(scope, element, attrs) {
        element.bind("keyup", function(event) {
          if (event.ctrlKey && event.keyCode === 13) { // ctrl + enter
            scope.$apply(attrs.onCtrlEnter);
          }
        });
      }
    };
  });
  module.directive('eatClick', function() {
    return function(scope, element, attrs) {
      element.click(function(event) {
        event.preventDefault();
      });
    };
  });
  module.directive('focusStart', function() {
    return {
      link: function(scope, element, attrs) {
        Q.fcall(function() {
          element.focus();
        });
      }
    };
  });
  module.directive('bkcell', function() {
    return {
      restrict: 'C',
      link: function(scope, element, attrs) {
        element.mouseover(function(event) {
          element.addClass("cell-bracket-selected");
          event.stopPropagation();
        });
        element.mouseout(function(event) {
          element.removeClass("cell-bracket-selected");
          event.stopPropagation();
        });
      }
    };
  });
  module.directive('bkShow', function() { // like ngShow, but animated
    return {
      link: function(scope, element, attrs) {
        var expression = attrs.bkShow;
        scope.$watch(expression, function(newValue, oldValue) {
          if (newValue) {
            element.stop(true, true).slideDown(200);
          } else {
            element.stop(true, true).slideUp(200);
          }
        });
      }
    };
  });
  module.directive('bkHide', function() { // like ngShow, but animated
    return {
      link: function(scope, element, attrs) {
        var expression = attrs.bkHide;
        scope.$watch(expression, function(newValue, oldValue) {
          if (newValue) {
            element.stop(true, true).slideUp(200);
          } else {
            element.stop(true, true).slideDown(200);
          }
        });
      }
    };
  });
  module.filter('isHidden', function() {
    return function(input) {
      return _(input).filter(function(it) {
        return !it.hidden;
      });
    }
  });
  module.directive('dropdownPromoted', function() {
    // Is your dropdown being covered by its ancestors siblings?
    // Promote that shiz, and prepend it to the body so it doesn't
    // ever get bullied again.
    return {
      restrict: 'C',
      link: function(scope, element, attrs) {
        var dropdown = element.find('.dropdown-menu').first();
        var toggle = element.find('.dropdown-toggle').first();

        var showDropdown = function() {
          var togglePosition = toggle.offset();

          dropdown.show().css({
            top: togglePosition.top + 'px',
            left: togglePosition.left - dropdown.outerWidth() + 'px',
          });

          dropdown.prependTo('body');
          dropdown.css('visibility', 'visible');

          $(document).on('click.bs.dropdown.data-api', hideDropdown);
        };

        var hideDropdown = function() {
          dropdown
          .hide()
          .css('visibility', 'hidden')
          .appendTo(element);

          $(document).off('click.bs.dropdown.data-api', hideDropdown);
        };

        element.on('click', '.dropdown-toggle', showDropdown);

        scope.$on('$destroy', function() {
          hideDropdown();
          element.off('click');
        });
      }
    }
  });
  module.directive('bkDropdownMenu', function() {
    return {
      restrict: 'E',
      template: '
' +
          '
' +
          '' +
          '  ' +
          '{{getName(item)}}' +
          '' +
        // XXX - the submenu needs to be hacked to be as wide as the parent
        // otherwise there is a gap and you can't hit the submenu. BEAKER-433
          '
' +
          '
' +
          '' +
          '  ' +
          '{{getName(subitem)}}' +
          '' +
          '
' +
          '
' +
          '
' +
          '
',
      scope: {
        "menuItems": "=",

        // Classes to be added to any submenu item. Used for adding
        // pull-left to menus that are on the far right (e.g. bkcellmenu).
        submenuClasses: '@'
      },
      replace: true,
      controller: function($scope) {
        var isItemDisabled = function(item) {
          if (_.isFunction(item.disabled)) {
            return item.disabled();
          }
          return item.disabled;
        };

        $scope.getMenuItems = function() {
          return _.result($scope, 'menuItems');
        };

        $scope.getAClass = function(item) {
          var result = [];
          if (isItemDisabled(item)) {
            result.push("disabled-link");
          } else if (item.items && item.items.length <= 1 && item.autoReduce) {
            if (item.items.length === 0) {
              result.push("disabled-link");
            } else if (item.items.length === 1) {
              if (isItemDisabled(item.items[0])) {
                result.push("disabled-link");
              }
            }
          }
          return result.join(" ");
        };

        $scope.getItemClass = function(item) {
          var result = [];
          if (item.type === "divider") {
            result.push("divider");
          } else if (item.type === "submenu" || item.items) {
            if (item.items && item.items.length <= 1 && item.autoReduce) {

            } else {
              result.push("dropdown-submenu");
              // Add any extra submenu classes. (e.g. to specify if it should be left or right).
              if ($scope.submenuClasses) {
                _.each(
                    $scope.submenuClasses.split(' '),
                    function(elt) {
                      result.push(elt);
                    }
                );
              }
            }
          }
          return result.join(" ");
        };

        $scope.runAction = function(item) {
          if (item.items && item.items.length === 1 && item.autoReduce) {
            item.items[0].action();
          } else {
            if (_.isFunction(item.action)) {
              item.action();
            }
          }
        };

        $scope.getName = function(item) {
          var name = "";
          if (item.items && item.items.length === 1 && item.autoReduce) {
            if (item.items[0].reducedName) {
              name = item.items[0].reducedName;
            } else {
              name = item.items[0].name;
            }
          } else {
            name = item.name;
          }
          if (_.isFunction(name)) {
            name = name();
          }
          return name;
        };

        $scope.isMenuItemChecked = function(item) {
          if (item.isChecked) {
            if (_.isFunction(item.isChecked)) {
              return item.isChecked();
            } else {
              return item.isChecked;
            }
          }
          return false;
        };
        $scope.getSubItems = function(parentItem) {
          if (_.isFunction(parentItem.items)) {
            return parentItem.items();
          }
          return parentItem.items;
        };
      },
      link: function(scope, element, attrs) {

      }
    };
  });

  module.directive('bkEnter', function() {
    return function(scope, element, attrs) {
      element.bind("keydown keypress", function(event) {
        if (event.which === 13) {
          scope.$apply(function() {
            scope.$eval(attrs.bkEnter);
          });
          event.preventDefault();
        }
      });
    };
  });

  module.directive('bkLanguageLogo', function() {
    return {
      restrict: "E",
      transclude: true,
      template: "",
      scope: {
        bgColor: "@",
        fgColor: "@",
        borderColor: "@"
      },
      link: function(scope, element, attrs) {
        scope.style = {
          'background-color': scope.bgColor,
          'color': scope.fgColor
        };
        var updateStyle = function() {
          scope.style = {
            'background-color': scope.bgColor,
            'color': scope.fgColor
          };
          if (scope.borderColor) {
            scope.style['border-width'] = "1px";
            scope.style['border-color'] = scope.borderColor;
            scope.style['border-style'] = "solid";
          } else {
            delete scope.style['border-width'];
            delete scope.style['border-color'];
            delete scope.style['border-style'];
          }
        };
        scope.$watch("bgColor", updateStyle);
        scope.$watch("fgColor", updateStyle);
        scope.$watch("borderColor", updateStyle);
      }
    }
  });
})();
<commit_msg>Use dropdown-toggle as actual toggle on promoted dropdowns<commit_after>/*
 *  Copyright 2014 TWO SIGMA OPEN SOURCE, LLC
 *
 *  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.
 */
/**
 * Module bk.commonUi
 * This module is the general store of low level UI directives, which should be separated out or
 * potentially found equivalent in 3rd party libraries.
 */

(function() {
  'use strict';
  var module = angular.module('bk.commonUi', []);
  module.directive('onCtrlEnter', function() {
    return {
      link: function(scope, element, attrs) {
        element.bind("keyup", function(event) {
          if (event.ctrlKey && event.keyCode === 13) { // ctrl + enter
            scope.$apply(attrs.onCtrlEnter);
          }
        });
      }
    };
  });
  module.directive('eatClick', function() {
    return function(scope, element, attrs) {
      element.click(function(event) {
        event.preventDefault();
      });
    };
  });
  module.directive('focusStart', function() {
    return {
      link: function(scope, element, attrs) {
        Q.fcall(function() {
          element.focus();
        });
      }
    };
  });
  module.directive('bkcell', function() {
    return {
      restrict: 'C',
      link: function(scope, element, attrs) {
        element.mouseover(function(event) {
          element.addClass("cell-bracket-selected");
          event.stopPropagation();
        });
        element.mouseout(function(event) {
          element.removeClass("cell-bracket-selected");
          event.stopPropagation();
        });
      }
    };
  });
  module.directive('bkShow', function() { // like ngShow, but animated
    return {
      link: function(scope, element, attrs) {
        var expression = attrs.bkShow;
        scope.$watch(expression, function(newValue, oldValue) {
          if (newValue) {
            element.stop(true, true).slideDown(200);
          } else {
            element.stop(true, true).slideUp(200);
          }
        });
      }
    };
  });
  module.directive('bkHide', function() { // like ngShow, but animated
    return {
      link: function(scope, element, attrs) {
        var expression = attrs.bkHide;
        scope.$watch(expression, function(newValue, oldValue) {
          if (newValue) {
            element.stop(true, true).slideUp(200);
          } else {
            element.stop(true, true).slideDown(200);
          }
        });
      }
    };
  });
  module.filter('isHidden', function() {
    return function(input) {
      return _(input).filter(function(it) {
        return !it.hidden;
      });
    }
  });
  module.directive('dropdownPromoted', function() {
    // Is your dropdown being covered by its ancestors siblings?
    // Promote that shiz, and prepend it to the body so it doesn't
    // ever get bullied again.
    return {
      restrict: 'C',
      link: function(scope, element, attrs) {
        var dropdown = element.find('.dropdown-menu').first();
        var toggle = element.find('.dropdown-toggle').first();

        var showDropdown = function() {
          var togglePosition = toggle.offset();

          dropdown.show().css({
            top: togglePosition.top + 'px',
            left: togglePosition.left - dropdown.outerWidth() + 'px',
          });

          dropdown.prependTo('body');
          dropdown.css('visibility', 'visible');

          element.on('click', '.dropdown-toggle', hideDropdown);
          $(document).on('click.bs.dropdown.data-api', hideDropdown);
        };

        var hideDropdown = function() {
          dropdown
          .hide()
          .css('visibility', 'hidden')
          .appendTo(element);

          element.on('click', '.dropdown-toggle', showDropdown);
          $(document).off('click.bs.dropdown.data-api', hideDropdown);
        };

        element.on('click', '.dropdown-toggle', showDropdown);

        scope.$on('$destroy', function() {
          hideDropdown();
          element.off('click');
        });
      }
    }
  });
  module.directive('bkDropdownMenu', function() {
    return {
      restrict: 'E',
      template: '
' +
          '
' +
          '' +
          '  ' +
          '{{getName(item)}}' +
          '' +
        // XXX - the submenu needs to be hacked to be as wide as the parent
        // otherwise there is a gap and you can't hit the submenu. BEAKER-433
          '
' +
          '
' +
          '' +
          '  ' +
          '{{getName(subitem)}}' +
          '' +
          '
' +
          '
' +
          '
' +
          '
',
      scope: {
        "menuItems": "=",

        // Classes to be added to any submenu item. Used for adding
        // pull-left to menus that are on the far right (e.g. bkcellmenu).
        submenuClasses: '@'
      },
      replace: true,
      controller: function($scope) {
        var isItemDisabled = function(item) {
          if (_.isFunction(item.disabled)) {
            return item.disabled();
          }
          return item.disabled;
        };

        $scope.getMenuItems = function() {
          return _.result($scope, 'menuItems');
        };

        $scope.getAClass = function(item) {
          var result = [];
          if (isItemDisabled(item)) {
            result.push("disabled-link");
          } else if (item.items && item.items.length <= 1 && item.autoReduce) {
            if (item.items.length === 0) {
              result.push("disabled-link");
            } else if (item.items.length === 1) {
              if (isItemDisabled(item.items[0])) {
                result.push("disabled-link");
              }
            }
          }
          return result.join(" ");
        };

        $scope.getItemClass = function(item) {
          var result = [];
          if (item.type === "divider") {
            result.push("divider");
          } else if (item.type === "submenu" || item.items) {
            if (item.items && item.items.length <= 1 && item.autoReduce) {

            } else {
              result.push("dropdown-submenu");
              // Add any extra submenu classes. (e.g. to specify if it should be left or right).
              if ($scope.submenuClasses) {
                _.each(
                    $scope.submenuClasses.split(' '),
                    function(elt) {
                      result.push(elt);
                    }
                );
              }
            }
          }
          return result.join(" ");
        };

        $scope.runAction = function(item) {
          if (item.items && item.items.length === 1 && item.autoReduce) {
            item.items[0].action();
          } else {
            if (_.isFunction(item.action)) {
              item.action();
            }
          }
        };

        $scope.getName = function(item) {
          var name = "";
          if (item.items && item.items.length === 1 && item.autoReduce) {
            if (item.items[0].reducedName) {
              name = item.items[0].reducedName;
            } else {
              name = item.items[0].name;
            }
          } else {
            name = item.name;
          }
          if (_.isFunction(name)) {
            name = name();
          }
          return name;
        };

        $scope.isMenuItemChecked = function(item) {
          if (item.isChecked) {
            if (_.isFunction(item.isChecked)) {
              return item.isChecked();
            } else {
              return item.isChecked;
            }
          }
          return false;
        };
        $scope.getSubItems = function(parentItem) {
          if (_.isFunction(parentItem.items)) {
            return parentItem.items();
          }
          return parentItem.items;
        };
      },
      link: function(scope, element, attrs) {

      }
    };
  });

  module.directive('bkEnter', function() {
    return function(scope, element, attrs) {
      element.bind("keydown keypress", function(event) {
        if (event.which === 13) {
          scope.$apply(function() {
            scope.$eval(attrs.bkEnter);
          });
          event.preventDefault();
        }
      });
    };
  });

  module.directive('bkLanguageLogo', function() {
    return {
      restrict: "E",
      transclude: true,
      template: "",
      scope: {
        bgColor: "@",
        fgColor: "@",
        borderColor: "@"
      },
      link: function(scope, element, attrs) {
        scope.style = {
          'background-color': scope.bgColor,
          'color': scope.fgColor
        };
        var updateStyle = function() {
          scope.style = {
            'background-color': scope.bgColor,
            'color': scope.fgColor
          };
          if (scope.borderColor) {
            scope.style['border-width'] = "1px";
            scope.style['border-color'] = scope.borderColor;
            scope.style['border-style'] = "solid";
          } else {
            delete scope.style['border-width'];
            delete scope.style['border-color'];
            delete scope.style['border-style'];
          }
        };
        scope.$watch("bgColor", updateStyle);
        scope.$watch("fgColor", updateStyle);
        scope.$watch("borderColor", updateStyle);
      }
    }
  });
})();
<|endoftext|>


<commit_before>/**
 * WidgetAreaRenderer component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import classnames from 'classnames';
import PropTypes from 'prop-types';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import { STORE_NAME, WIDGET_AREA_STYLES } from '../datastore/constants';
import WidgetRenderer from './WidgetRenderer';
import { getWidgetLayout, combineWidgets, getWidgetComponentProps } from '../util';
import { Cell, Grid, Row } from '../../../material-components';
const { useSelect } = Data;

export default function WidgetAreaRenderer( { slug, totalAreas } ) {
	const widgetArea = useSelect( ( select ) => select( STORE_NAME ).getWidgetArea( slug ) );

	const { widgets, widgetStates } = useSelect( ( select ) => {
		const allWidgets = select( STORE_NAME ).getWidgets( slug );
		const allWidgetStates = {};
		allWidgets.forEach( ( widget ) => {
			allWidgetStates[ widget.slug ] = select( STORE_NAME ).getWidgetState( widget.slug );
		} );
		return {
			widgets: allWidgets,
			widgetStates: allWidgetStates,
		};
	} );

	// TODO: Solve this in a better way.
	const activeWidgets = widgets.filter( ( widget ) => {
		const widgetExists = widgets.some( ( item ) => item.slug === widget.slug );
		const isComponent = typeof widget.Component === 'function';
		if ( ! widgetExists || ! isComponent ) {
			return false;
		}
		const widgetComponentProps = getWidgetComponentProps( widget.slug );
		const isActive = widget.Component.prototype.render
			? new widget.Component( widgetComponentProps ).render()
			: widget.Component( widgetComponentProps );

		return Boolean( isActive );
	} );

	if ( activeWidgets.length === 0 ) {
		return null;
	}

	// Compute the layout.
	const {
		classNames,
		columnWidths,
		rowIndexes,
	} = getWidgetLayout( activeWidgets );

	// Combine widgets with similar CTAs and prepare final props to pass to
	// `WidgetRenderer` below. Only one consecutive instance of a similar CTA
	// will be maintained (via an "override component"), and all other similar
	// ones will receive a CSS class to hide them.
	// A combined CTA will span the combined width of all widgets that it was
	// combined from.
	const {
		gridClassNames,
		overrideComponents,
	} = combineWidgets( activeWidgets, widgetStates, {
		classNames,
		columnWidths,
		rowIndexes,
	} );

	// Render all widgets.
	const widgetsOutput = activeWidgets.map( ( widget, i ) => (
		<WidgetRenderer
			gridClassName={ classnames( gridClassNames[ i ] ) }
			OverrideComponent={ overrideComponents[ i ] ? () => {
				const { Component, metadata } = overrideComponents[ i ];
				return <Component { ...metadata } />;
			} : undefined }
			key={ widget.slug }
			slug={ widget.slug }
		/>
	) );

	const { Icon, title, style, subtitle } = widgetArea;

	return (
		<Grid className={ `googlesitekit-widget-area googlesitekit-widget-area--${ slug } googlesitekit-widget-area--${ style }` }>
			{ totalAreas > 1 && (
				<Row>
					<Cell className="googlesitekit-widget-area-header" size={ 12 }>
						{ Icon && (
							<Icon width={ 33 } height={ 33 } />
						) }

						{ title && (

								{ title }

						) }

						{ subtitle && (

								{ subtitle }

						) }
					</Cell>
				</Row>
			) }

				<Row>
					{ style === WIDGET_AREA_STYLES.BOXES && widgetsOutput }
					{ style === WIDGET_AREA_STYLES.COMPOSITE && (
						<Cell size={ 12 }>
							<Grid>
								<Row>
									{ widgetsOutput }
								</Row>
							</Grid>
						</Cell>
					) }
				</Row>

		</Grid>
	);
}

WidgetAreaRenderer.propTypes = {
	slug: PropTypes.string.isRequired,
};
<commit_msg>Add inactive widget rendering.<commit_after>/**
 * WidgetAreaRenderer component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import classnames from 'classnames';
import PropTypes from 'prop-types';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import { STORE_NAME, WIDGET_AREA_STYLES } from '../datastore/constants';
import WidgetRenderer from './WidgetRenderer';
import { getWidgetLayout, combineWidgets } from '../util';
import { Cell, Grid, Row } from '../../../material-components';
import { separateNullWidgets } from '../util/separate-null-widgets';
import { HIDDEN_CLASS } from '../util/constants';
const { useSelect } = Data;

export default function WidgetAreaRenderer( { slug, totalAreas } ) {
	const widgetArea = useSelect( ( select ) => select( STORE_NAME ).getWidgetArea( slug ) );

	const { widgets, widgetStates } = useSelect( ( select ) => {
		const allWidgets = select( STORE_NAME ).getWidgets( slug );
		const allWidgetStates = {};
		allWidgets.forEach( ( widget ) => {
			allWidgetStates[ widget.slug ] = select( STORE_NAME ).getWidgetState( widget.slug );
		} );
		return {
			widgets: allWidgets,
			widgetStates: allWidgetStates,
		};
	} );

	// TODO: Solve this in a better way.
	/*
	const activeWidgets = widgets.filter( ( widget ) => {
		const widgetExists = widgets.some( ( item ) => item.slug === widget.slug );
		const isComponent = typeof widget.Component === 'function';
		if ( ! widgetExists || ! isComponent ) {
			return false;
		}
		const widgetComponentProps = getWidgetComponentProps( widget.slug );
		console.log( widgetComponentProps ); // eslint-disable-line no-console
		const isActive = widget.Component.prototype.render
			? new widget.Component( widgetComponentProps ).render()
			: widget.Component( widgetComponentProps );

		return Boolean( isActive );
	} );
	*/

	const { activeWidgets, inactiveWidgets } = separateNullWidgets( widgets, widgetStates );

	/*
	if ( activeWidgets.length === 0 ) {
		return null;
	}
	 */

	// Compute the layout.
	const {
		classNames,
		columnWidths,
		rowIndexes,
	} = getWidgetLayout( activeWidgets );

	// Combine widgets with similar CTAs and prepare final props to pass to
	// `WidgetRenderer` below. Only one consecutive instance of a similar CTA
	// will be maintained (via an "override component"), and all other similar
	// ones will receive a CSS class to hide them.
	// A combined CTA will span the combined width of all widgets that it was
	// combined from.
	const {
		gridClassNames,
		overrideComponents,
	} = combineWidgets( activeWidgets, widgetStates, {
		classNames,
		columnWidths,
		rowIndexes,
	} );

	// Render all widgets.
	const widgetsOutput = activeWidgets.map( ( widget, i ) => (
		<WidgetRenderer
			gridClassName={ classnames( gridClassNames[ i ] ) }
			OverrideComponent={ overrideComponents[ i ] ? () => {
				const { Component, metadata } = overrideComponents[ i ];
				return <Component { ...metadata } />;
			} : undefined }
			key={ widget.slug }
			slug={ widget.slug }
		/>
	) );

	const { Icon, title, style, subtitle } = widgetArea;

	return (
		<Grid className={ `googlesitekit-widget-area googlesitekit-widget-area--${ slug } googlesitekit-widget-area--${ style }` }>
			{ totalAreas > 1 && (
				<Row>
					<Cell className="googlesitekit-widget-area-header" size={ 12 }>
						{ Icon && (
							<Icon width={ 33 } height={ 33 } />
						) }

						{ title && (

								{ title }

						) }

						{ subtitle && (

								{ subtitle }

						) }
					</Cell>
				</Row>
			) }

				<Row>
					{ style === WIDGET_AREA_STYLES.BOXES && widgetsOutput }
					{ style === WIDGET_AREA_STYLES.COMPOSITE && (
						<Cell size={ 12 }>
							<Grid>
								<Row>
									{ widgetsOutput }
								</Row>
							</Grid>
						</Cell>
					) }
				</Row>

			{ inactiveWidgets.length &&
			(

					{ inactiveWidgets.map( ( widget ) => (

							<widget.Component />

					) ) }

			)
			}
		</Grid>
	);
}

WidgetAreaRenderer.propTypes = {
	slug: PropTypes.string.isRequired,
};
<|endoftext|>


<commit_before>'use strict';

// NOTE: ADVANCED_OPTIMIZATIONS mode.
//
// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using
// constructs incompatible with that mode.

/* global $interpolateMinErr: false */
/* global MessageFormatParser: false */
/* global stringify: false */

/**
 * @ngdoc module
 * @name ngMessageFormat
 * @packageName angular-message-format
 *
 * @description
 *
 * ## What is  ngMessageFormat?
 *
 * The ngMessageFormat module extends the Angular {@link ng.$interpolate `$interpolate`} service
 * with a syntax for handling pluralization and gender specific messages, which is based on the
 * [ICU MessageFormat syntax][ICU].
 *
 * See [the design doc][ngMessageFormat doc] for more information.
 *
 * [ICU]: http://userguide.icu-project.org/formatparse/messages#TOC-MessageFormat
 * [ngMessageFormat doc]: https://docs.google.com/a/google.com/document/d/1pbtW2yvtmFBikfRrJd8VAsabiFkKezmYZ_PbgdjQOVU/edit
 *
 * ## Examples
 *
 * ### Gender
 *
 * This example uses the "select" keyword to specify the message based on gender.
 *
 * <example name="ngMessageFormat-example-gender" module="msgFmtExample" deps="angular-message-format.js">
 * <file name="index.html">
 *

 *    Select Recipient:

      <select ng-model="recipient" ng-options="person as person.name for person in recipients">
      </select>

{{recipient.gender, select,
                male {{{recipient.name}} unwrapped his gift. }
                female {{{recipient.name}} unwrapped her gift. }
                other {{{recipient.name}} unwrapped their gift. }
      }}

 *

 * </file>
 * <file name="script.js">
 *   function Person(name, gender) {
 *     this.name = name;
 *     this.gender = gender;
 *   }
 *
 *   var alice   = new Person("Alice", "female"),
 *       bob     = new Person("Bob", "male"),
 *       ashley = new Person("Ashley", "");
 *
 *   angular.module('msgFmtExample', ['ngMessageFormat'])
 *     .controller('AppController', ['$scope', function($scope) {
 *         $scope.recipients = [alice, bob, ashley];
 *         $scope.recipient = $scope.recipients[0];
 *       }]);
 * </file>
 * </example>
 *
 * ### Plural
 *
 * This example shows how the "plural" keyword is used to account for a variable number of entities.
 * The "#" variable holds the current number and can be embedded in the message.
 *
 * Note that "=1" takes precedence over "one".
 *
 * The example also shows the "offset" keyword, which allows you to offset the value of the "#" variable.
 *
 * <example name="ngMessageFormat-example-plural" module="msgFmtExample" deps="angular-message-format.js">
 * <file name="index.html">
 *

      Select recipients:

      <select multiple size=5 ng-model="recipients" ng-options="person as person.name for person in people">
      </select>

 *
{{recipients.length, plural, offset:1
 *             =0    {{{sender.name}} gave no gifts (\#=#)}
 *             =1    {{{sender.name}} gave a gift to {{recipients[0].name}} (\#=#)}
 *             one   {{{sender.name}} gave {{recipients[0].name}} and one other person a gift (\#=#)}
 *             other {{{sender.name}} gave {{recipients[0].name}} and # other people a gift (\#=#)}
 *           }}

 *

 * </file>
 *
 * <file name="script.js">
 *   function Person(name, gender) {
 *     this.name = name;
 *     this.gender = gender;
 *   }
 *
 *   var alice   = new Person("Alice", "female"),
 *       bob     = new Person("Bob", "male"),
 *       sarah     = new Person("Sarah", "female"),
 *       harry   = new Person("Harry Potter", "male"),
 *       ashley   = new Person("Ashley", "");
 *
 *   angular.module('msgFmtExample', ['ngMessageFormat'])
 *     .controller('AppController', ['$scope', function($scope) {
 *         $scope.people = [alice, bob, sarah, ashley];
 *         $scope.recipients = [alice, bob, sarah];
 *         $scope.sender = harry;
 *       }]);
 * </file>
 *
 * <file name="protractor.js" type="protractor">
 *   describe('MessageFormat plural', function() {
 *     function clickOptionCtrl(select, index) {
 *      element.all(by.css('select option')).then(function(arr) {
           browser.actions()
              .mouseMove(arr[index])
              .keyDown(protractor.Key.CONTROL)
              .click()
              .keyUp(protractor.Key.CONTROL)
              .perform();
        });
 *     }
 *
 *     it('should pluralize initial values', function() {
 *       var messageElem = element(by.binding('recipients.length')),
 *         select = element(by.css('select'));
 *
 *       expect(messageElem.getText()).toEqual('Harry Potter gave Alice and 2 other people a gift (#=2)');
         clickOptionCtrl(select, 2);
 *       expect(messageElem.getText()).toEqual('Harry Potter gave Alice and one other person a gift (#=1)');
         clickOptionCtrl(select, 1);
 *       expect(messageElem.getText()).toEqual('Harry Potter gave a gift to Alice (#=0)');
         clickOptionCtrl(select, 0);
 *       expect(messageElem.getText()).toEqual('Harry Potter gave no gifts (#=-1)');
 *     });
 *   });
 * </file>
 * </example>
 *
 * ### Plural and Gender together
 *
 * This example shows how you can specify gender rules for specific plural matches - in this case,
 * =1 is special cased for gender.
 * <example name="ngMessageFormat-example-plural-gender" module="msgFmtExample" deps="angular-message-format.js">
 *   <file name="index.html">
 *

       Select recipients:

       <select multiple size=5 ng-model="recipients" ng-options="person as person.name for person in people">
       </select>

{{recipients.length, plural,
          =0 {{{sender.name}} has not given any gifts to anyone.}
          =1 {  {{recipients[0].gender, select,
                 female { {{sender.name}} gave {{recipients[0].name}} her gift.}
                 male { {{sender.name}} gave {{recipients[0].name}} his gift.}
                 other { {{sender.name}} gave {{recipients[0].name}} their gift.}
                }}
              }
          other {{{sender.name}} gave {{recipients.length}} people gifts.}
          }}

      </file>
 *    <file name="script.js">
 *      function Person(name, gender) {
 *        this.name = name;
 *        this.gender = gender;
 *      }
 *
 *      var alice   = new Person("Alice", "female"),
 *          bob     = new Person("Bob", "male"),
 *          harry   = new Person("Harry Potter", "male"),
 *          ashley   = new Person("Ashley", "");
 *
 *      angular.module('msgFmtExample', ['ngMessageFormat'])
 *        .controller('AppController', ['$scope', function($scope) {
 *            $scope.people = [alice, bob, ashley];
 *            $scope.recipients = [alice];
 *            $scope.sender = harry;
 *          }]);
 *    </file>
    </example>
 */

var $$MessageFormatFactory = ['$parse', '$locale', '$sce', '$exceptionHandler', function $$messageFormat(
                   $parse,   $locale,   $sce,   $exceptionHandler) {

  function getStringifier(trustedContext, allOrNothing, text) {
    return function stringifier(value) {
      try {
        value = trustedContext ? $sce['getTrusted'](trustedContext, value) : $sce['valueOf'](value);
        return allOrNothing && (value === void 0) ? value : stringify(value);
      } catch (err) {
        $exceptionHandler($interpolateMinErr['interr'](text, err));
      }
    };
  }

  function interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
    var stringifier = getStringifier(trustedContext, allOrNothing, text);
    var parser = new MessageFormatParser(text, 0, $parse, $locale['pluralCat'], stringifier,
                                         mustHaveExpression, trustedContext, allOrNothing);
    parser.run(parser.ruleInterpolate);
    return parser.parsedFn;
  }

  return {
    'interpolate': interpolate
  };
}];

var $$interpolateDecorator = ['$$messageFormat', '$delegate', function $$interpolateDecorator($$messageFormat, $interpolate) {
  if ($interpolate['startSymbol']() !== "{{" || $interpolate['endSymbol']() !== "}}") {
    throw $interpolateMinErr('nochgmustache', 'angular-message-format.js currently does not allow you to use custom start and end symbols for interpolation.');
  }
  var interpolate = $$messageFormat['interpolate'];
  interpolate['startSymbol'] = $interpolate['startSymbol'];
  interpolate['endSymbol'] = $interpolate['endSymbol'];
  return interpolate;
}];

var module = window['angular']['module']('ngMessageFormat', ['ng']);
module['factory']('$$messageFormat', $$MessageFormatFactory);
module['config'](['$provide', function($provide) {
  $provide['decorator']('$interpolate', $$interpolateDecorator);
}]);<commit_msg>style(ngMessageFormat): fix jscs whitespace error<commit_after>'use strict';

// NOTE: ADVANCED_OPTIMIZATIONS mode.
//
// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using
// constructs incompatible with that mode.

/* global $interpolateMinErr: false */
/* global MessageFormatParser: false */
/* global stringify: false */

/**
 * @ngdoc module
 * @name ngMessageFormat
 * @packageName angular-message-format
 *
 * @description
 *
 * ## What is  ngMessageFormat?
 *
 * The ngMessageFormat module extends the Angular {@link ng.$interpolate `$interpolate`} service
 * with a syntax for handling pluralization and gender specific messages, which is based on the
 * [ICU MessageFormat syntax][ICU].
 *
 * See [the design doc][ngMessageFormat doc] for more information.
 *
 * [ICU]: http://userguide.icu-project.org/formatparse/messages#TOC-MessageFormat
 * [ngMessageFormat doc]: https://docs.google.com/a/google.com/document/d/1pbtW2yvtmFBikfRrJd8VAsabiFkKezmYZ_PbgdjQOVU/edit
 *
 * ## Examples
 *
 * ### Gender
 *
 * This example uses the "select" keyword to specify the message based on gender.
 *
 * <example name="ngMessageFormat-example-gender" module="msgFmtExample" deps="angular-message-format.js">
 * <file name="index.html">
 *

 *    Select Recipient:

      <select ng-model="recipient" ng-options="person as person.name for person in recipients">
      </select>

{{recipient.gender, select,
                male {{{recipient.name}} unwrapped his gift. }
                female {{{recipient.name}} unwrapped her gift. }
                other {{{recipient.name}} unwrapped their gift. }
      }}

 *

 * </file>
 * <file name="script.js">
 *   function Person(name, gender) {
 *     this.name = name;
 *     this.gender = gender;
 *   }
 *
 *   var alice   = new Person("Alice", "female"),
 *       bob     = new Person("Bob", "male"),
 *       ashley = new Person("Ashley", "");
 *
 *   angular.module('msgFmtExample', ['ngMessageFormat'])
 *     .controller('AppController', ['$scope', function($scope) {
 *         $scope.recipients = [alice, bob, ashley];
 *         $scope.recipient = $scope.recipients[0];
 *       }]);
 * </file>
 * </example>
 *
 * ### Plural
 *
 * This example shows how the "plural" keyword is used to account for a variable number of entities.
 * The "#" variable holds the current number and can be embedded in the message.
 *
 * Note that "=1" takes precedence over "one".
 *
 * The example also shows the "offset" keyword, which allows you to offset the value of the "#" variable.
 *
 * <example name="ngMessageFormat-example-plural" module="msgFmtExample" deps="angular-message-format.js">
 * <file name="index.html">
 *

      Select recipients:

      <select multiple size=5 ng-model="recipients" ng-options="person as person.name for person in people">
      </select>

 *
{{recipients.length, plural, offset:1
 *             =0    {{{sender.name}} gave no gifts (\#=#)}
 *             =1    {{{sender.name}} gave a gift to {{recipients[0].name}} (\#=#)}
 *             one   {{{sender.name}} gave {{recipients[0].name}} and one other person a gift (\#=#)}
 *             other {{{sender.name}} gave {{recipients[0].name}} and # other people a gift (\#=#)}
 *           }}

 *

 * </file>
 *
 * <file name="script.js">
 *   function Person(name, gender) {
 *     this.name = name;
 *     this.gender = gender;
 *   }
 *
 *   var alice   = new Person("Alice", "female"),
 *       bob     = new Person("Bob", "male"),
 *       sarah     = new Person("Sarah", "female"),
 *       harry   = new Person("Harry Potter", "male"),
 *       ashley   = new Person("Ashley", "");
 *
 *   angular.module('msgFmtExample', ['ngMessageFormat'])
 *     .controller('AppController', ['$scope', function($scope) {
 *         $scope.people = [alice, bob, sarah, ashley];
 *         $scope.recipients = [alice, bob, sarah];
 *         $scope.sender = harry;
 *       }]);
 * </file>
 *
 * <file name="protractor.js" type="protractor">
 *   describe('MessageFormat plural', function() {
 *     function clickOptionCtrl(select, index) {
 *      element.all(by.css('select option')).then(function(arr) {
           browser.actions()
              .mouseMove(arr[index])
              .keyDown(protractor.Key.CONTROL)
              .click()
              .keyUp(protractor.Key.CONTROL)
              .perform();
        });
 *     }
 *
 *     it('should pluralize initial values', function() {
 *       var messageElem = element(by.binding('recipients.length')),
 *         select = element(by.css('select'));
 *
 *       expect(messageElem.getText()).toEqual('Harry Potter gave Alice and 2 other people a gift (#=2)');
         clickOptionCtrl(select, 2);
 *       expect(messageElem.getText()).toEqual('Harry Potter gave Alice and one other person a gift (#=1)');
         clickOptionCtrl(select, 1);
 *       expect(messageElem.getText()).toEqual('Harry Potter gave a gift to Alice (#=0)');
         clickOptionCtrl(select, 0);
 *       expect(messageElem.getText()).toEqual('Harry Potter gave no gifts (#=-1)');
 *     });
 *   });
 * </file>
 * </example>
 *
 * ### Plural and Gender together
 *
 * This example shows how you can specify gender rules for specific plural matches - in this case,
 * =1 is special cased for gender.
 * <example name="ngMessageFormat-example-plural-gender" module="msgFmtExample" deps="angular-message-format.js">
 *   <file name="index.html">
 *

       Select recipients:

       <select multiple size=5 ng-model="recipients" ng-options="person as person.name for person in people">
       </select>

{{recipients.length, plural,
          =0 {{{sender.name}} has not given any gifts to anyone.}
          =1 {  {{recipients[0].gender, select,
                 female { {{sender.name}} gave {{recipients[0].name}} her gift.}
                 male { {{sender.name}} gave {{recipients[0].name}} his gift.}
                 other { {{sender.name}} gave {{recipients[0].name}} their gift.}
                }}
              }
          other {{{sender.name}} gave {{recipients.length}} people gifts.}
          }}

      </file>
 *    <file name="script.js">
 *      function Person(name, gender) {
 *        this.name = name;
 *        this.gender = gender;
 *      }
 *
 *      var alice   = new Person("Alice", "female"),
 *          bob     = new Person("Bob", "male"),
 *          harry   = new Person("Harry Potter", "male"),
 *          ashley   = new Person("Ashley", "");
 *
 *      angular.module('msgFmtExample', ['ngMessageFormat'])
 *        .controller('AppController', ['$scope', function($scope) {
 *            $scope.people = [alice, bob, ashley];
 *            $scope.recipients = [alice];
 *            $scope.sender = harry;
 *          }]);
 *    </file>
    </example>
 */

var $$MessageFormatFactory = ['$parse', '$locale', '$sce', '$exceptionHandler', function $$messageFormat(
                   $parse,   $locale,   $sce,   $exceptionHandler) {

  function getStringifier(trustedContext, allOrNothing, text) {
    return function stringifier(value) {
      try {
        value = trustedContext ? $sce['getTrusted'](trustedContext, value) : $sce['valueOf'](value);
        return allOrNothing && (value === void 0) ? value : stringify(value);
      } catch (err) {
        $exceptionHandler($interpolateMinErr['interr'](text, err));
      }
    };
  }

  function interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
    var stringifier = getStringifier(trustedContext, allOrNothing, text);
    var parser = new MessageFormatParser(text, 0, $parse, $locale['pluralCat'], stringifier,
                                         mustHaveExpression, trustedContext, allOrNothing);
    parser.run(parser.ruleInterpolate);
    return parser.parsedFn;
  }

  return {
    'interpolate': interpolate
  };
}];

var $$interpolateDecorator = ['$$messageFormat', '$delegate', function $$interpolateDecorator($$messageFormat, $interpolate) {
  if ($interpolate['startSymbol']() !== "{{" || $interpolate['endSymbol']() !== "}}") {
    throw $interpolateMinErr('nochgmustache', 'angular-message-format.js currently does not allow you to use custom start and end symbols for interpolation.');
  }
  var interpolate = $$messageFormat['interpolate'];
  interpolate['startSymbol'] = $interpolate['startSymbol'];
  interpolate['endSymbol'] = $interpolate['endSymbol'];
  return interpolate;
}];

var module = window['angular']['module']('ngMessageFormat', ['ng']);
module['factory']('$$messageFormat', $$MessageFormatFactory);
module['config'](['$provide', function($provide) {
  $provide['decorator']('$interpolate', $$interpolateDecorator);
}]);
<|endoftext|>


<commit_before>'use strict';
/*
 * EnOcean Serial Node for Packet Type 10: RADIO_ERP2
 * Supported Protocols (Partially):
 * - EnOcean Serial Protocol 3 (ESP3) V1.27 / July 30, 2014
 * - EnOcean Radio Protocol 2 SPECIFICATION V1.0 September 26, 2013
 *
 * This node expects 'node-red-node-serialport' to be available
 * on the editor in order to use its `/serialports` endpoint.
 */

import { SerialPool } from './lib/enocean';
import { ERP2_HANDLERS } from './lib/eep_handlers';

export default function(RED) {
  function EnOceanPortNode(n) {
    RED.nodes.createNode(this, n);
    let that = this;
    try {
      that.serialPort = n.serialPort;
      EnOceanPortNode.pool.add(that);
    } catch (e) {
      RED.log.warn(RED._('enocean.errors.serialPortError', { error: e }));
    }
  }
  EnOceanPortNode.pool = new SerialPool(RED);
  RED.nodes.registerType('EnOcean Port', EnOceanPortNode);

  function EnOceanInNode(n) {
    RED.nodes.createNode(this, n);
    let that = this;
    that.name = n.name;
    that.originatorId = n.originatorId;
    that.eepType = n.eepType;
    that.addEepType = n.addEepType;
    that.useString = n.useString;
    that.enoceanPortNodeId = n.enoceanPort;
    that.enoceanPortNode = RED.nodes.getNode(that.enoceanPortNodeId);
    that.on('close', done => {
      if (that.enoceanPortNode) {
        EnOceanPortNode.pool.close(that.enoceanPortNode.serialPort).then(() => {
          done();
        });
      } else {
        done();
      }
    });
    try {
      let enocean = EnOceanPortNode.pool.get(that.enoceanPortNode.serialPort);
      enocean.port.on(`ctx-${that.originatorId}`, ctx => {
        let handleIt = ERP2_HANDLERS[that.eepType];
        if (!handleIt) {
          RED.log.warn(RED._('enocean.warn.noHandler', { eepType: that.eepType }));
          return;
        }
        let payload = handleIt(ctx);
        payload.tstamp = Date.now();
        payload.rssi = ctx.container.dBm;
        payload.id = ctx.originatorId;
        if (that.addEepType) {
          payload.eep = that.eepType;
        }
        if (that.useString) {
          payload = JSON.stringify(payload);
        }
        that.send({ payload: payload });
      });
      enocean.port.on('ready', () => {
        that.status({ fill: 'green', shape: 'dot', text: 'node-red:common.status.connected'});
      });
      enocean.port.on('closed', () => {
        that.status({ fill: 'red', shape: 'ring', text: 'node-red:common.status.not-connected'});
      });
    } catch (e) {
      RED.log.warn(RED._('enocean.errors.serialPortError', { error: e }));
    }
  }
  RED.nodes.registerType('EnOcean in', EnOceanInNode);

  RED.httpAdmin.get('/eeps', RED.auth.needsPermission('eep.read'), function(req,res) {
    res.json(Object.keys(ERP2_HANDLERS));
  });

}
<commit_msg>Translate ES5 classes intro ES6 classes<commit_after>'use strict';
/*
 * EnOcean Serial Node for Packet Type 10: RADIO_ERP2
 * Supported Protocols (Partially):
 * - EnOcean Serial Protocol 3 (ESP3) V1.27 / July 30, 2014
 * - EnOcean Radio Protocol 2 SPECIFICATION V1.0 September 26, 2013
 *
 * This node expects 'node-red-node-serialport' to be available
 * on the editor in order to use its `/serialports` endpoint.
 */

import { SerialPool } from './lib/enocean';
import { ERP2_HANDLERS } from './lib/eep_handlers';

export default function(RED) {
  class EnOceanPortNode {
    constructor(n) {
      RED.nodes.createNode(this, n);
      try {
        this.serialPort = n.serialPort;
        EnOceanPortNode.pool.add(this);
      } catch (e) {
        RED.log.warn(RED._('enocean.errors.serialPortError', { error: e }));
      }
    }
  }
  EnOceanPortNode.pool = new SerialPool(RED);
  RED.nodes.registerType('EnOcean Port', EnOceanPortNode);

  class EnOceanInNode {
    constructor(n) {
      RED.nodes.createNode(this, n);
      this.name = n.name;
      this.originatorId = n.originatorId;
      this.eepType = n.eepType;
      this.addEepType = n.addEepType;
      this.useString = n.useString;
      this.enoceanPortNodeId = n.enoceanPort;
      this.enoceanPortNode = RED.nodes.getNode(this.enoceanPortNodeId);
      this.on('close', done => {
        if (this.enoceanPortNode) {
          EnOceanPortNode.pool.close(this.enoceanPortNode.serialPort).then(() => {
            done();
          });
        } else {
          done();
        }
      });
      try {
        let enocean = EnOceanPortNode.pool.get(this.enoceanPortNode.serialPort);
        enocean.port.on(`ctx-${this.originatorId}`, ctx => {
          let handleIt = ERP2_HANDLERS[this.eepType];
          if (!handleIt) {
            RED.log.warn(RED._('enocean.warn.noHandler', { eepType: this.eepType }));
            return;
          }
          let payload = handleIt(ctx);
          payload.tstamp = Date.now();
          payload.rssi = ctx.container.dBm;
          payload.id = ctx.originatorId;
          if (this.addEepType) {
            payload.eep = this.eepType;
          }
          if (this.useString) {
            payload = JSON.stringify(payload);
          }
          this.send({ payload: payload });
        });
        enocean.port.on('ready', () => {
          this.status({ fill: 'green', shape: 'dot', text: 'node-red:common.status.connected'});
        });
        enocean.port.on('closed', () => {
          this.status({ fill: 'red', shape: 'ring', text: 'node-red:common.status.not-connected'});
        });
      } catch (e) {
        RED.log.warn(RED._('enocean.errors.serialPortError', { error: e }));
      }
    }
  }
  RED.nodes.registerType('EnOcean in', EnOceanInNode);

  RED.httpAdmin.get('/eeps', RED.auth.needsPermission('eep.read'), function(req,res) {
    res.json(Object.keys(ERP2_HANDLERS));
  });
}
<|endoftext|>


<commit_before>/**
 * Client class that talks to bugzilla
 */

const BZ_DOMAIN = "https://bugzilla.mozilla.org";
const REST_BUG  = "/rest/bug";

export default {
  fetch: function(options) {
    return this._fetchFromUrl(this._optionsToUrl(options));
  },

  _optionsToUrl: function(options) {
    if (!this.bzParams.isWhiteListed(Object.keys(options))) {
      throw "Invalid Bugzilla parameter.";
    }

    let url = BZ_DOMAIN + REST_BUG + "?";
    for (let key of Object.keys(options)) {
      url += key + "=" + options[key] + "&";
    }
    return url.substr(0, url.length-1);
  },

  _fetchFromUrl: function(url) {
    return new Promise(function(resolve, reject) {
      let req = new XMLHttpRequest();
      req.open('GET', url);
      req.setRequestHeader("Content-type", "application/json");
      req.setRequestHeader("Accept", "application/json");

      req.onload = function() {
        if (req.status == 200) {
          try {
            resolve(JSON.parse(req.response).bugs);
          } catch (e) {
            console.log("Failed to parse the request bugs");
            reject(Error(e));
          }
        }
        else {
          reject(Error(req.statusText));
        }
      };
      req.onerror = function() {
        reject(Error("Network Error"));
      };
      req.send();
    });
  },

  _requestResponse: function() {
    console.log(this.responseText);
  },

  bzParams : {
    PARAMS_WHITELIST: [
      "alias",
      "assigned_to",
      "component",
      "creation_time",
      "creator",
      "id",
      "last_change_time",
      "limit",
      "offset",
      "op_sys",
      "platform",
      "priority",
      "product",
      "resolution",
      "severity",
      "status",
      "summary",
      "tags",
      "target_milestone",
      "qa_contact",
      "url",
      "version",
      "whiteboard",
      "quicksearch",
    ],

    isWhiteListed: function(params) {
      if (!Array.isArray(params)) {
        params = [params];
      }
      return params.every(p => this.PARAMS_WHITELIST.some(w => w == p));
    },
  },
}
<commit_msg>BzClient uses quicksearch only<commit_after>/**
 * Client class that talks to bugzilla
 */

const BZ_DOMAIN = "https://bugzilla.mozilla.org";
const REST_BUG  = "/rest/bug";

export default {
  fetch: function(filter) {
    return this._fetchFromUrl(this._filterToUrl(filter));
  },

  _filterToUrl: function (filter) {
    return BZ_DOMAIN + REST_BUG + "?quicksearch=" + filter.value;
  },

  _fetchFromUrl: function(url) {
    return new Promise(function(resolve, reject) {
      let req = new XMLHttpRequest();
      req.open('GET', url);
      req.setRequestHeader("Content-type", "application/json");
      req.setRequestHeader("Accept", "application/json");

      req.onload = function() {
        if (req.status == 200) {
          try {
            resolve(JSON.parse(req.response).bugs);
          } catch (e) {
            console.log("Failed to parse the request bugs");
            reject(Error(e));
          }
        }
        else {
          reject(Error(req.statusText));
        }
      };
      req.onerror = function() {
        reject(Error("Network Error"));
      };
      req.send();
    });
  }
}
<|endoftext|>


<commit_before>const fs = require('fs');
const { exec } = require('child_process');
const customVersion = process.argv[2] || ''; // allows users to enter a specific node version

/**
 * @desc Parse out the most recent version of node available to nvm. It may be
 * possible that the last line in versionStr is "N/A" if version is invalid.
 * @param {String} versionsStr - output of "nvm ls-remote --no-colors ${version}"
 * @returns {String} - most recent node version, or "N/A"
 */
function parseVersion(versionsStr) {
    const semanticVersionRegEx = /\d+.\d+.\d+/;
    const versionsArray = versionsStr.split('\n');
    let version = versionsArray.pop().trim();
    if (!version) {
        // if version is a blank line
        version = versionsArray.pop().trim();
    }
    const matchGroup = version.match(semanticVersionRegEx);
    return matchGroup ? matchGroup[0] : version;
}

/**
 * @name getRecentNodeVersion
 * @desc Finds the most recent version of node available to nvm
 * @returns {Promise} - resolves with the latest version number, or rejects with
 * an error string.
 */
function getRecentNodeVersion(customVersion) {
    const versionPromise = new Promise((resolve, reject) => {
        const versions = `source ~/.nvm/nvm.sh && nvm ls-remote --no-colors ${customVersion}`;
        exec(versions, function(err, stdout, stderr) {
            if (err) {
                let msg = `${err.message}\
                    \nMake sure you are connected to the internet.`;
                if (customVersion) {
                    msg += `\nVerify that "${customVersion}" is a valid nodeJS version.`;
                }
                reject(msg);
            }
            // is it safe to assume that parseVersion will never return N/A????
            const version = parseVersion(stdout);
            if (version !== 'N/A') {
                resolve(version);
            } else {
                const msg = `Error: nodeJS version "${customVersion}" not found!\
                    \nRun 'nvm ls-remote'\to confirm your requested version exists.`;
                reject(msg);
            }
        });
    });
    return versionPromise;
}

/**
 * @name createVsCodeConfig
 * @desc Creates a VS Code config for debugging against the latest node version.
 * @param {String} version - node version number
 * @returns {Promised} - will resolve to a success launch.json created message, or
 * reject with the error message.
 */
function createVsCodeConfig(version) {
    const vscodeDir = '.vscode';
    const launchFilePath = [vscodeDir, 'launch.json'].join('/');
    const getNodePath = `source $HOME/.nvm/nvm-exec\
        \necho ${version} > .nvmrc\
        \nnvm use > /dev/null\
        \nnvm which ${version}`;
    const configPromise = new Promise((resolve, reject) => {
        exec(getNodePath, function(err, localNodePath, stderr) {
            if (err) {
                reject(err.message);
            }
            const launchFileTemplate = createLaunchJsonTemplate(localNodePath.trim());
            if (!fs.existsSync(vscodeDir)) {
                fs.mkdirSync('.vscode');
            }
            fs.writeFile(launchFilePath, launchFileTemplate, err => {
                if (err) {
                    reject(err.message);
                } else {
                    resolve(`VS Code can now debug this project with nodeJS version "${version}"`);
                }
            });
        });
    });
    return configPromise;
}

function createLaunchJsonTemplate(nodePath) {
    const launchFileTemplate = `\
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Lanuch to Process",
      "program": "\$\{file\}",
      "protocol": "auto",
      "runtimeExecutable": "${nodePath}"
    }
  ]
}`;
    return launchFileTemplate;
}

module.exports = {
    parseVersion,
    getRecentNodeVersion,
    createVsCodeConfig
};
<commit_msg>used nvm.sh instead of nvm-exec<commit_after>const fs = require('fs');
const { exec } = require('child_process');
const customVersion = process.argv[2] || ''; // allows users to enter a specific node version

/**
 * @desc Parse out the most recent version of node available to nvm. It may be
 * possible that the last line in versionStr is "N/A" if version is invalid.
 * @param {String} versionsStr - output of "nvm ls-remote --no-colors ${version}"
 * @returns {String} - most recent node version, or "N/A"
 */
function parseVersion(versionsStr) {
    const semanticVersionRegEx = /\d+.\d+.\d+/;
    const versionsArray = versionsStr.split('\n');
    let version = versionsArray.pop().trim();
    if (!version) {
        // if version is a blank line
        version = versionsArray.pop().trim();
    }
    const matchGroup = version.match(semanticVersionRegEx);
    return matchGroup ? matchGroup[0] : version;
}

/**
 * @name getRecentNodeVersion
 * @desc Finds the most recent version of node available to nvm
 * @returns {Promise} - resolves with the latest version number, or rejects with
 * an error string.
 */
function getRecentNodeVersion(customVersion) {
    const versionPromise = new Promise((resolve, reject) => {
        const versions = `source ~/.nvm/nvm.sh && nvm ls-remote --no-colors ${customVersion}`;
        exec(versions, function(err, stdout, stderr) {
            if (err) {
                let msg = `${err.message}\
                    \nMake sure you are connected to the internet.`;
                if (customVersion) {
                    msg += `\nVerify that "${customVersion}" is a valid nodeJS version.`;
                }
                reject(msg);
            }
            // is it safe to assume that parseVersion will never return N/A????
            const version = parseVersion(stdout);
            if (version !== 'N/A') {
                resolve(version);
            } else {
                const msg = `Error: nodeJS version "${customVersion}" not found!\
                    \nRun 'nvm ls-remote'\to confirm your requested version exists.`;
                reject(msg);
            }
        });
    });
    return versionPromise;
}

/**
 * @name createVsCodeConfig
 * @desc Creates a VS Code config for debugging against the latest node version.
 * @param {String} version - node version number
 * @returns {Promised} - will resolve to a success launch.json created message, or
 * reject with the error message.
 */
function createVsCodeConfig(version) {
    const vscodeDir = '.vscode';
    const launchFilePath = [vscodeDir, 'launch.json'].join('/');
    const getNodePath = `source $HOME/.nvm/nvm.sh\
        \necho ${version} > .nvmrc\
        \nnvm use > /dev/null\
        \nnvm which ${version}`;
    const configPromise = new Promise((resolve, reject) => {
        exec(getNodePath, function(err, localNodePath, stderr) {
            if (err) {
                reject(err.message);
            }
            const launchFileTemplate = createLaunchJsonTemplate(localNodePath.trim());
            if (!fs.existsSync(vscodeDir)) {
                fs.mkdirSync('.vscode');
            }
            fs.writeFile(launchFilePath, launchFileTemplate, err => {
                if (err) {
                    reject(err.message);
                } else {
                    resolve(`VS Code can now debug this project with nodeJS version "${version}"`);
                }
            });
        });
    });
    return configPromise;
}

function createLaunchJsonTemplate(nodePath) {
    const launchFileTemplate = `\
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Lanuch to Process",
      "program": "\$\{file\}",
      "protocol": "auto",
      "runtimeExecutable": "${nodePath}"
    }
  ]
}`;
    return launchFileTemplate;
}

module.exports = {
    parseVersion,
    getRecentNodeVersion,
    createVsCodeConfig
};
<|endoftext|>


<commit_before>export const command = 'secret'
export const desc = 'Get/Set secret values'
export const builder = yargs => yargs.commandDir('secret-cmds')
export const handler = argv => {}
<commit_msg>Require a subcommand for secret<commit_after>export const command = 'secret'
export const desc = 'Get/Set secret values'
export const builder = yargs => yargs
  .commandDir('secret-cmds')
  .demandCommand()
<|endoftext|>