<commit_before>(function($) {
  var config = {
    urls: {
      pivotal: {
        api: 'https://www.pivotaltracker.com/services/v5',
      }
    }
  };

  /**
   * removeDupesFromArray returns an array without dupes
   * @param  {Array} arr Any array
   * @return {Array}     Array without dups
   */
  function removeDupesFromArray(arr) {
    return arr.filter(function(elem, i) {
      return arr.indexOf(elem) == i;
    });
  }

  /**
   * mapStatusLabelClass returns label class type (as of Twitter Bootstrap 3)
   * @param  {string} status Pivotal story status
   * @return {string}        Bootstrap class name
   */
  function mapStatusLabelClass(status) {
    switch(status) {
      case 'rejected':
        return 'danger';
      case 'accepted':
        return 'success';
      case 'delivered':
        return 'warning';
      case 'finished':
        return 'primary';
      case 'started':
        return 'info';
      default:
        return 'default';
    }
  }

  /**
   * getProjectDiffs return an Object, project name as the key and github diff URLs
   * as the value
   * @return {Object} {project_name: diff_url}
   */
  function getProjectDiffs() {
    var diffURLs = {};

    $('.project').each(function() {
      var $this = $(this);
      var project = $this.data('id');
      var url = $this.find('.GitHubDiffURL').attr('href');

      if (url) {
        diffURLs[project] = url;
      }
    });

    return diffURLs;
  }

  /**
   * getGithubCommits returns Github commits for a given repository
   * @param  {String}   github_token Github token
   * @param  {String}   repo         Github repository name
   * @param  {String}   commitHashes Current and Latest commit hash
   * @param  {Function} callback
   */
  function getGithubCommits(github_token, repo, commitHashes, callback) {
    $.ajax({
      url: 'https://api.github.com/repos/gengo/'+ repo +'/compare/'+ commitHashes[0] +'...'+ commitHashes[1],
      type: 'GET',
      headers: {
        'Authorization': 'token '+ github_token
      },
      success: function(data) {
        callback(data.commits);
      },
      error: function(err) {
        console.error(err);
      }
    });
  }

  /**
   * getPivotalStoryIDs return an array of Pivotal story IDs from array of messages
   * @param  {Array} msgs Array of string messages
   * @return {Array}      Array of pivotal IDs
   */
  function getPivotalStoryIDs(msgs) {
    var storyIDs = msgs.map(function(str) {
      return parseInt(str.match(/\[#(\d+)\]/)[1], 10);
    });

    return removeDupesFromArray(storyIDs);
  }

  /**
   * getPivotalStoryInfo return pivotal story data
   * @param  {string}   pt_token Pivotal token
   * @param  {number}   story_id Story id
   * @param  {Function} callback
   */
  function getPivotalStoryInfo(pt_token, story_id, callback) {
    $.ajax({
      url: config.urls.pivotal.api + '/stories/' + story_id,
      type: 'GET',
      headers: {
        'X-TrackerToken': pt_token
      },
      success: function(data) {
        getRepoDependencies(pt_token, data.project_id, story_id, function(list) {
          callback({
            id: story_id,
            url: data.url,
            status: data.current_state,
            dependencies: list
          });
        });
      }
    });
  }

  /**
   * getRepoDependencies return a list of dependencies for stroy in a project
   * @param  {String}   pt_token   Pivotal token
   * @param  {number}   project_id Project ID
   * @param  {number}   story_id   Story ID
   * @param  {Function} callback   Returns Pivotal story ID list
   */
  function getRepoDependencies(pt_token, project_id, story_id, callback) {
    $.ajax({
      url: config.urls.pivotal.api + '/projects/' + project_id + '/stories/' + story_id + '/comments',
      type: 'GET',
      headers: {
        'X-TrackerToken': pt_token
      },
      success: function(data) {
        // Find all git commit messages
        var list = data.filter(function(obj) {
          return obj.commit_type;
        });
        // Find repo name from comment body text
        list = list.map(function(item) {
          return item.text.split('https://github.com/gengo/')[1].split('/commit/')[0];
        });

        callback(removeDupesFromArray(list));
      }
    });
  }

  /**
   * onGetPivotalStoryInfoComplete is triggered when pitotal info loading completted,
   * and building up HTML to insert into the Story block.
   * @param  {String} project   GitHub project name
   * @param  {Array} storyList  Pivotal story info array
   */
  function onGetPivotalStoryInfoComplete(project, storyList) {
    var html = storyList.map(function(story) {
      return '
 \
                #'+ story.id +' \
                  \
                '+ story.status +' \
                  \
                '+ story.dependencies.length +' \

';
    });

    $('.project[data-id="'+ project +'"]')
      .find('.story')
        .append(html.join(''))
      .find('[data-toggle="popover"]')
        .popover({
          html: true,
          trigger: 'hover',
          placement: 'top'
        });
  }

  /**
   * showNoStoriesMessage
   * @param  {jQuery} $target jQuery button object
   */
  function showNoStoriesMessage($target) {
    $target.closest('.story').text('No stories found.');
  }

  $(document).ready(function() {
    // add button to story columns
    var button = '<button class="btn btn-default getStories">Get stories</button>Loading...';
    $('.project .story').each(function() {
      $(this).html(button);
    });

    // When reset button clicked add Get stories button
    $('.refresh').click(function() {
      $(this).closest('.project').find('.story').html(button);
    });

    $('.project').on('click', '.getStories', function(e) {
      e.preventDefault();

      var $this_button = $(e.currentTarget);
      var project = $this_button.parents('.project').data('id');
      var diffs = getProjectDiffs();

      $this_button.hide();

      if (project in diffs) {
        $this_button.siblings('.loading').show();

        var url = diffs[project];
        // hashes: currentCommit...latestCommit
        var commitHashes = (url.substr(url.lastIndexOf('/') + 1)).split('...');
        var repositoryName = url.match(/.*\/(.*)\/compare/)[1];

        getGithubCommits(GITHUB_TOKEN, repositoryName, commitHashes, function(commits) {
          // Array of comitt messages
          var messages = commits.map(function(obj) {
            return obj.commit.message;
          });
          // Array of pivotal story IDs
          var pivotal_ids = getPivotalStoryIDs(messages);
          if (!pivotal_ids) {
            showNoStoriesMessage($this_button);
            return;
          };

          var storyList = [];
          for (var i = 0, imax = pivotal_ids.length; i < imax; i++) {
            getPivotalStoryInfo(PIVOTAL_TOKEN, pivotal_ids[i], function(info) {
              storyList.push(info);

              if (storyList.length === pivotal_ids.length) {
                $this_button.siblings('.loading').hide();
                onGetPivotalStoryInfoComplete(project, storyList);
              }
            });
          }
        });
      }
      else {
        showNoStoriesMessage($this_button);
      }
    });

  });
}(jQuery));
<commit_msg>refactoring on feedback<commit_after>(function($) {
  var config = {
    urls: {
      pivotal: {
        api: 'https://www.pivotaltracker.com/services/v5',
      }
    }
  };

  /**
   * removeDupesFromArray returns an array without dupes
   * @param  {Array} arr Any array
   * @return {Array}     Array without dups
   */
  function removeDupesFromArray(arr) {
    return arr.filter(function(elem, i) {
      return arr.indexOf(elem) == i;
    });
  }

  /**
   * mapStatusLabelClass returns label class type (as of Twitter Bootstrap 3)
   * @param  {string} status Pivotal story status
   * @return {string}        Bootstrap class name
   */
  function mapStatusLabelClass(status) {
    switch(status) {
      case 'rejected':
        return 'danger';
      case 'accepted':
        return 'success';
      case 'delivered':
        return 'warning';
      case 'finished':
        return 'primary';
      case 'started':
        return 'info';
      default:
        return 'default';
    }
  }

  /**
   * getProjectDiffs return an Object, project name as the key and github diff URLs
   * as the value
   * @return {Object} {project_name: diff_url}
   */
  function getProjectDiffs() {
    var diffURLs = {};

    $('.project').each(function() {
      var $this = $(this);
      var project = $this.data('id');
      var url = $this.find('.GitHubDiffURL').attr('href');

      if (url) {
        diffURLs[project] = url;
      }
    });

    return diffURLs;
  }

  /**
   * getGithubCommits returns Github commits for a given repository
   * @param  {String}   github_token Github token
   * @param  {String}   repo         Github repository name
   * @param  {String}   commitHashe  Current and Latest commit hash
   * @param  {Function} callback
   */
  function getGithubCommits(github_token, repo, commitHashe, callback) {
    $.ajax({
      url: 'https://api.github.com/repos/gengo/'+ repo +'/compare/'+ commitHashes,
      type: 'GET',
      headers: {
        'Authorization': 'token '+ github_token
      },
      success: function(data) {
        callback(data.commits);
      },
      error: function(err) {
        console.error(err);
      }
    });
  }

  /**
   * getPivotalStoryIDs return an array of Pivotal story IDs from array of messages
   * @param  {Array} msgs Array of string messages
   * @return {Array}      Array of pivotal IDs
   */
  function getPivotalStoryIDs(msgs) {
    var storyIDs = msgs.map(function(str) {
      return parseInt(str.match(/\[#(\d+)\]/)[1], 10);
    });

    return removeDupesFromArray(storyIDs);
  }

  /**
   * getPivotalStoryInfo return pivotal story data
   * @param  {string}   pt_token Pivotal token
   * @param  {number}   story_id Story id
   * @param  {Function} callback
   */
  function getPivotalStoryInfo(pt_token, story_id, callback) {
    $.ajax({
      url: config.urls.pivotal.api + '/stories/' + story_id,
      type: 'GET',
      headers: {
        'X-TrackerToken': pt_token
      },
      success: function(data) {
        getRepoDependencies(pt_token, data.project_id, story_id, function(list) {
          callback({
            id: story_id,
            url: data.url,
            status: data.current_state,
            dependencies: list
          });
        });
      }
    });
  }

  /**
   * getRepoDependencies return a list of dependencies for stroy in a project
   * @param  {String}   pt_token   Pivotal token
   * @param  {number}   project_id Project ID
   * @param  {number}   story_id   Story ID
   * @param  {Function} callback   Returns Pivotal story ID list
   */
  function getRepoDependencies(pt_token, project_id, story_id, callback) {
    $.ajax({
      url: config.urls.pivotal.api + '/projects/' + project_id + '/stories/' + story_id + '/comments',
      type: 'GET',
      headers: {
        'X-TrackerToken': pt_token
      },
      success: function(data) {
        // Find all git commit messages
        var list = data.filter(function(obj) {
          return obj.commit_type;
        });
        // Find repo name from comment body text
        list = list.map(function(item) {
          return item.text.split('https://github.com/gengo/')[1].split('/commit/')[0];
        });

        callback(removeDupesFromArray(list));
      }
    });
  }

  /**
   * onGetPivotalStoryInfoComplete is triggered when pitotal info loading completted,
   * and building up HTML to insert into the Story block.
   * @param  {String} project   GitHub project name
   * @param  {Array} storyList  Pivotal story info array
   */
  function onGetPivotalStoryInfoComplete(project, storyList) {
    var html = storyList.map(function(story) {
      return '
 \
                #'+ story.id +' \
                  \
                '+ story.status +' \
                  \
                '+ story.dependencies.length +' \

';
    });

    $('.project[data-id="'+ project +'"]')
      .find('.story')
        .html(html)
      .find('[data-toggle="popover"]')
        .popover({
          html: true,
          trigger: 'hover',
          placement: 'top'
        });
  }

  /**
   * showNoStoriesMessage
   * @param  {jQuery} $target jQuery button object
   */
  function showNoStoriesMessage($target) {
    $target.closest('.story').text('No stories found.');
  }

  $(document).ready(function() {
    // add button to story columns
    var button = '<button class="btn btn-default getStories">Get stories</button>Loading...';
    $('.project .story').each(function() {
      $(this).html(button);
    });

    // When reset button clicked add Get stories button
    $('.refresh').click(function() {
      $(this).closest('.project').find('.story').html(button);
    });

    $('.project').on('click', '.getStories', function(e) {
      e.preventDefault();

      var $this_button = $(e.currentTarget);
      var project = $this_button.parents('.project').data('id');
      var diffs = getProjectDiffs();

      $this_button.hide();

      if (project in diffs) {
        $this_button.siblings('.loading').show();

        var url = diffs[project];
        // hashes: currentCommit...latestCommit
        var commitHashe = url.substr(url.lastIndexOf('/') + 1);
        var repositoryName = url.match(/.*\/(.*)\/compare/)[1];

        getGithubCommits(GITHUB_TOKEN, repositoryName, commitHashe, function(commits) {
          // Array of comitt messages
          var messages = commits.map(function(obj) {
            return obj.commit.message;
          });
          // Array of pivotal story IDs
          var pivotal_ids = getPivotalStoryIDs(messages);
          if (pivotal_ids.length === 0) {
            showNoStoriesMessage($this_button);
            return;
          };

          var storyList = [];
          for (var i = 0, imax = pivotal_ids.length; i < imax; i++) {
            getPivotalStoryInfo(PIVOTAL_TOKEN, pivotal_ids[i], function(info) {
              storyList.push(info);

              if (storyList.length === imax) {
                $this_button.siblings('.loading').hide();
                onGetPivotalStoryInfoComplete(project, storyList);
              }
            });
          }
        });
      }
      else {
        showNoStoriesMessage($this_button);
      }
    });

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


<commit_before>import $ from "jquery";
import _ from "lodash";
import tippy, {delegate} from "tippy.js";

import render_message_inline_image_tooltip from "../templates/message_inline_image_tooltip.hbs";
import render_narrow_to_compose_recipients_tooltip from "../templates/narrow_to_compose_recipients_tooltip.hbs";

import * as common from "./common";
import * as compose_state from "./compose_state";
import {$t} from "./i18n";
import * as message_lists from "./message_lists";
import * as narrow_state from "./narrow_state";
import * as popover_menus from "./popover_menus";
import * as reactions from "./reactions";
import * as recent_topics_util from "./recent_topics_util";
import * as rows from "./rows";
import * as timerender from "./timerender";
import {parse_html} from "./ui_util";

// For tooltips without data-tippy-content, we use the HTML content of
// a <template> whose id is given by data-tooltip-template-id.
function get_tooltip_content(reference) {
    if ("tooltipTemplateId" in reference.dataset) {
        const template = document.querySelector(
            `template#${CSS.escape(reference.dataset.tooltipTemplateId)}`,
        );
        return template.content.cloneNode(true);
    }
    return "";
}

// Defining observer outside ensures that at max only one observer is active at all times.
let observer;
function hide_tooltip_if_reference_removed(
    target_node,
    config,
    instance,
    nodes_to_check_for_removal,
) {
    // Use MutationObserver to check for removal of nodes on which tooltips
    // are still active.
    if (!target_node) {
        // The tooltip reference was removed from DOM before we reached here.
        // In that case, we simply hide the tooltip.
        // We have to be smart about hiding the instance, so we hide it as soon
        // as it is displayed.
        setTimeout(instance.hide, 0);
        return;
    }
    const callback = function (mutationsList) {
        for (const mutation of mutationsList) {
            for (const node of nodes_to_check_for_removal) {
                // Hide instance if reference is in the removed node list.
                if (Array.prototype.includes.call(mutation.removedNodes, node)) {
                    instance.hide();
                }
            }
        }
    };
    observer = new MutationObserver(callback);
    observer.observe(target_node, config);
}

// We override the defaults set by tippy library here,
// so make sure to check this too after checking tippyjs
// documentation for default properties.
tippy.setDefaultProps({
    // We don't want tooltips
    // to take more space than
    // mobile widths ever.
    maxWidth: 300,

    // Some delay to showing / hiding the tooltip makes
    // it look less forced and more natural.
    delay: [100, 20],
    placement: "top",

    // disable animations to make the
    // tooltips feel snappy
    animation: false,

    // Show tooltips on long press on touch based
    // devices.
    touch: ["hold", 750],

    // This has the side effect of some properties of parent applying to
    // tooltips.
    appendTo: "parent",

    // To add a text tooltip, override this by setting data-tippy-content.
    // To add an HTML tooltip, set data-tooltip-template-id to the id of a <template>.
    // Or, override this with a function returning string (text) or DocumentFragment (HTML).
    content: get_tooltip_content,
});

export function initialize() {
    // Our default tooltip configuration. For this, one simply needs to:
    // * Set `class="tippy-zulip-tooltip"` on an element for enable this.
    // * Set `data-tippy-content="{{t 'Tooltip content' }}"`, often
    //   replacing a `title` attribute on an element that had both.
    // * Set placement; we typically use `data-tippy-placement="top"`.
    delegate("body", {
        target: ".tippy-zulip-tooltip",
    });

    // The below definitions are for specific tooltips that require
    // custom JavaScript code or configuration.  Note that since the
    // below specify the target directly, elements using those should
    // not have the tippy-zulip-tooltip class.

    // message reaction tooltip showing who reacted.
    let observer;
    delegate("body", {
        target: ".message_reaction, .message_reactions .reaction_button",
        placement: "bottom",
        onShow(instance) {
            const $elem = $(instance.reference);
            if (!instance.reference.classList.contains("reaction_button")) {
                const local_id = $elem.attr("data-reaction-id");
                const message_id = rows.get_message_id(instance.reference);
                const title = reactions.get_reaction_title_data(message_id, local_id);
                instance.setContent(title);
            }

            const config = {attributes: false, childList: true, subtree: true};
            const target = $elem.parents(".message_table.focused_table").get(0);
            const nodes_to_check_for_removal = [
                $elem.parents(".recipient_row").get(0),
                $elem.parents(".message_reactions").get(0),
                $elem.get(0),
            ];
            hide_tooltip_if_reference_removed(target, config, instance, nodes_to_check_for_removal);
        },
        onHidden(instance) {
            instance.destroy();
            if (observer) {
                observer.disconnect();
            }
        },
        appendTo: () => document.body,
    });

    delegate("body", {
        target: ".compose_control_button",
        // Add some additional delay when they open
        // so that regular users don't have to see
        // them unless they want to.
        delay: [300, 20],
        // This ensures that the upload files tooltip
        // doesn't hide behind the left sidebar.
        appendTo: () => document.body,
    });

    delegate("body", {
        target: ".message_control_button",
        // This ensures that the tooltip doesn't
        // hide by the selected message blue border.
        appendTo: () => document.body,
        // Add some additional delay when they open
        // so that regular users don't have to see
        // them unless they want to.
        delay: [300, 20],
        onShow(instance) {
            // Handle dynamic "starred messages" and "edit" widgets.
            const $elem = $(instance.reference);
            let content = $elem.attr("data-tippy-content");
            if (content === undefined) {
                // Tippy cannot get the content for message edit button
                // as it is dynamically inserted based on editability.
                // So, we have to manually get the i element to get the
                // content from it.
                //
                // TODO: Change the template structure so logic is unnecessary.
                const $edit_button = $elem.find("i.edit_message_button");
                content = $edit_button.attr("data-tippy-content");
            }

            instance.setContent(content);
            return true;
        },
    });

    $("body").on("blur", ".message_control_button", (e) => {
        // Remove tooltip when user is trying to tab through all the icons.
        // If user tabs slowly, tooltips are displayed otherwise they are
        // destroyed before they can be displayed.
        e.currentTarget._tippy.destroy();
    });

    delegate("body", {
        target: ".message_table .message_time",
        appendTo: () => document.body,
        onShow(instance) {
            const $time_elem = $(instance.reference);
            const $row = $time_elem.closest(".message_row");
            const message = message_lists.current.get(rows.id($row));
            const time = new Date(message.timestamp * 1000);
            instance.setContent(timerender.get_full_datetime(time));
        },
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        target: ".recipient_row_date > span",
        appendTo: () => document.body,
        onHidden(instance) {
            instance.destroy();
        },
    });

    // In case of recipient bar icons, following change
    // ensures that tooltip doesn't hide behind the message
    // box or it is not limited by the parent container.
    delegate("body", {
        target: [
            ".recipient_bar_icon",
            ".sidebar-title",
            "#user_filter_icon",
            "#scroll-to-bottom-button-clickable-area",
            ".code_external_link",
        ],
        appendTo: () => document.body,
    });

    delegate("body", {
        target: ".rendered_markdown time",
        content: timerender.get_markdown_time_tooltip,
        appendTo: () => document.body,
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        target: "#stream-specific-notify-table .unmute_stream",
        appendTo: () => document.body,
    });

    delegate("body", {
        target: [
            ".rendered_markdown .copy_codeblock",
            "#compose_top_right [data-tippy-content]",
            "#compose_top_right [data-tooltip-template-id]",
        ],
        appendTo: () => document.body,
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        target: ".narrow_to_compose_recipients",
        appendTo: () => document.body,
        content() {
            const narrow_filter = narrow_state.filter();
            let display_current_view;
            if (!recent_topics_util.is_visible()) {
                if (narrow_filter === undefined) {
                    display_current_view = $t({defaultMessage: "Currently viewing all messages."});
                } else if (
                    _.isEqual(narrow_filter.sorted_term_types(), ["stream"]) &&
                    compose_state.get_message_type() === "stream" &&
                    narrow_filter.operands("stream")[0] === compose_state.stream_name()
                ) {
                    display_current_view = $t({
                        defaultMessage: "Currently viewing the entire stream.",
                    });
                } else if (
                    _.isEqual(narrow_filter.sorted_term_types(), ["is-private"]) &&
                    compose_state.get_message_type() === "private"
                ) {
                    display_current_view = $t({
                        defaultMessage: "Currently viewing all private messages.",
                    });
                }
            }

            const shortcut_html = (common.has_mac_keyboard() ? "⌘" : "Ctrl") + " + .";
            return parse_html(
                render_narrow_to_compose_recipients_tooltip({shortcut_html, display_current_view}),
            );
        },
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        target: [".enter_sends_true", ".enter_sends_false"],
        content: $t({defaultMessage: "Change send shortcut"}),
        onShow() {
            // Don't show tooltip if the popover is displayed.
            if (popover_menus.compose_enter_sends_popover_displayed) {
                return false;
            }
            return true;
        },
        appendTo: () => document.body,
    });

    delegate("body", {
        target: ".message_inline_image > a > img",
        appendTo: () => document.body,
        // Add a short delay so the user can mouseover several inline images without
        // tooltips showing and hiding rapidly
        delay: [300, 20],
        onShow(instance) {
            // Some message_inline_images aren't actually images with a title,
            // for example youtube videos, so we default to the actual href
            const title =
                $(instance.reference).parent().attr("aria-label") ||
                $(instance.reference).parent().attr("href");
            instance.setContent(parse_html(render_message_inline_image_tooltip({title})));

            const target_node = $(instance.reference)
                .parents(".message_table.focused_table")
                .get(0);
            const config = {attributes: false, childList: true, subtree: false};
            const nodes_to_check_for_removal = [
                $(instance.reference).parents(".message_inline_image").get(0),
            ];
            hide_tooltip_if_reference_removed(
                target_node,
                config,
                instance,
                nodes_to_check_for_removal,
            );
        },
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        target: ".image-info-wrapper > .image-description > .title",
        appendTo: () => document.body,
        onShow(instance) {
            const title = $(instance.reference).attr("aria-label");
            const filename = $(instance.reference).prop("data-filename");
            const $markup = $("<span>").text(title);
            if (title !== filename) {
                // If the image title is the same as the filename, there's no reason
                // to show this next line.
                const second_line = $t({defaultMessage: "File name: {filename}"}, {filename});
                $markup.append($("<br>"), $("<span>").text(second_line));
            }
            instance.setContent($markup[0]);
        },
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        // Configure tooltips for the stream_sorter_toggle buttons.

        // TODO: Ideally, we'd extend this to be a common mechanism for
        // tab switchers, with the strings living in a more normal configuration
        // location.
        target: ".stream_sorter_toggle .ind-tab [data-tippy-content]",

        // Adjust their placement to `bottom`.
        placement: "bottom",

        // Avoid inheriting `position: relative` CSS on the stream sorter widget.
        appendTo: () => document.body,
    });

    delegate("body", {
        // This tooltip appears on the "Summary" checkboxes in
        // settings > custom profile fields, when at the limit of 2
        // fields with display_in_profile_summary enabled.
        target: [
            "#profile-field-settings .display_in_profile_summary_tooltip",
            "#edit-custom-profile-field-form-modal .display_in_profile_summary_tooltip",
            "#add-new-custom-profile-field-form .display_in_profile_summary_tooltip",
        ],
        content: $t({
            defaultMessage: "Only 2 custom profile fields can be displayed in the profile summary.",
        }),
        appendTo: () => document.body,
        onTrigger(instance) {
            // Sometimes just removing class is not enough to destroy/remove tooltip, especially in
            // "Add a new custom profile field" form, so here we are manually calling `destroy()`.
            if (!instance.reference.classList.contains("display_in_profile_summary_tooltip")) {
                instance.destroy();
            }
        },
    });
}
<commit_msg>tippy: Check if reaction still exists in DOM before showing tooltip.<commit_after>import $ from "jquery";
import _ from "lodash";
import tippy, {delegate} from "tippy.js";

import render_message_inline_image_tooltip from "../templates/message_inline_image_tooltip.hbs";
import render_narrow_to_compose_recipients_tooltip from "../templates/narrow_to_compose_recipients_tooltip.hbs";

import * as common from "./common";
import * as compose_state from "./compose_state";
import {$t} from "./i18n";
import * as message_lists from "./message_lists";
import * as narrow_state from "./narrow_state";
import * as popover_menus from "./popover_menus";
import * as reactions from "./reactions";
import * as recent_topics_util from "./recent_topics_util";
import * as rows from "./rows";
import * as timerender from "./timerender";
import {parse_html} from "./ui_util";

// For tooltips without data-tippy-content, we use the HTML content of
// a <template> whose id is given by data-tooltip-template-id.
function get_tooltip_content(reference) {
    if ("tooltipTemplateId" in reference.dataset) {
        const template = document.querySelector(
            `template#${CSS.escape(reference.dataset.tooltipTemplateId)}`,
        );
        return template.content.cloneNode(true);
    }
    return "";
}

// Defining observer outside ensures that at max only one observer is active at all times.
let observer;
function hide_tooltip_if_reference_removed(
    target_node,
    config,
    instance,
    nodes_to_check_for_removal,
) {
    // Use MutationObserver to check for removal of nodes on which tooltips
    // are still active.
    if (!target_node) {
        // The tooltip reference was removed from DOM before we reached here.
        // In that case, we simply hide the tooltip.
        // We have to be smart about hiding the instance, so we hide it as soon
        // as it is displayed.
        setTimeout(instance.hide, 0);
        return;
    }
    const callback = function (mutationsList) {
        for (const mutation of mutationsList) {
            for (const node of nodes_to_check_for_removal) {
                // Hide instance if reference is in the removed node list.
                if (Array.prototype.includes.call(mutation.removedNodes, node)) {
                    instance.hide();
                }
            }
        }
    };
    observer = new MutationObserver(callback);
    observer.observe(target_node, config);
}

// We override the defaults set by tippy library here,
// so make sure to check this too after checking tippyjs
// documentation for default properties.
tippy.setDefaultProps({
    // We don't want tooltips
    // to take more space than
    // mobile widths ever.
    maxWidth: 300,

    // Some delay to showing / hiding the tooltip makes
    // it look less forced and more natural.
    delay: [100, 20],
    placement: "top",

    // disable animations to make the
    // tooltips feel snappy
    animation: false,

    // Show tooltips on long press on touch based
    // devices.
    touch: ["hold", 750],

    // This has the side effect of some properties of parent applying to
    // tooltips.
    appendTo: "parent",

    // To add a text tooltip, override this by setting data-tippy-content.
    // To add an HTML tooltip, set data-tooltip-template-id to the id of a <template>.
    // Or, override this with a function returning string (text) or DocumentFragment (HTML).
    content: get_tooltip_content,
});

export function initialize() {
    // Our default tooltip configuration. For this, one simply needs to:
    // * Set `class="tippy-zulip-tooltip"` on an element for enable this.
    // * Set `data-tippy-content="{{t 'Tooltip content' }}"`, often
    //   replacing a `title` attribute on an element that had both.
    // * Set placement; we typically use `data-tippy-placement="top"`.
    delegate("body", {
        target: ".tippy-zulip-tooltip",
    });

    // The below definitions are for specific tooltips that require
    // custom JavaScript code or configuration.  Note that since the
    // below specify the target directly, elements using those should
    // not have the tippy-zulip-tooltip class.

    // message reaction tooltip showing who reacted.
    let observer;
    delegate("body", {
        target: ".message_reaction, .message_reactions .reaction_button",
        placement: "bottom",
        onShow(instance) {
            if (!document.body.contains(instance.reference)) {
                // It is possible for reaction to be removed before `onShow` is triggered,
                // so, we check if the element exists before proceeding.
                return false;
            }
            const $elem = $(instance.reference);
            if (!instance.reference.classList.contains("reaction_button")) {
                const local_id = $elem.attr("data-reaction-id");
                const message_id = rows.get_message_id(instance.reference);
                const title = reactions.get_reaction_title_data(message_id, local_id);
                instance.setContent(title);
            }

            const config = {attributes: false, childList: true, subtree: true};
            const target = $elem.parents(".message_table.focused_table").get(0);
            const nodes_to_check_for_removal = [
                $elem.parents(".recipient_row").get(0),
                $elem.parents(".message_reactions").get(0),
                $elem.get(0),
            ];
            hide_tooltip_if_reference_removed(target, config, instance, nodes_to_check_for_removal);
            return true;
        },
        onHidden(instance) {
            instance.destroy();
            if (observer) {
                observer.disconnect();
            }
        },
        appendTo: () => document.body,
    });

    delegate("body", {
        target: ".compose_control_button",
        // Add some additional delay when they open
        // so that regular users don't have to see
        // them unless they want to.
        delay: [300, 20],
        // This ensures that the upload files tooltip
        // doesn't hide behind the left sidebar.
        appendTo: () => document.body,
    });

    delegate("body", {
        target: ".message_control_button",
        // This ensures that the tooltip doesn't
        // hide by the selected message blue border.
        appendTo: () => document.body,
        // Add some additional delay when they open
        // so that regular users don't have to see
        // them unless they want to.
        delay: [300, 20],
        onShow(instance) {
            // Handle dynamic "starred messages" and "edit" widgets.
            const $elem = $(instance.reference);
            let content = $elem.attr("data-tippy-content");
            if (content === undefined) {
                // Tippy cannot get the content for message edit button
                // as it is dynamically inserted based on editability.
                // So, we have to manually get the i element to get the
                // content from it.
                //
                // TODO: Change the template structure so logic is unnecessary.
                const $edit_button = $elem.find("i.edit_message_button");
                content = $edit_button.attr("data-tippy-content");
            }

            instance.setContent(content);
            return true;
        },
    });

    $("body").on("blur", ".message_control_button", (e) => {
        // Remove tooltip when user is trying to tab through all the icons.
        // If user tabs slowly, tooltips are displayed otherwise they are
        // destroyed before they can be displayed.
        e.currentTarget._tippy.destroy();
    });

    delegate("body", {
        target: ".message_table .message_time",
        appendTo: () => document.body,
        onShow(instance) {
            const $time_elem = $(instance.reference);
            const $row = $time_elem.closest(".message_row");
            const message = message_lists.current.get(rows.id($row));
            const time = new Date(message.timestamp * 1000);
            instance.setContent(timerender.get_full_datetime(time));
        },
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        target: ".recipient_row_date > span",
        appendTo: () => document.body,
        onHidden(instance) {
            instance.destroy();
        },
    });

    // In case of recipient bar icons, following change
    // ensures that tooltip doesn't hide behind the message
    // box or it is not limited by the parent container.
    delegate("body", {
        target: [
            ".recipient_bar_icon",
            ".sidebar-title",
            "#user_filter_icon",
            "#scroll-to-bottom-button-clickable-area",
            ".code_external_link",
        ],
        appendTo: () => document.body,
    });

    delegate("body", {
        target: ".rendered_markdown time",
        content: timerender.get_markdown_time_tooltip,
        appendTo: () => document.body,
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        target: "#stream-specific-notify-table .unmute_stream",
        appendTo: () => document.body,
    });

    delegate("body", {
        target: [
            ".rendered_markdown .copy_codeblock",
            "#compose_top_right [data-tippy-content]",
            "#compose_top_right [data-tooltip-template-id]",
        ],
        appendTo: () => document.body,
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        target: ".narrow_to_compose_recipients",
        appendTo: () => document.body,
        content() {
            const narrow_filter = narrow_state.filter();
            let display_current_view;
            if (!recent_topics_util.is_visible()) {
                if (narrow_filter === undefined) {
                    display_current_view = $t({defaultMessage: "Currently viewing all messages."});
                } else if (
                    _.isEqual(narrow_filter.sorted_term_types(), ["stream"]) &&
                    compose_state.get_message_type() === "stream" &&
                    narrow_filter.operands("stream")[0] === compose_state.stream_name()
                ) {
                    display_current_view = $t({
                        defaultMessage: "Currently viewing the entire stream.",
                    });
                } else if (
                    _.isEqual(narrow_filter.sorted_term_types(), ["is-private"]) &&
                    compose_state.get_message_type() === "private"
                ) {
                    display_current_view = $t({
                        defaultMessage: "Currently viewing all private messages.",
                    });
                }
            }

            const shortcut_html = (common.has_mac_keyboard() ? "⌘" : "Ctrl") + " + .";
            return parse_html(
                render_narrow_to_compose_recipients_tooltip({shortcut_html, display_current_view}),
            );
        },
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        target: [".enter_sends_true", ".enter_sends_false"],
        content: $t({defaultMessage: "Change send shortcut"}),
        onShow() {
            // Don't show tooltip if the popover is displayed.
            if (popover_menus.compose_enter_sends_popover_displayed) {
                return false;
            }
            return true;
        },
        appendTo: () => document.body,
    });

    delegate("body", {
        target: ".message_inline_image > a > img",
        appendTo: () => document.body,
        // Add a short delay so the user can mouseover several inline images without
        // tooltips showing and hiding rapidly
        delay: [300, 20],
        onShow(instance) {
            // Some message_inline_images aren't actually images with a title,
            // for example youtube videos, so we default to the actual href
            const title =
                $(instance.reference).parent().attr("aria-label") ||
                $(instance.reference).parent().attr("href");
            instance.setContent(parse_html(render_message_inline_image_tooltip({title})));

            const target_node = $(instance.reference)
                .parents(".message_table.focused_table")
                .get(0);
            const config = {attributes: false, childList: true, subtree: false};
            const nodes_to_check_for_removal = [
                $(instance.reference).parents(".message_inline_image").get(0),
            ];
            hide_tooltip_if_reference_removed(
                target_node,
                config,
                instance,
                nodes_to_check_for_removal,
            );
        },
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        target: ".image-info-wrapper > .image-description > .title",
        appendTo: () => document.body,
        onShow(instance) {
            const title = $(instance.reference).attr("aria-label");
            const filename = $(instance.reference).prop("data-filename");
            const $markup = $("<span>").text(title);
            if (title !== filename) {
                // If the image title is the same as the filename, there's no reason
                // to show this next line.
                const second_line = $t({defaultMessage: "File name: {filename}"}, {filename});
                $markup.append($("<br>"), $("<span>").text(second_line));
            }
            instance.setContent($markup[0]);
        },
        onHidden(instance) {
            instance.destroy();
        },
    });

    delegate("body", {
        // Configure tooltips for the stream_sorter_toggle buttons.

        // TODO: Ideally, we'd extend this to be a common mechanism for
        // tab switchers, with the strings living in a more normal configuration
        // location.
        target: ".stream_sorter_toggle .ind-tab [data-tippy-content]",

        // Adjust their placement to `bottom`.
        placement: "bottom",

        // Avoid inheriting `position: relative` CSS on the stream sorter widget.
        appendTo: () => document.body,
    });

    delegate("body", {
        // This tooltip appears on the "Summary" checkboxes in
        // settings > custom profile fields, when at the limit of 2
        // fields with display_in_profile_summary enabled.
        target: [
            "#profile-field-settings .display_in_profile_summary_tooltip",
            "#edit-custom-profile-field-form-modal .display_in_profile_summary_tooltip",
            "#add-new-custom-profile-field-form .display_in_profile_summary_tooltip",
        ],
        content: $t({
            defaultMessage: "Only 2 custom profile fields can be displayed in the profile summary.",
        }),
        appendTo: () => document.body,
        onTrigger(instance) {
            // Sometimes just removing class is not enough to destroy/remove tooltip, especially in
            // "Add a new custom profile field" form, so here we are manually calling `destroy()`.
            if (!instance.reference.classList.contains("display_in_profile_summary_tooltip")) {
                instance.destroy();
            }
        },
    });
}
<|endoftext|>


<commit_before>import { ScriptBuilder } from '../sc'
import { getScriptHashFromAddress, Account } from '../wallet'
import { Query, VMZip } from '../rpc'
import { reverseHex, hexstring2str } from '../utils'
import { getRPCEndpoint, getBalance } from './neonDB'
import { Transaction } from '../transactions'
import { ASSET_ID } from '../consts'
import logger from '../logging'

const log = logger('api')
/**
 * Parses the VM output for decimals. The VM returns an integer for most cases but it can be an empty string for zero.
 */
const parseDecimals = (VMOutput) => {
  if (VMOutput === '') return 0
  return parseInt(VMOutput, 10)
}
const parseHexNum = (hex) => hex ? parseInt(reverseHex(hex), 16) : 0

const parseTokenInfo = VMZip(hexstring2str, hexstring2str, parseDecimals, parseHexNum)

const parseTokenInfoAndBalance = VMZip(hexstring2str, hexstring2str, parseDecimals, parseHexNum, parseHexNum)
/**
 * Queries for NEP5 Token information.
 * @param {string} url - URL of the NEO node to query.
 * @param {string} scriptHash - Contract scriptHash.
 * @return {Promise<{name: string, symbol: string, decimals: number, totalSupply: number}>}
 */
export const getTokenInfo = (url, scriptHash) => {
  const sb = new ScriptBuilder()
  sb
    .emitAppCall(scriptHash, 'name')
    .emitAppCall(scriptHash, 'symbol')
    .emitAppCall(scriptHash, 'decimals')
    .emitAppCall(scriptHash, 'totalSupply')
  const script = sb.str
  return Query.invokeScript(script, false).parseWith(parseTokenInfo).execute(url)
    .then((res) => {
      return {
        name: res[0],
        symbol: res[1],
        decimals: res[2],
        totalSupply: res[3] / Math.pow(10, res[2])
      }
    })
    .catch(err => {
      log.error(`getTokenInfo failed with : ${err.message}`)
      throw err
    })
}

/**
 * Get the token balance of Address from Contract
 * @param {string} url - URL of the NEO node to query.
 * @param {string} scriptHash - Contract scriptHash.
 * @param {string} address - Address to query for balance of tokens.
 * @return {Promise<number>}
 */
export const getTokenBalance = (url, scriptHash, address) => {
  const addrScriptHash = reverseHex(getScriptHashFromAddress(address))
  const sb = new ScriptBuilder()
  const script =
    sb
      .emitAppCall(scriptHash, 'decimals')
      .emitAppCall(scriptHash, 'balanceOf', [addrScriptHash])
      .str
  return Query.invokeScript(script, false).execute(url)
    .then((res) => {
      try {
        const decimals = parseDecimals(res.result.stack[0].value)
        return parseHexNum(res.result.stack[1].value) / Math.pow(10, decimals)
      } catch (error) {
        return 0
      }
    })
    .catch(err => {
      log.error(`getTokenBalance failed with : ${err.message}`)
      throw err
    })
}

/**
 * Get the token info and also balance if address is provided.
 * @param {string} url - URL of the NEO node to query.
 * @param {string} scriptHash - Contract scriptHash.
 * @param {string} [address] - Address to query for balance of tokens.
 * @return {Promise<object>} Object containing name, symbol, decimals, totalSupply. balance will be included if address is provided.
 */
export const getToken = (url, scriptHash, address) => {
  let parser = address ? parseTokenInfoAndBalance : parseTokenInfo
  const sb = new ScriptBuilder()
  sb
    .emitAppCall(scriptHash, 'name')
    .emitAppCall(scriptHash, 'symbol')
    .emitAppCall(scriptHash, 'decimals')
    .emitAppCall(scriptHash, 'totalSupply')
  if (address) {
    const addrScriptHash = reverseHex(getScriptHashFromAddress(address))
    sb.emitAppCall(scriptHash, 'balanceOf', [addrScriptHash])
  }
  const script = sb.str
  return Query.invokeScript(script, false).parseWith(parser).execute(url)
    .then((res) => {
      return {
        name: res[0],
        symbol: res[1],
        decimals: res[2],
        totalSupply: res[3] / Math.pow(10, res[2]),
        balance: res.length === 5 ? res[4] / Math.pow(10, res[2]) : null
      }
    })
    .catch(err => {
      log.error(`getToken failed with : ${err.message}`)
      throw err
    })
}

/**
 * Transfers NEP5 Tokens.
 * @param {string} net - 'MainNet', 'TestNet' or a custom NeonDB url.
 * @param {string} scriptHash - Contract scriptHash
 * @param {string} fromWif - WIF key of the address where the tokens are coming from.
 * @param {string} toAddress - The address to send the tokens to.
 * @param {number} transferAmount - Amount to transfer. This number will be divided by 100000000.
 * @param {number} gasCost - Amount of gas to pay for transfer.
 * @param {function} [signingFunction] - Optional external signing function.
 * @return {Promise<Response>} RPC response
 */
export const doTransferToken = (net, scriptHash, fromWif, toAddress, transferAmount, gasCost = 0, signingFunction = null) => {
  const account = new Account(fromWif)
  const rpcEndpointPromise = getRPCEndpoint(net)
  const balancePromise = getBalance(net, account.address)
  let signedTx
  let endpt
  return Promise.all([rpcEndpointPromise, balancePromise])
    .then((values) => {
      endpt = values[0]
      const balances = values[1]
      const fromAddrScriptHash = getScriptHashFromAddress(account.address)
      const toAddrScriptHash = reverseHex(getScriptHashFromAddress(toAddress))
      const intents = [
        { assetId: ASSET_ID.GAS, value: 0.00000001, scriptHash: fromAddrScriptHash }
      ]
      const invoke = { scriptHash, operation: 'transfer', args: [reverseHex(fromAddrScriptHash), toAddrScriptHash, transferAmount] }
      const unsignedTx = Transaction.createInvocationTx(balances, intents, invoke, gasCost, { version: 1 })
      if (signingFunction) {
        return signingFunction(unsignedTx, account.publicKey)
      } else {
        return unsignedTx.sign(account.privateKey)
      }
    })
    .then((signedResult) => {
      signedTx = signedResult
      return Query.sendRawTransaction(signedTx).execute(endpt)
    })
    .then((res) => {
      if (res.result === true) {
        res.txid = signedTx.hash
      }
      return res
    })
}
<commit_msg>chore(api): Mark nep5.doTransferToken for deprecation<commit_after>import { ScriptBuilder } from '../sc'
import { getScriptHashFromAddress, Account } from '../wallet'
import { Query, VMZip } from '../rpc'
import { reverseHex, hexstring2str } from '../utils'
import { getRPCEndpoint, getBalance } from './neonDB'
import { Transaction } from '../transactions'
import { ASSET_ID } from '../consts'
import logger from '../logging'

const log = logger('api')
/**
 * Parses the VM output for decimals. The VM returns an integer for most cases but it can be an empty string for zero.
 */
const parseDecimals = (VMOutput) => {
  if (VMOutput === '') return 0
  return parseInt(VMOutput, 10)
}
const parseHexNum = (hex) => hex ? parseInt(reverseHex(hex), 16) : 0

const parseTokenInfo = VMZip(hexstring2str, hexstring2str, parseDecimals, parseHexNum)

const parseTokenInfoAndBalance = VMZip(hexstring2str, hexstring2str, parseDecimals, parseHexNum, parseHexNum)
/**
 * Queries for NEP5 Token information.
 * @param {string} url - URL of the NEO node to query.
 * @param {string} scriptHash - Contract scriptHash.
 * @return {Promise<{name: string, symbol: string, decimals: number, totalSupply: number}>}
 */
export const getTokenInfo = (url, scriptHash) => {
  const sb = new ScriptBuilder()
  sb
    .emitAppCall(scriptHash, 'name')
    .emitAppCall(scriptHash, 'symbol')
    .emitAppCall(scriptHash, 'decimals')
    .emitAppCall(scriptHash, 'totalSupply')
  const script = sb.str
  return Query.invokeScript(script, false).parseWith(parseTokenInfo).execute(url)
    .then((res) => {
      return {
        name: res[0],
        symbol: res[1],
        decimals: res[2],
        totalSupply: res[3] / Math.pow(10, res[2])
      }
    })
    .catch(err => {
      log.error(`getTokenInfo failed with : ${err.message}`)
      throw err
    })
}

/**
 * Get the token balance of Address from Contract
 * @param {string} url - URL of the NEO node to query.
 * @param {string} scriptHash - Contract scriptHash.
 * @param {string} address - Address to query for balance of tokens.
 * @return {Promise<number>}
 */
export const getTokenBalance = (url, scriptHash, address) => {
  const addrScriptHash = reverseHex(getScriptHashFromAddress(address))
  const sb = new ScriptBuilder()
  const script =
    sb
      .emitAppCall(scriptHash, 'decimals')
      .emitAppCall(scriptHash, 'balanceOf', [addrScriptHash])
      .str
  return Query.invokeScript(script, false).execute(url)
    .then((res) => {
      try {
        const decimals = parseDecimals(res.result.stack[0].value)
        return parseHexNum(res.result.stack[1].value) / Math.pow(10, decimals)
      } catch (error) {
        return 0
      }
    })
    .catch(err => {
      log.error(`getTokenBalance failed with : ${err.message}`)
      throw err
    })
}

/**
 * Get the token info and also balance if address is provided.
 * @param {string} url - URL of the NEO node to query.
 * @param {string} scriptHash - Contract scriptHash.
 * @param {string} [address] - Address to query for balance of tokens.
 * @return {Promise<object>} Object containing name, symbol, decimals, totalSupply. balance will be included if address is provided.
 */
export const getToken = (url, scriptHash, address) => {
  let parser = address ? parseTokenInfoAndBalance : parseTokenInfo
  const sb = new ScriptBuilder()
  sb
    .emitAppCall(scriptHash, 'name')
    .emitAppCall(scriptHash, 'symbol')
    .emitAppCall(scriptHash, 'decimals')
    .emitAppCall(scriptHash, 'totalSupply')
  if (address) {
    const addrScriptHash = reverseHex(getScriptHashFromAddress(address))
    sb.emitAppCall(scriptHash, 'balanceOf', [addrScriptHash])
  }
  const script = sb.str
  return Query.invokeScript(script, false).parseWith(parser).execute(url)
    .then((res) => {
      return {
        name: res[0],
        symbol: res[1],
        decimals: res[2],
        totalSupply: res[3] / Math.pow(10, res[2]),
        balance: res.length === 5 ? res[4] / Math.pow(10, res[2]) : null
      }
    })
    .catch(err => {
      log.error(`getToken failed with : ${err.message}`)
      throw err
    })
}

/**
 * Transfers NEP5 Tokens.
 * @param {string} net - 'MainNet', 'TestNet' or a custom NeonDB url.
 * @param {string} scriptHash - Contract scriptHash
 * @param {string} fromWif - WIF key of the address where the tokens are coming from.
 * @param {string} toAddress - The address to send the tokens to.
 * @param {number} transferAmount - Amount to transfer. This number will be divided by 100000000.
 * @param {number} gasCost - Amount of gas to pay for transfer.
 * @param {function} [signingFunction] - Optional external signing function.
 * @return {Promise<Response>} RPC response
 */
export const doTransferToken = (net, scriptHash, fromWif, toAddress, transferAmount, gasCost = 0, signingFunction = null) => {
  log.warn('doTransferToken will be deprecated in favor of doInvoke')
  const account = new Account(fromWif)
  const rpcEndpointPromise = getRPCEndpoint(net)
  const balancePromise = getBalance(net, account.address)
  let signedTx
  let endpt
  return Promise.all([rpcEndpointPromise, balancePromise])
    .then((values) => {
      endpt = values[0]
      const balances = values[1]
      const fromAddrScriptHash = getScriptHashFromAddress(account.address)
      const toAddrScriptHash = reverseHex(getScriptHashFromAddress(toAddress))
      const intents = [
        { assetId: ASSET_ID.GAS, value: 0.00000001, scriptHash: fromAddrScriptHash }
      ]
      const invoke = { scriptHash, operation: 'transfer', args: [reverseHex(fromAddrScriptHash), toAddrScriptHash, transferAmount] }
      const unsignedTx = Transaction.createInvocationTx(balances, intents, invoke, gasCost, { version: 1 })
      if (signingFunction) {
        return signingFunction(unsignedTx, account.publicKey)
      } else {
        return unsignedTx.sign(account.privateKey)
      }
    })
    .then((signedResult) => {
      signedTx = signedResult
      return Query.sendRawTransaction(signedTx).execute(endpt)
    })
    .then((res) => {
      if (res.result === true) {
        res.txid = signedTx.hash
      }
      return res
    })
}
<|endoftext|>


<commit_before>import React from 'react';
import { Query, Mutation } from 'react-apollo';
import EmployeeHome from './EmployeeHome';
import LoadingAnimation from '../shared/LoadingAnimation';
import Error from '../shared/Error';
import { UPDATE_AUTHMODAL } from '../utilities/auth/graphql/authMutations';
import { getUser } from '../utilities/auth/graphql/authQueries';

const Homepage = props => (
  <Query query={getUser}>
    {({ loading, error, data }) => {
      if (loading) return <LoadingAnimation />;
      if (error) return <Error message={error.message} />;

      if (!data.user.loggedIn) {
        return (
          <Mutation mutation={UPDATE_AUTHMODAL}>
            {updateAuthModal => (

                Welcome to City of Asheville Employee Check-in. Please
                 {
                    e.preventDefault();
                    updateAuthModal({
                      variables: {
                        open: true,
                      },
                    });
                  }}
                  className=""
                >log in.

Remember to use Chrome or Firefox. IE is not yet supported.

            )}
          </Mutation>
        );
      }
      if (data.user.loggedIn && !data.user.email.endsWith('ashevillenc.gov')) {
        return (
Invalid user
);
      }
      return (<EmployeeHome {...props} />);
    }}
  </Query>
);

export default Homepage;
<commit_msg>err message clarification<commit_after>import React from 'react';
import { Query, Mutation } from 'react-apollo';
import EmployeeHome from './EmployeeHome';
import LoadingAnimation from '../shared/LoadingAnimation';
import Error from '../shared/Error';
import { UPDATE_AUTHMODAL } from '../utilities/auth/graphql/authMutations';
import { getUser } from '../utilities/auth/graphql/authQueries';

const Homepage = props => (
  <Query query={getUser}>
    {({ loading, error, data }) => {
      if (loading) return <LoadingAnimation />;
      if (error) return <Error message={error.message} />;

      if (!data.user.loggedIn) {
        return (
          <Mutation mutation={UPDATE_AUTHMODAL}>
            {updateAuthModal => (

                Welcome to City of Asheville Employee Check-in. Please
                 {
                    e.preventDefault();
                    updateAuthModal({
                      variables: {
                        open: true,
                      },
                    });
                  }}
                  className=""
                >log in.

Remember to use Chrome or Firefox. IE is not yet supported.

            )}
          </Mutation>
        );
      }
      if (data.user.loggedIn && !data.user.email.endsWith('ashevillenc.gov')) {
        return (
Invalid user: You must log in with an ashevillenc.gov account.
);
      }
      return (<EmployeeHome {...props} />);
    }}
  </Query>
);

export default Homepage;
<|endoftext|>


<commit_before>const set = (typeof Set === "function") ? new Set() : (function () {
	const list = [];

	return {
		has(key) {
			return Boolean(list.indexOf(key) > -1);
		},
		add(key) {
			list.push(key);
		},
		delete(key) {
			list.splice(list.indexOf(key), 1);
		},
	}
})();

function assign(ta, {setOverflowX = true, setOverflowY = true} = {}) {
	if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;

	let heightOffset = null;
	let overflowY = null;
	let clientWidth = ta.clientWidth;

	function init() {
		const style = window.getComputedStyle(ta, null);

		overflowY = style.overflowY;

		if (style.resize === 'vertical') {
			ta.style.resize = 'none';
		} else if (style.resize === 'both') {
			ta.style.resize = 'horizontal';
		}

		if (style.boxSizing === 'content-box') {
			heightOffset = -(parseFloat(style.paddingTop)+parseFloat(style.paddingBottom));
		} else {
			heightOffset = parseFloat(style.borderTopWidth)+parseFloat(style.borderBottomWidth);
		}
		// Fix when a textarea is not on document body and heightOffset is Not a Number
		if (isNaN(heightOffset)) {
			heightOffset = 0;
		}

		update();
	}

	function changeOverflow(value) {
		{
			// Chrome/Safari-specific fix:
			// When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
			// made available by removing the scrollbar. The following forces the necessary text reflow.
			const width = ta.style.width;
			ta.style.width = '0px';
			// Force reflow:
			/* jshint ignore:start */
			ta.offsetWidth;
			/* jshint ignore:end */
			ta.style.width = width;
		}

		overflowY = value;

		if (setOverflowY) {
			ta.style.overflowY = value;
		}

		resize();
	}

	function resize() {
		const htmlTop = window.pageYOffset;
		const bodyTop = document.body.scrollTop;
		const originalHeight = ta.style.height;

		ta.style.height = 'auto';

		let endHeight = ta.scrollHeight+heightOffset;

		if (ta.scrollHeight === 0) {
			// If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
			ta.style.height = originalHeight;
			return;
		}

		ta.style.height = endHeight+'px';

		// used to check if an update is actually necessary on window.resize
		clientWidth = ta.clientWidth;

		// prevents scroll-position jumping
		document.documentElement.scrollTop = htmlTop;
		document.body.scrollTop = bodyTop;
	}

	function update() {
		const startHeight = ta.style.height;

		resize();

		const style = window.getComputedStyle(ta, null);

		if (style.height !== ta.style.height) {
			if (overflowY !== 'visible') {
				changeOverflow('visible');
			}
		} else {
			if (overflowY !== 'hidden') {
				changeOverflow('hidden');
			}
		}

		if (startHeight !== ta.style.height) {
			const evt = document.createEvent('Event');
			evt.initEvent('autosize:resized', true, false);
			ta.dispatchEvent(evt);
		}
	}

	const pageResize = () => {
		if (ta.clientWidth !== clientWidth) {
			update();
		}
	};

	const destroy = style => {
		window.removeEventListener('resize', pageResize);
		ta.removeEventListener('input', update);
		ta.removeEventListener('keyup', update);
		ta.removeEventListener('autosize:destroy', destroy);
		set.delete(ta);

		Object.keys(style).forEach(key => {
			ta.style[key] = style[key];
		});
	}.bind(ta, {
		height: ta.style.height,
		resize: ta.style.resize,
		overflowY: ta.style.overflowY,
		overflowX: ta.style.overflowX,
		wordWrap: ta.style.wordWrap,
	});

	ta.addEventListener('autosize:destroy', destroy);

	// IE9 does not fire onpropertychange or oninput for deletions,
	// so binding to onkeyup to catch most of those events.
	// There is no way that I know of to detect something like 'cut' in IE9.
	if ('onpropertychange' in ta && 'oninput' in ta) {
		ta.addEventListener('keyup', update);
	}

	window.addEventListener('resize', pageResize);
	ta.addEventListener('input', update);
	ta.addEventListener('autosize:update', update);
	set.add(ta);

	if (setOverflowX) {
		ta.style.overflowX = 'hidden';
		ta.style.wordWrap = 'break-word';
	}

	init();
}

function destroy(ta) {
	if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
	const evt = document.createEvent('Event');
	evt.initEvent('autosize:destroy', true, false);
	ta.dispatchEvent(evt);
}

function update(ta) {
	if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
	const evt = document.createEvent('Event');
	evt.initEvent('autosize:update', true, false);
	ta.dispatchEvent(evt);
}

let autosize = null;

// Do nothing in Node.js environment and IE8 (or lower)
if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
	autosize = el => el;
	autosize.destroy = el => el;
	autosize.update = el => el;
} else {
	autosize = (el, options) => {
		if (el) {
			Array.prototype.forEach.call(el.length ? el : [el], x => assign(x, options));
		}
		return el;
	};
	autosize.destroy = el => {
		if (el) {
			Array.prototype.forEach.call(el.length ? el : [el], destroy);
		}
		return el;
	};
	autosize.update = el => {
		if (el) {
			Array.prototype.forEach.call(el.length ? el : [el], update);
		}
		return el;
	};
}

export default autosize;
<commit_msg>Fixed memory leak<commit_after>const set = (typeof Set === "function") ? new Set() : (function () {
	const list = [];

	return {
		has(key) {
			return Boolean(list.indexOf(key) > -1);
		},
		add(key) {
			list.push(key);
		},
		delete(key) {
			list.splice(list.indexOf(key), 1);
		},
	}
})();

function assign(ta, {setOverflowX = true, setOverflowY = true} = {}) {
	if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;

	let heightOffset = null;
	let overflowY = null;
	let clientWidth = ta.clientWidth;

	function init() {
		const style = window.getComputedStyle(ta, null);

		overflowY = style.overflowY;

		if (style.resize === 'vertical') {
			ta.style.resize = 'none';
		} else if (style.resize === 'both') {
			ta.style.resize = 'horizontal';
		}

		if (style.boxSizing === 'content-box') {
			heightOffset = -(parseFloat(style.paddingTop)+parseFloat(style.paddingBottom));
		} else {
			heightOffset = parseFloat(style.borderTopWidth)+parseFloat(style.borderBottomWidth);
		}
		// Fix when a textarea is not on document body and heightOffset is Not a Number
		if (isNaN(heightOffset)) {
			heightOffset = 0;
		}

		update();
	}

	function changeOverflow(value) {
		{
			// Chrome/Safari-specific fix:
			// When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
			// made available by removing the scrollbar. The following forces the necessary text reflow.
			const width = ta.style.width;
			ta.style.width = '0px';
			// Force reflow:
			/* jshint ignore:start */
			ta.offsetWidth;
			/* jshint ignore:end */
			ta.style.width = width;
		}

		overflowY = value;

		if (setOverflowY) {
			ta.style.overflowY = value;
		}

		resize();
	}

	function resize() {
		const htmlTop = window.pageYOffset;
		const bodyTop = document.body.scrollTop;
		const originalHeight = ta.style.height;

		ta.style.height = 'auto';

		let endHeight = ta.scrollHeight+heightOffset;

		if (ta.scrollHeight === 0) {
			// If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
			ta.style.height = originalHeight;
			return;
		}

		ta.style.height = endHeight+'px';

		// used to check if an update is actually necessary on window.resize
		clientWidth = ta.clientWidth;

		// prevents scroll-position jumping
		document.documentElement.scrollTop = htmlTop;
		document.body.scrollTop = bodyTop;
	}

	function update() {
		const startHeight = ta.style.height;

		resize();

		const style = window.getComputedStyle(ta, null);

		if (style.height !== ta.style.height) {
			if (overflowY !== 'visible') {
				changeOverflow('visible');
			}
		} else {
			if (overflowY !== 'hidden') {
				changeOverflow('hidden');
			}
		}

		if (startHeight !== ta.style.height) {
			const evt = document.createEvent('Event');
			evt.initEvent('autosize:resized', true, false);
			ta.dispatchEvent(evt);
		}
	}

	const pageResize = () => {
		if (ta.clientWidth !== clientWidth) {
			update();
		}
	};

	const destroy = style => {
		window.removeEventListener('resize', pageResize);
		ta.removeEventListener('input', update);
		ta.removeEventListener('keyup', update);
		ta.removeEventListener('autosize:destroy', destroy);
		ta.removeEventListener('autosize:update', update);
		set.delete(ta);

		Object.keys(style).forEach(key => {
			ta.style[key] = style[key];
		});
	}.bind(ta, {
		height: ta.style.height,
		resize: ta.style.resize,
		overflowY: ta.style.overflowY,
		overflowX: ta.style.overflowX,
		wordWrap: ta.style.wordWrap,
	});

	ta.addEventListener('autosize:destroy', destroy);

	// IE9 does not fire onpropertychange or oninput for deletions,
	// so binding to onkeyup to catch most of those events.
	// There is no way that I know of to detect something like 'cut' in IE9.
	if ('onpropertychange' in ta && 'oninput' in ta) {
		ta.addEventListener('keyup', update);
	}

	window.addEventListener('resize', pageResize);
	ta.addEventListener('input', update);
	ta.addEventListener('autosize:update', update);
	set.add(ta);

	if (setOverflowX) {
		ta.style.overflowX = 'hidden';
		ta.style.wordWrap = 'break-word';
	}

	init();
}

function destroy(ta) {
	if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
	const evt = document.createEvent('Event');
	evt.initEvent('autosize:destroy', true, false);
	ta.dispatchEvent(evt);
}

function update(ta) {
	if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
	const evt = document.createEvent('Event');
	evt.initEvent('autosize:update', true, false);
	ta.dispatchEvent(evt);
}

let autosize = null;

// Do nothing in Node.js environment and IE8 (or lower)
if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
	autosize = el => el;
	autosize.destroy = el => el;
	autosize.update = el => el;
} else {
	autosize = (el, options) => {
		if (el) {
			Array.prototype.forEach.call(el.length ? el : [el], x => assign(x, options));
		}
		return el;
	};
	autosize.destroy = el => {
		if (el) {
			Array.prototype.forEach.call(el.length ? el : [el], destroy);
		}
		return el;
	};
	autosize.update = el => {
		if (el) {
			Array.prototype.forEach.call(el.length ? el : [el], update);
		}
		return el;
	};
}

export default autosize;
<|endoftext|>


<commit_before>// Barbiche
// version: 0.4.0
// author: Manuel Baclet <manuel@eda.sarl>
// license: MIT

'use strict';

var prefix = 'bb-';
var globalAttr = 'bb-global';
var destructive = false;
var re;
var store = {};

var context = {
	stack: []
};

context.resolve = function(identifier) {
	var m = this.stack.length - 1;
	var value;
	while (value === undefined && m >= 0) {
		value = this.stack[m][identifier];
		m--;
	}
	return (value === undefined) ? window[identifier] : value;
};

context.init = function(arr) {
	this.stack = arr || [];
};

context.push = function(obj) {
	this.stack.push(obj);
};

context.pop = function() {
	return this.stack.pop();
};

function bbObj(a, b) {
	this.value = a;
	this.name = b;
}

bbObj.prototype.toString = function() {
	if (this.value && this.name != null) return this.name.toString();
	else return '';
}

var Parser = require('../parser.js');
Parser.parser.yy.context = context;
Parser.parser.yy.bbObj = bbObj;

function Barbiche(node) {
	if (node instanceof bbObj) {
		var name = node.name;
		if (store[name]) return store[name];
		else {
			var t = document.createElement('template');
			t.innerHTML = node.value;
			if (name) t.id = name;
			node = t;
		}
	}
	if (typeof(node) == 'string') {
		if (store[node]) return store[node];
		else node = document.querySelector('#' + node);
	}
	if (!(this instanceof Barbiche)) {
		return new Barbiche(node);
	}
	if (node) {
		if (node.id) store[node.id] = this;
		this.closures = {};
		this.node = (destructive) ? node : node.cloneNode(true);
		this.ready = false;
	}
	return this;
}

Barbiche.bbObj = bbObj;

Barbiche.setPrefix = function(str) {
	prefix = str;
	globalAttr = str + 'global';
};

Barbiche.setDestructive = function(bool) {
	destructive = bool;
};

Barbiche.setDelimiters = function(arr) {
	var str = '{{([^{}]*)}}|{{{([^{}]*)}}}|([^{}]+)';
	var escaped = arr.map(function(char) {
		return char.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
	});
	re = new RegExp(str.replace(/{|}/g, function(match) {if (match == '{') return escaped[0]; else return escaped[1];}), 'g');
};

Barbiche.setDelimiters(['{', '}']);

Barbiche.clean = function(name) {
	if (name) delete store[name];
	else {
		for (var key in store) {
			delete store[key];
		}
	}
};

function addPrefix(str) {
	return prefix + str;
}

var compile_works = {};
compile_works[Node.ELEMENT_NODE] = function(node, template) {
	if (node.hasAttribute(prefix + 'repeat') && node.nodeName != 'TEMPLATE') {
		if (node.hasAttribute(prefix + 'text') || node.hasAttribute(prefix + 'html')) node.removeAttribute(prefix + 'repeat');
		else {
			var wrapper = document.createElement('template');
			wrapper.setAttribute(prefix + 'repeat', node.getAttribute(prefix + 'repeat'));
			node.removeAttribute(prefix + 'repeat');
			['if', 'alias'].map(addPrefix).forEach(function(attr) {
				if (node.hasAttribute(attr)) {
					wrapper.setAttribute(attr, node.getAttribute(attr));
					node.removeAttribute(attr);
				}
			});
			node.before(wrapper);
			wrapper.content.appendChild(node);
			node = wrapper;
		}
	}
	var bbAttrs = {};
	var attrFound = false;
	['if', 'alias', 'text', 'html', 'repeat', 'import', 'attr', 'class'].forEach(function(attr) {
		if (node.hasAttribute(prefix + attr)) {
			attrFound = true;
			var parsed = Parser.parse(node.getAttribute(prefix + attr));
			bbAttrs[attr] = template._addClosure(parsed);
			node.removeAttribute(prefix + attr);
		}
	});
	if (attrFound) node.setAttribute(globalAttr, JSON.stringify(bbAttrs));
	if (node.nodeName == 'TEMPLATE') {
		compile(node.content, template);
	} else {
		Array.from(node.childNodes).forEach(function(child) {
			compile(child, template);
		});
	}
}

compile_works[Node.TEXT_NODE] = function(node, template) {
	var value = node.nodeValue;
	var tokenFound = false;
	var res;
	var stack = [];
	while(res = re.exec(value)) {
		if (res[3]) {
			stack.push({
				content: res[3],
				type: 'plain'
			});
		} else if (res[1]) {
			stack.push({
				content: res[1],
				type: 'text'
			});
			tokenFound = true;
		} else {
			stack.push({
				content: res[2],
				type: 'html'
			});
			tokenFound = true;
		}
	}
	if (tokenFound) {
		function resolve(accu) {
			var exp = accu.join('+');
			if (exp) {
				var t = document.createElement('template');
				t.setAttribute(prefix + 'text', exp);
				node.before(t); compile(t, template);
			}
		}
		resolve(stack.reduce(function(accu, item) {
			if (item.type == 'plain') {
				accu.push('"' + item.content.replace(/('|"|\n|\t|\r)/g, function() {return '\\' + arguments[1];}) + '"');
				return accu;
			} else if (item.type == 'text') {
				accu.push("(" + item.content + ")");
				return accu;
			} else {
				resolve(accu);
				var t = document.createElement('template');
				t.setAttribute(prefix + 'html', item.content);
				node.before(t); compile(t, template);
				return [];
			}
		}, []));
		node.remove();
	}
};

compile_works[Node.COMMENT_NODE] = function(node, template) {};

compile_works[Node.DOCUMENT_FRAGMENT_NODE] = function(node, template) {
	Array.from(node.childNodes).forEach(function(child) {compile(child, template);});
};

Barbiche.prototype._addClosure = (function() {
	var counter = 0;
	return function(fun) {
		counter++;
		var str = 'fun' + counter;
		this.closures[str] = fun;
		return str;
	};
})();

Barbiche.prototype._compile = function() {
	compile(this.node.content, this);
	this.ready = true;
	return this;
};

function compile(node, template) {
	(compile_works[node.nodeType])(node, template);
}

Barbiche.prototype._clone = function() {
	if (!this.ready) this._compile();
	var t = new Barbiche();
	t.node = this.node.cloneNode(true);
	t.closures = this.closures;
	return t;
}

Barbiche.prototype.merge = function() {
	var clone = this._clone();
	var args = new Array(arguments.length);
	for(var i = 0; i < args.length; ++i) {
		args[i] = arguments[i];
	}
	context.init(args);
	merge(clone.node.content, clone);
	context.init();
	return clone.node.content;
}

var works = {};
works[Node.ELEMENT_NODE] = function(node, template) {
	var nodeContext = {};
	var nodeContextPushed = false;
	var bbAttrs = JSON.parse(node.getAttribute(globalAttr));
	node.removeAttribute(globalAttr);
	if (bbAttrs.if) {
		var value = (template.closures[bbAttrs.if])();
		if (!value) return node.remove();
	}
	if (bbAttrs.alias) {
		var parsed = (template.closures[bbAttrs.alias])();
		if (!Array.isArray(parsed)) parsed = [parsed];
		parsed.forEach(function(item) {
			nodeContext[item.name] = item.value;
		});
		context.push(nodeContext);
		nodeContextPushed = true;
	}
	if (bbAttrs.text) {
		var value = (template.closures[bbAttrs.text])();
		if (value != null) value = value.toString();
		if (value) {
			node.replaceWith(value);
		} else node.remove();
	} else if (bbAttrs.html) {
		var value = (template.closures[bbAttrs.html])();
		if (value != null) value = value.toString();
		if (value) {
			var template = document.createElement('template');
			template.innerHTML = value;
			node.replaceWith(template.content);
		} else node.remove();
	} else if (node.nodeName == "TEMPLATE") {
		if (bbAttrs.repeat) {
			if (!nodeContextPushed) {
				context.push(nodeContext);
				nodeContextPushed = true;
			}
			var parsed = (template.closures[bbAttrs.repeat])();
			var order = parsed._order || 'before';
			if (!Array.isArray(parsed)) parsed = [parsed];

			var reduceInit = (function(str) {
				var closure = template.closures[bbAttrs.import];
				if (closure) return function() {
					var clone = Barbiche(closure())._clone();
					node[order](merge(clone.node.content, clone));
				}; else return function() {
					node[order](merge(node.cloneNode(true).content, template));
				};
			})(bbAttrs.import);

			//iterate on cartesian product of arrays:
			(parsed.reduceRight(function(accu, task) {
				var alias = task.name;
				var value = task.value;
				return function() {
					value.forEach(function(item, index) {
						nodeContext[alias] = value[index];
						nodeContext['_' + alias + '_'] = index;
						accu();
					})
				};
			}, reduceInit))();
		} else if (bbAttrs.import) {
			var importId = (template.closures[bbAttrs.import])();
			var clone = Barbiche(importId)._clone();
			node.before(merge(clone.node.content, clone));
		} else {
			node.before(merge(node.content, template));
		}
		node.remove();
	} else {
		if (bbAttrs.attr) {
			var parsed = (template.closures[bbAttrs.attr])();
			if (!Array.isArray(parsed)) parsed = [parsed];
			parsed.forEach(function(item) {
				var value = item.value;
				var name = item.name && item.name.toString();
				if (name && value != null) node.setAttribute(name, value);
			});
		}
		if (bbAttrs.class) {
			var parsed = (template.closures[bbAttrs.class])();
			if (!Array.isArray(parsed)) parsed = [parsed];
			parsed.forEach(function(item) {
				if (item != null) {
					var value = item.toString();
					if (value) node.classList.add(value);
				}
			});
		}
		var child;
		while(child = node.querySelector(globalAttrSel)) {merge(child, template);}
	}
	if (nodeContextPushed) context.pop();
};

works[Node.DOCUMENT_FRAGMENT_NODE] = function(node, template) {
	var child;
	while(child = node.querySelector(globalAttrSel)) {merge(child, template);}
};

function merge(node, template) {
	(works[node.nodeType])(node, template);
	return node;
}

module.exports = Barbiche;
<commit_msg>Handle <template> with no special attributes when compiling<commit_after>// Barbiche
// version: 0.4.0
// author: Manuel Baclet <manuel@eda.sarl>
// license: MIT

'use strict';

var prefix = 'bb-';
var globalAttr = 'bb-global';
var destructive = false;
var re;
var store = {};

var context = {
	stack: []
};

context.resolve = function(identifier) {
	var m = this.stack.length - 1;
	var value;
	while (value === undefined && m >= 0) {
		value = this.stack[m][identifier];
		m--;
	}
	return (value === undefined) ? window[identifier] : value;
};

context.init = function(arr) {
	this.stack = arr || [];
};

context.push = function(obj) {
	this.stack.push(obj);
};

context.pop = function() {
	return this.stack.pop();
};

function bbObj(a, b) {
	this.value = a;
	this.name = b;
}

bbObj.prototype.toString = function() {
	if (this.value && this.name != null) return this.name.toString();
	else return '';
}

var Parser = require('../parser.js');
Parser.parser.yy.context = context;
Parser.parser.yy.bbObj = bbObj;

function Barbiche(node) {
	if (node instanceof bbObj) {
		var name = node.name;
		if (store[name]) return store[name];
		else {
			var t = document.createElement('template');
			t.innerHTML = node.value;
			if (name) t.id = name;
			node = t;
		}
	}
	if (typeof(node) == 'string') {
		if (store[node]) return store[node];
		else node = document.querySelector('#' + node);
	}
	if (!(this instanceof Barbiche)) {
		return new Barbiche(node);
	}
	if (node) {
		if (node.id) store[node.id] = this;
		this.closures = {};
		this.node = (destructive) ? node : node.cloneNode(true);
		this.ready = false;
	}
	return this;
}

Barbiche.bbObj = bbObj;

Barbiche.setPrefix = function(str) {
	prefix = str;
	globalAttr = str + 'global';
};

Barbiche.setDestructive = function(bool) {
	destructive = bool;
};

Barbiche.setDelimiters = function(arr) {
	var str = '{{([^{}]*)}}|{{{([^{}]*)}}}|([^{}]+)';
	var escaped = arr.map(function(char) {
		return char.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
	});
	re = new RegExp(str.replace(/{|}/g, function(match) {if (match == '{') return escaped[0]; else return escaped[1];}), 'g');
};

Barbiche.setDelimiters(['{', '}']);

Barbiche.clean = function(name) {
	if (name) delete store[name];
	else {
		for (var key in store) {
			delete store[key];
		}
	}
};

function addPrefix(str) {
	return prefix + str;
}

var compile_works = {};
compile_works[Node.ELEMENT_NODE] = function(node, template) {
	if (node.hasAttribute(prefix + 'repeat') && node.nodeName != 'TEMPLATE') {
		if (node.hasAttribute(prefix + 'text') || node.hasAttribute(prefix + 'html')) node.removeAttribute(prefix + 'repeat');
		else {
			var wrapper = document.createElement('template');
			wrapper.setAttribute(prefix + 'repeat', node.getAttribute(prefix + 'repeat'));
			node.removeAttribute(prefix + 'repeat');
			['if', 'alias'].map(addPrefix).forEach(function(attr) {
				if (node.hasAttribute(attr)) {
					wrapper.setAttribute(attr, node.getAttribute(attr));
					node.removeAttribute(attr);
				}
			});
			node.before(wrapper);
			wrapper.content.appendChild(node);
			node = wrapper;
		}
	}
	var bbAttrs = {};
	var attrFound = false;
	['if', 'alias', 'text', 'html', 'repeat', 'import', 'attr', 'class'].forEach(function(attr) {
		if (node.hasAttribute(prefix + attr)) {
			attrFound = true;
			var parsed = Parser.parse(node.getAttribute(prefix + attr));
			bbAttrs[attr] = template._addClosure(parsed);
			node.removeAttribute(prefix + attr);
		}
	});
	if (attrFound) node.setAttribute(globalAttr, JSON.stringify(bbAttrs));
	if (node.nodeName == 'TEMPLATE') {
		compile(node.content, template);
		if (!attrFound) node.replaceWith(node.content);
	} else {
		Array.from(node.childNodes).forEach(function(child) {
			compile(child, template);
		});
	}
}

compile_works[Node.TEXT_NODE] = function(node, template) {
	var value = node.nodeValue;
	var tokenFound = false;
	var res;
	var stack = [];
	while(res = re.exec(value)) {
		if (res[3]) {
			stack.push({
				content: res[3],
				type: 'plain'
			});
		} else if (res[1]) {
			stack.push({
				content: res[1],
				type: 'text'
			});
			tokenFound = true;
		} else {
			stack.push({
				content: res[2],
				type: 'html'
			});
			tokenFound = true;
		}
	}
	if (tokenFound) {
		function resolve(accu) {
			var exp = accu.join('+');
			if (exp) {
				var t = document.createElement('template');
				t.setAttribute(prefix + 'text', exp);
				node.before(t); compile(t, template);
			}
		}
		resolve(stack.reduce(function(accu, item) {
			if (item.type == 'plain') {
				accu.push('"' + item.content.replace(/('|"|\n|\t|\r)/g, function() {return '\\' + arguments[1];}) + '"');
				return accu;
			} else if (item.type == 'text') {
				accu.push("(" + item.content + ")");
				return accu;
			} else {
				resolve(accu);
				var t = document.createElement('template');
				t.setAttribute(prefix + 'html', item.content);
				node.before(t); compile(t, template);
				return [];
			}
		}, []));
		node.remove();
	}
};

compile_works[Node.COMMENT_NODE] = function(node, template) {};

compile_works[Node.DOCUMENT_FRAGMENT_NODE] = function(node, template) {
	Array.from(node.childNodes).forEach(function(child) {compile(child, template);});
};

Barbiche.prototype._addClosure = (function() {
	var counter = 0;
	return function(fun) {
		counter++;
		var str = 'fun' + counter;
		this.closures[str] = fun;
		return str;
	};
})();

Barbiche.prototype._compile = function() {
	compile(this.node.content, this);
	this.ready = true;
	return this;
};

function compile(node, template) {
	(compile_works[node.nodeType])(node, template);
}

Barbiche.prototype._clone = function() {
	if (!this.ready) this._compile();
	var t = new Barbiche();
	t.node = this.node.cloneNode(true);
	t.closures = this.closures;
	return t;
}

Barbiche.prototype.merge = function() {
	var clone = this._clone();
	var args = new Array(arguments.length);
	for(var i = 0; i < args.length; ++i) {
		args[i] = arguments[i];
	}
	context.init(args);
	merge(clone.node.content, clone);
	context.init();
	return clone.node.content;
}

var works = {};
works[Node.ELEMENT_NODE] = function(node, template) {
	var nodeContext = {};
	var nodeContextPushed = false;
	var bbAttrs = JSON.parse(node.getAttribute(globalAttr));
	node.removeAttribute(globalAttr);
	if (bbAttrs.if) {
		var value = (template.closures[bbAttrs.if])();
		if (!value) return node.remove();
	}
	if (bbAttrs.alias) {
		var parsed = (template.closures[bbAttrs.alias])();
		if (!Array.isArray(parsed)) parsed = [parsed];
		parsed.forEach(function(item) {
			nodeContext[item.name] = item.value;
		});
		context.push(nodeContext);
		nodeContextPushed = true;
	}
	if (bbAttrs.text) {
		var value = (template.closures[bbAttrs.text])();
		if (value != null) value = value.toString();
		if (value) {
			node.replaceWith(value);
		} else node.remove();
	} else if (bbAttrs.html) {
		var value = (template.closures[bbAttrs.html])();
		if (value != null) value = value.toString();
		if (value) {
			var template = document.createElement('template');
			template.innerHTML = value;
			node.replaceWith(template.content);
		} else node.remove();
	} else if (node.nodeName == "TEMPLATE") {
		if (bbAttrs.repeat) {
			if (!nodeContextPushed) {
				context.push(nodeContext);
				nodeContextPushed = true;
			}
			var parsed = (template.closures[bbAttrs.repeat])();
			var order = parsed._order || 'before';
			if (!Array.isArray(parsed)) parsed = [parsed];

			var reduceInit = (function(str) {
				var closure = template.closures[bbAttrs.import];
				if (closure) return function() {
					var clone = Barbiche(closure())._clone();
					node[order](merge(clone.node.content, clone));
				}; else return function() {
					node[order](merge(node.cloneNode(true).content, template));
				};
			})(bbAttrs.import);

			//iterate on cartesian product of arrays:
			(parsed.reduceRight(function(accu, task) {
				var alias = task.name;
				var value = task.value;
				return function() {
					value.forEach(function(item, index) {
						nodeContext[alias] = value[index];
						nodeContext['_' + alias + '_'] = index;
						accu();
					})
				};
			}, reduceInit))();
		} else if (bbAttrs.import) {
			var importId = (template.closures[bbAttrs.import])();
			var clone = Barbiche(importId)._clone();
			node.before(merge(clone.node.content, clone));
		} else {
			node.before(merge(node.content, template));
		}
		node.remove();
	} else {
		if (bbAttrs.attr) {
			var parsed = (template.closures[bbAttrs.attr])();
			if (!Array.isArray(parsed)) parsed = [parsed];
			parsed.forEach(function(item) {
				var value = item.value;
				var name = item.name && item.name.toString();
				if (name && value != null) node.setAttribute(name, value);
			});
		}
		if (bbAttrs.class) {
			var parsed = (template.closures[bbAttrs.class])();
			if (!Array.isArray(parsed)) parsed = [parsed];
			parsed.forEach(function(item) {
				if (item != null) {
					var value = item.toString();
					if (value) node.classList.add(value);
				}
			});
		}
		var child;
		while(child = node.querySelector(globalAttrSel)) {merge(child, template);}
	}
	if (nodeContextPushed) context.pop();
};

works[Node.DOCUMENT_FRAGMENT_NODE] = function(node, template) {
	var child;
	while(child = node.querySelector(globalAttrSel)) {merge(child, template);}
};

function merge(node, template) {
	(works[node.nodeType])(node, template);
	return node;
}

module.exports = Barbiche;
<|endoftext|>


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

    /**
     * An abstract class of which all API modules should extend.
     * @class BaseApi
     */
    var BaseApi = {

        /**
         * Loads the script to the API.
         * @param {Object} options - load options
         * @param {Object} options.scriptUrl - The src url of the script js file
         * @param {Object} options.apiConfig - The FB.init() options
         * @param {Function} [callback] - Fires when the FB SDK has been loaded passed the FB object
         * @abstract
         */
        load: function (options, callback) {
            callback ? callback() : null;
        },

        /**
         * Loads the javascript file for the API.
         * @param {HTMLElement} el - The element of which to attach the script element
         * @param {string} path - The value that is added as the src path to the script
         * @param {string} id - The unique id attribute that will be added to the script tag
         * @param {Function} [callback] - The callback fired when the script finishes loading.
         * @abstract
         */
        loadScript: function (el, path, id, callback) {
            var js,
                fjs = el.getElementsByTagName('script')[0];
            if (!el.querySelector('#' + id)) {
                js = document.createElement('script');
                js.id = id;
                js.src = path;
                fjs.parentNode.insertBefore(js, fjs);
            }
            callback ? callback() : null;
        }

    };
    return BaseApi;

});
<commit_msg>Making so that loadScript() fires callback on true successful download of script<commit_after>define( function () {
    'use strict';

    /**
     * An abstract class of which all API modules should extend.
     * @class BaseApi
     */
    var BaseApi = {

        /**
         * Loads the script to the API.
         * @param {Object} options - load options
         * @param {Object} options.scriptUrl - The src url of the script js file
         * @param {Object} options.apiConfig - The FB.init() options
         * @param {Function} [callback] - Fires when the FB SDK has been loaded passed the FB object
         * @abstract
         */
        load: function (options, callback) {
            callback ? callback() : null;
        },

        /**
         * Loads the javascript file for the API.
         * @param {HTMLElement} el - The element of which to attach the script element
         * @param {string} path - The value that is added as the src path to the script
         * @param {string} id - The unique id attribute that will be added to the script tag
         * @param {Function} [callback] - The callback fired when the script finishes loading.
         * @abstract
         */
        loadScript: function (el, path, id, callback) {
            var scriptEl,
                parentEl = el.getElementsByTagName('script')[0],
                loaded = false;
            if (!el.querySelector('#' + id)) {
                scriptEl = document.createElement('script');
                scriptEl.id = id;
                scriptEl.src = path;
                scriptEl.onload = scriptEl.onreadystatechange = function () {
                    if (!loaded && (!this.readyState || this.readyState === 'complete')) {
                        loaded = true;
                        callback ? callback() : null;
                    }
                };
                parentEl.parentNode.insertBefore(scriptEl, parentEl);
            }
        }
    };
    return BaseApi;

});
<|endoftext|>


<commit_before>'use strict';

var awsRegions = require(__dirname + '/awsRegions.js');

function Constructor(rssItem) {
  var itemData = {title: '', summary: '', pubdate: '', link: '', guid:''};
  var i;
  var itemAttributes = Object.keys(itemData);

  function parseGuid() {
    var guid = itemData.guid;
    var service;
    var region;
    var guidParts = guid.split('#');
    var regexp;
    var regexpResults;
    if (guidParts[0] !== itemData.link) {
      throw new Error('Unknown guid format - expecting base url: ' + guid);
    }
    service = guidParts[1].split('-')[0];
    if (! service) {
      throw new Error('Unknown guid format - cannot parse service: ' + guid);
    }
    regexp = new RegExp('^' + service + '-([a-z0-9-]+)_[0-9]+$', 'i');
    regexpResults = guidParts[1].match(regexp);
    if (! regexpResults) {
      throw new Error('Unknown guid format - cannot parse region: ' + guid);
    }
    region = regexpResults[1];

    if (awsRegions.indexOf(region) < 0) {
      throw new Error('Unknown guid format - unknown region ' +
          region + ': ' + guid);
    }

    itemData.service = service;
    itemData.region = region;
  }

  if (!rssItem) {
    throw new Error('RSS Item must be passed to constructor');
  }

  for (i = 0; i < itemAttributes.length; i++) {
    if (rssItem.hasOwnProperty(itemAttributes[i])) {
      itemData[itemAttributes[i]] = rssItem[itemAttributes[i]];
    } else {
      throw new Error('Missing required attribute ' + itemAttributes[i]);
    }
  }

  parseGuid();

  if (itemData.title.match(/RESOLVED/)) {
    itemData.color = 'good';
  } else {
    itemData.color = 'warning';
  }

  itemData.slackAttachment = {
    'fallback': itemData.pubdate.toUTCString() + ': ' +
      itemData.title + '\n' + itemData.summary,
    'color': itemData.color,
    'pretext': itemData.pubdate.toUTCString(),
    'title': itemData.title,
    'title_link': itemData.guid,
    'author_name': 'AWS Service Status',
    'author_link': itemData.link,
    'text': itemData.summary,
    'fields': [{
      'title': 'Region',
      'value': itemData.region,
      'short': true
    },{
      'title': 'Service',
      'value': itemData.service,
      'short': true
    }]
  };

  this.getSlackAttachments = function () {
    return [itemData.slackAttachment];
  };
}

module.exports = Constructor;
<commit_msg>Support global services<commit_after>'use strict';

var awsRegions = require(__dirname + '/awsRegions.js');

function Constructor(rssItem) {
  var itemData = {title: '', summary: '', pubdate: '', link: '', guid:''};
  var i;
  var itemAttributes = Object.keys(itemData);

  function parseGuid() {
    var guid = itemData.guid;
    var service;
    var region;
    var guidParts = guid.split('#');
    var regexp;
    var regexpResults;
    if (guidParts[0] !== itemData.link) {
      throw new Error('Unknown guid format - expecting base url: ' + guid);
    }

    // bail out early if it's a Global service
    if (guidParts[1].match(/^(cloudfront|route53|waf)_[0-9]+/)) {
      itemData.service = service;
      itemData.region = 'global';
      return;
    }

    service = guidParts[1].split('-')[0];
    if (! service) {
      throw new Error('Unknown guid format - cannot parse service: ' + guid);
    }

    regexp = new RegExp('^' + service + '-([a-z0-9-]+)_[0-9]+$', 'i');
    regexpResults = guidParts[1].match(regexp);
    if (! regexpResults) {
      throw new Error('Unknown guid format - cannot parse region: ' + guid);
    }
    region = regexpResults[1];

    if (awsRegions.indexOf(region) < 0) {
      throw new Error('Unknown guid format - unknown region ' +
          region + ': ' + guid);
    }

    itemData.service = service;
    itemData.region = region;
  }

  if (!rssItem) {
    throw new Error('RSS Item must be passed to constructor');
  }

  for (i = 0; i < itemAttributes.length; i++) {
    if (rssItem.hasOwnProperty(itemAttributes[i])) {
      itemData[itemAttributes[i]] = rssItem[itemAttributes[i]];
    } else {
      throw new Error('Missing required attribute ' + itemAttributes[i]);
    }
  }

  parseGuid();

  if (itemData.title.match(/RESOLVED/)) {
    itemData.color = 'good';
  } else {
    itemData.color = 'warning';
  }

  itemData.slackAttachment = {
    'fallback': itemData.pubdate.toUTCString() + ': ' +
      itemData.title + '\n' + itemData.summary,
    'color': itemData.color,
    'pretext': itemData.pubdate.toUTCString(),
    'title': itemData.title,
    'title_link': itemData.guid,
    'author_name': 'AWS Service Status',
    'author_link': itemData.link,
    'text': itemData.summary,
    'fields': [{
      'title': 'Region',
      'value': itemData.region,
      'short': true
    },{
      'title': 'Service',
      'value': itemData.service,
      'short': true
    }]
  };

  this.getSlackAttachments = function () {
    return [itemData.slackAttachment];
  };
}

module.exports = Constructor;
<|endoftext|>