<commit_before>/* ===================================================
 * bootstrap-tagmanager.js v3.0.0
 * http://welldonethings.com/tags/manager
 * ===================================================
 * Copyright 2012 Max Favilli
 *
 * Licensed under the Mozilla Public License, Version 2.0 You may not use this work except in compliance with the License.
 *
 * http://www.mozilla.org/MPL/2.0/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ========================================================== */

(function ($) {

  "use strict";

  if (typeof console === "undefined" || typeof console.log === "undefined") {
    console = {};
    console.log = function () { };
  }

  $.fn.tagsManager = function (options, tagToManipulate) {
    var obj = this;
    var rndid = "";

    var tagManagerOptions = {
      version_three: true,
      prefilled: null,
      CapitalizeFirstLetter: false,
      preventSubmitOnEnter: true, // deprecated
      isClearInputOnEsc: true, // deprecated
      typeahead: false,
      typeaheadAjaxMethod: "POST",
      typeaheadAjaxSource: null,
      typeaheadAjaxPolling: false,
      typeaheadOverrides: null,
      typeaheadDelegate: {},
      typeaheadSource: null,
      AjaxPush: null,
      AjaxPushAllTags: null,
      AjaxPushParameters: null,
      delimiters: [9, 13, 44], // tab, enter, comma
      backspace: [8],
      maxTags: 0,
      hiddenTagListName: null,
      hiddenTagListId: null,
      deleteTagsOnBackspace: true, // deprecated
      tagsContainer: null,
      tagCloseIcon: 'x',
      tagClass: '',
      validator: null,
      onlyTagList: false
    };

    // exit when no matched elements
    if (!(0 in this)) {
      return this;
    }

    if (typeof options == 'string') {
      tagManagerOptions = obj.data("tm_options");
    } else {
      $.extend(tagManagerOptions, options);
      obj.data("tm_options", tagManagerOptions);
    }

    if (typeof options == 'string') {
      rndid = obj.data("tm_rndid");
    } else {
      var albet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
      for (var i = 0; i < 5; i++)
        rndid += albet.charAt(Math.floor(Math.random() * albet.length));
      obj.data("tm_rndid", rndid);
    }

    if (tagManagerOptions.hiddenTagListName === null) {
      tagManagerOptions.hiddenTagListName = "hidden-" + rndid;
    }

    var delimiters = tagManagerOptions.delimeters || tagManagerOptions.delimiters; // 'delimeter' is deprecated
    // delimiter values to be handled as key codes
    var keyNums = [9, 13, 17, 18, 19, 37, 38, 39, 40];
    var delimiterChars = [], delimiterKeys = [];
    $.each(delimiters, function (i, v) {
      if ($.inArray(v, keyNums) != -1) {
        delimiterKeys.push(v);
      } else {
        delimiterChars.push(v);
      }
    });
    var baseDelimiter = String.fromCharCode(delimiterChars[0] || 44);
    var backspace = tagManagerOptions.backspace;
    var tagBaseClass = 'tm-tag';
    var inputBaseClass = 'tm-input';

    if ($.isFunction(tagManagerOptions.validator)) obj.data('validator', tagManagerOptions.validator);

    //var ajaxPolling = function (query, process) {
    //  if (typeof (tagManagerOptions.typeaheadAjaxSource) == "string") {
    //    $.ajax({
    //      cache: false,
    //      type: "POST",
    //      contentType: "application/json",
    //      dataType: "json",
    //      url: tagManagerOptions.typeaheadAjaxSource,
    //      data: JSON.stringify({ typeahead: query }),
    //      success: function (data) { onTypeaheadAjaxSuccess(data, false, process); }
    //    });
    //  }
    //};

    var tagClasses = function () {
      // 1) default class (tm-tag)
      var cl = tagBaseClass;
      // 2) interpolate from input class: tm-input-xxx --> tm-tag-xxx
      if (obj.attr('class')) {
        $.each(obj.attr('class').split(' '), function (index, value) {
          if (value.indexOf(inputBaseClass + '-') != -1) {
            cl += ' ' + tagBaseClass + value.substring(inputBaseClass.length);
          }
        });
      }
      // 3) tags from tagClass option
      cl += (tagManagerOptions.tagClass ? ' ' + tagManagerOptions.tagClass : '');
      return cl;
    };

    var trimTag = function (tag) {
      tag = $.trim(tag);
      // truncate at the first delimiter char
      var i = 0;
      for (i; i < tag.length; i++) {
        if ($.inArray(tag.charCodeAt(i), delimiterChars) != -1) break;
      }
      return tag.substring(0, i);
    };

    var popTag = function () {
      var tlis = obj.data("tlis");
      var tlid = obj.data("tlid");

      if (tlid.length > 0) {
        var tagId = tlid.pop();

        var tagBeingRemoved = tlis[tlis.length - 1];
        obj.trigger('tm:popping', tagBeingRemoved);
        tlis.pop();

        // console.log("TagIdToRemove: " + tagId);
        $("#" + rndid + "_" + tagId).remove();
        refreshHiddenTagList();
        obj.trigger('tm:popped', tagBeingRemoved);
        // console.log(tlis);
      }

      if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
        obj.show();
      }
    };

    var empty = function () {
      var tlis = obj.data("tlis");
      var tlid = obj.data("tlid");

      while (tlid.length > 0) {
        var tagId = tlid.pop();
        tlis.pop();
        // console.log("TagIdToRemove: " + tagId);
        $("#" + rndid + "_" + tagId).remove();
        refreshHiddenTagList();
        // console.log(tlis);
      }
      obj.trigger('tm:emptied', null);

      if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
        obj.show();
      }
    };

    var refreshHiddenTagList = function () {
      var tlis = obj.data("tlis");
      var lhiddenTagList = obj.data("lhiddenTagList");

      obj.trigger('tm:refresh', tlis.join(baseDelimiter));

      if (lhiddenTagList) {
        $(lhiddenTagList).val(tlis.join(baseDelimiter)).change();
      }
    };

    var spliceTag = function (tagId) {
      var tlis = obj.data("tlis");
      var tlid = obj.data("tlid");

      var p = $.inArray(tagId, tlid);

      // console.log("TagIdToRemove: " + tagId);
      // console.log("position: " + p);

      if (-1 != p) {
        var tagBeingRemoved = tlis[p];

        obj.trigger('tm:splicing', tagBeingRemoved);

        $("#" + rndid + "_" + tagId).remove();
        tlis.splice(p, 1);
        tlid.splice(p, 1);
        refreshHiddenTagList();

        obj.trigger('tm:spliced', tagBeingRemoved);

        // console.log(tlis);
      }

      if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
        obj.show();
      }
    };

    var pushAllTags = function (e, tag) {
      if (tagManagerOptions.AjaxPushAllTags) {
        if (e.type != 'tm:pushed' || $.inArray(tag, tagManagerOptions.prefilled) == -1) {
          var tlis = obj.data("tlis");
          $.post(tagManagerOptions.AjaxPush, { tags: tlis.join(baseDelimiter) });
        }
      }
    };

    var pushTag = function (tag) {
      tag = trimTag(tag);

      if (!tag || tag.length <= 0) return;

      if (tagManagerOptions.CapitalizeFirstLetter && tag.length > 1) {
        tag = tag.charAt(0).toUpperCase() + tag.slice(1).toLowerCase();
      }

      // call the validator (if any) and do not let the tag pass if invalid
      if (obj.data('validator') && !obj.data('validator')(tag)) return;

      var tlis = obj.data("tlis");
      var tlid = obj.data("tlid");

      // dont accept new tags beyond the defined maximum
      if (tagManagerOptions.maxTags > 0 && tlis.length >= tagManagerOptions.maxTags) return;

      var alreadyInList = false;
      var tlisLowerCase = tlis.map(function (elem) { return elem.toLowerCase(); });
      var p = $.inArray(tag.toLowerCase(), tlisLowerCase);
      if (-1 != p) {
        // console.log("tag:" + tag + " !!already in list!!");
        alreadyInList = true;
      }

      if (alreadyInList) {
        var pTagId = tlid[p];
        $("#" + rndid + "_" + pTagId).stop()
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_1 }, 100)
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_2 }, 100)
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_1 }, 100)
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_2 }, 100)
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_1 }, 100)
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_2 }, 100);
      } else {
        obj.trigger('tm:pushing', tag);

        var max = Math.max.apply(null, tlid);
        max = max == -Infinity ? 0 : max;

        var tagId = ++max;
        tlis.push(tag);
        tlid.push(tagId);

        if (tagManagerOptions.AjaxPush != null) {
          if ($.inArray(tag, tagManagerOptions.prefilled) == -1) {
            $.post(tagManagerOptions.AjaxPush, $.extend({ tag: tag }, tagManagerOptions.AjaxPushParameters));
          }
        }

        // console.log("tagList: " + tlis);

        var newTagId = rndid + '_' + tagId;
        var newTagRemoveId = rndid + '_Remover_' + tagId;
        var escaped = $("").text(tag).html();

        var html = '';
        html += '' + escaped + '';
        html += '';
        html += tagManagerOptions.tagCloseIcon + ' ';
        var $el = $(html);

        if (tagManagerOptions.tagsContainer != null) {
          $(tagManagerOptions.tagsContainer).append($el);
        } else {
          obj.before($el);
        }

        $el.find("#" + newTagRemoveId).on("click", obj, function (e) {
          e.preventDefault();
          var TagIdToRemove = parseInt($(this).attr("TagIdToRemove"));
          spliceTag(TagIdToRemove, e.data);
        });

        refreshHiddenTagList();

        obj.trigger('tm:pushed', tag);

        if (tagManagerOptions.maxTags > 0 && tlis.length >= tagManagerOptions.maxTags) {
          obj.hide();
        }
      }
      obj.val("");
    };

    var prefill = function (pta) {
      $.each(pta, function (key, val) {
        pushTag(val);
      });
    };

    var killEvent = function (e) {
      e.cancelBubble = true;
      e.returnValue = false;
      e.stopPropagation();
      e.preventDefault();
    };

    var keyInArray = function (e, ary) {
      return $.inArray(e.which, ary) != -1
    };

    var applyDelimiter = function (e) {
      pushTag(obj.val());
      e.preventDefault();
    };

    var returnValue = null;
    this.each(function () {

      if (typeof options == 'string') {
        switch (options) {
          case "empty":
            empty();
            break;
          case "popTag":
            popTag();
            break;
          case "pushTag":
            pushTag(tagToManipulate);
            break;
          case "tags":
            returnValue = { tags: obj.data("tlis") };
            break;
        }
        return;
      }

      // prevent double-initialization of TagManager
      if ($(this).data('tagManager')) { return false; }
      $(this).data('tagManager', true);

      // store instance-specific data in the DOM object
      var tlis = new Array();
      var tlid = new Array();
      obj.data("tlis", tlis); //list of string tags
      obj.data("tlid", tlid); //list of ID of the string tags

      if (tagManagerOptions.hiddenTagListId == null) { /* if hidden input not given default activity */
        var hiddenTag = $("input[name='" + tagManagerOptions.hiddenTagListName + "']");
        if (hiddenTag.length > 0) {
          hiddenTag.remove();
        }

        var html = "";
        html += "<input name='" + tagManagerOptions.hiddenTagListName + "' type='hidden' value=''/>";
        obj.after(html);
        obj.data("lhiddenTagList",
          obj.siblings("input[name='" + tagManagerOptions.hiddenTagListName + "']")[0]
        );
      } else {
        obj.data("lhiddenTagList", $('#' + tagManagerOptions.hiddenTagListId))
      }

      if (tagManagerOptions.AjaxPushAllTags) {
        obj.on('tm:spliced', pushAllTags);
        obj.on('tm:popped', pushAllTags);
        obj.on('tm:pushed', pushAllTags);
      }

      // hide popovers on focus and keypress events
      obj.on('focus keypress', function (e) {
        if ($(this).popover) {
          $(this).popover('hide');
        }
      });

      // handle ESC (keyup used for browser compatibility)
      if (tagManagerOptions.isClearInputOnEsc) {
        obj.on('keyup', function (e) {
          if (e.which == 27) {
            // console.log('esc detected');
            $(this).val('');
            killEvent(e);
          }
        });
      }

      obj.on('keypress', function (e) {
        // push ASCII-based delimiters
        if (keyInArray(e, delimiterChars)) {
          applyDelimiter(e);
        }
      });

      obj.on('keydown', function (e) {
        // disable ENTER
        if (e.which == 13) {
          if (tagManagerOptions.preventSubmitOnEnter) {
            killEvent(e);
          }
        }

        // push key-based delimiters (includes <enter> by default)
        if (keyInArray(e, delimiterKeys)) {
          applyDelimiter(e);
        }
      });

      // BACKSPACE (keydown used for browser compatibility)
      if (tagManagerOptions.deleteTagsOnBackspace) {
        obj.on('keydown', function (e) {
          if (keyInArray(e, backspace)) {
            // console.log("backspace detected");
            if ($(this).val().length <= 0) {
              popTag();
              killEvent(e);
            }
          }
        });
      }

      obj.change(function (e) {

        if (!/webkit/.test(navigator.userAgent.toLowerCase())) { $(this).focus(); } // why?

        /* unimplemented mode to push tag on blur
         else if (tagManagerOptions.pushTagOnBlur) {
         console.log('change: pushTagOnBlur ' + tag);
         pushTag($(this).val());
         } */
        killEvent(e);
      });

      if (tagManagerOptions.prefilled != null) {
        if (typeof (tagManagerOptions.prefilled) == "object") {
          prefill(tagManagerOptions.prefilled);
        } else if (typeof (tagManagerOptions.prefilled) == "string") {
          prefill(tagManagerOptions.prefilled.split(baseDelimiter));
        } else if (typeof (tagManagerOptions.prefilled) == "function") {
          prefill(tagManagerOptions.prefilled());
        }
      } else if (tagManagerOptions.hiddenTagListId != null) {
        prefill($('#' + tagManagerOptions.hiddenTagListId).val().split(baseDelimiter));
      }
    });

    if (!returnValue)
      returnValue = this;

    return returnValue;
  }
})(jQuery);<commit_msg>re-fix of issue #149<commit_after>/* ===================================================
 * bootstrap-tagmanager.js v3.0.0
 * http://welldonethings.com/tags/manager
 * ===================================================
 * Copyright 2012 Max Favilli
 *
 * Licensed under the Mozilla Public License, Version 2.0 You may not use this work except in compliance with the License.
 *
 * http://www.mozilla.org/MPL/2.0/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ========================================================== */

(function ($) {

  "use strict";

  if (typeof console === "undefined" || typeof console.log === "undefined") {
    console = {};
    console.log = function () { };
  }

  $.fn.tagsManager = function (options, tagToManipulate) {
    var obj = this;
    var rndid = "";

    var tagManagerOptions = {
      version_three: true,
      prefilled: null,
      CapitalizeFirstLetter: false,
      preventSubmitOnEnter: true, // deprecated
      isClearInputOnEsc: true, // deprecated
      typeahead: false,
      typeaheadAjaxMethod: "POST",
      typeaheadAjaxSource: null,
      typeaheadAjaxPolling: false,
      typeaheadOverrides: null,
      typeaheadDelegate: {},
      typeaheadSource: null,
      AjaxPush: null,
      AjaxPushAllTags: null,
      AjaxPushParameters: null,
      delimiters: [9, 13, 44], // tab, enter, comma
      backspace: [8],
      maxTags: 0,
      hiddenTagListName: null,
      hiddenTagListId: null,
      deleteTagsOnBackspace: true, // deprecated
      tagsContainer: null,
      tagCloseIcon: 'x',
      tagClass: '',
      validator: null,
      onlyTagList: false
    };

    // exit when no matched elements
    if (!(0 in this)) {
      return this;
    }

    if (typeof options == 'string') {
      tagManagerOptions = obj.data("tm_options");
    } else {
      $.extend(tagManagerOptions, options);
      obj.data("tm_options", tagManagerOptions);
    }

    if (typeof options == 'string') {
      rndid = obj.data("tm_rndid");
    } else {
      var albet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
      for (var i = 0; i < 5; i++)
        rndid += albet.charAt(Math.floor(Math.random() * albet.length));
      obj.data("tm_rndid", rndid);
    }

    if (tagManagerOptions.hiddenTagListName === null) {
      tagManagerOptions.hiddenTagListName = "hidden-" + rndid;
    }

    var delimiters = tagManagerOptions.delimeters || tagManagerOptions.delimiters; // 'delimeter' is deprecated
    // delimiter values to be handled as key codes
    var keyNums = [9, 13, 17, 18, 19, 37, 38, 39, 40];
    var delimiterChars = [], delimiterKeys = [];
    $.each(delimiters, function (i, v) {
      if ($.inArray(v, keyNums) != -1) {
        delimiterKeys.push(v);
      } else {
        delimiterChars.push(v);
      }
    });
    var baseDelimiter = String.fromCharCode(delimiterChars[0] || 44);
    var backspace = tagManagerOptions.backspace;
    var tagBaseClass = 'tm-tag';
    var inputBaseClass = 'tm-input';

    if ($.isFunction(tagManagerOptions.validator)) obj.data('validator', tagManagerOptions.validator);

    //var ajaxPolling = function (query, process) {
    //  if (typeof (tagManagerOptions.typeaheadAjaxSource) == "string") {
    //    $.ajax({
    //      cache: false,
    //      type: "POST",
    //      contentType: "application/json",
    //      dataType: "json",
    //      url: tagManagerOptions.typeaheadAjaxSource,
    //      data: JSON.stringify({ typeahead: query }),
    //      success: function (data) { onTypeaheadAjaxSuccess(data, false, process); }
    //    });
    //  }
    //};

    var tagClasses = function () {
      // 1) default class (tm-tag)
      var cl = tagBaseClass;
      // 2) interpolate from input class: tm-input-xxx --> tm-tag-xxx
      if (obj.attr('class')) {
        $.each(obj.attr('class').split(' '), function (index, value) {
          if (value.indexOf(inputBaseClass + '-') != -1) {
            cl += ' ' + tagBaseClass + value.substring(inputBaseClass.length);
          }
        });
      }
      // 3) tags from tagClass option
      cl += (tagManagerOptions.tagClass ? ' ' + tagManagerOptions.tagClass : '');
      return cl;
    };

    var trimTag = function (tag) {
      tag = $.trim(tag);
      // truncate at the first delimiter char
      var i = 0;
      for (i; i < tag.length; i++) {
        if ($.inArray(tag.charCodeAt(i), delimiterChars) != -1) break;
      }
      return tag.substring(0, i);
    };

    var popTag = function () {
      var tlis = obj.data("tlis");
      var tlid = obj.data("tlid");

      if (tlid.length > 0) {
        var tagId = tlid.pop();

        var tagBeingRemoved = tlis[tlis.length - 1];
        obj.trigger('tm:popping', tagBeingRemoved);
        tlis.pop();

        // console.log("TagIdToRemove: " + tagId);
        $("#" + rndid + "_" + tagId).remove();
        refreshHiddenTagList();
        obj.trigger('tm:popped', tagBeingRemoved);
        // console.log(tlis);
      }

      if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
        obj.show();
      }
    };

    var empty = function () {
      var tlis = obj.data("tlis");
      var tlid = obj.data("tlid");

      while (tlid.length > 0) {
        var tagId = tlid.pop();
        tlis.pop();
        // console.log("TagIdToRemove: " + tagId);
        $("#" + rndid + "_" + tagId).remove();
        refreshHiddenTagList();
        // console.log(tlis);
      }
      obj.trigger('tm:emptied', null);

      if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
        obj.show();
      }
    };

    var refreshHiddenTagList = function () {
      var tlis = obj.data("tlis");
      var lhiddenTagList = obj.data("lhiddenTagList");

      obj.trigger('tm:refresh', tlis.join(baseDelimiter));

      if (lhiddenTagList) {
        $(lhiddenTagList).val(tlis.join(baseDelimiter)).change();
      }
    };

    var spliceTag = function (tagId) {
      var tlis = obj.data("tlis");
      var tlid = obj.data("tlid");

      var p = $.inArray(tagId, tlid);

      // console.log("TagIdToRemove: " + tagId);
      // console.log("position: " + p);

      if (-1 != p) {
        var tagBeingRemoved = tlis[p];

        obj.trigger('tm:splicing', tagBeingRemoved);

        $("#" + rndid + "_" + tagId).remove();
        tlis.splice(p, 1);
        tlid.splice(p, 1);
        refreshHiddenTagList();

        obj.trigger('tm:spliced', tagBeingRemoved);

        // console.log(tlis);
      }

      if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
        obj.show();
      }
    };

    var pushAllTags = function (e, tag) {
      if (tagManagerOptions.AjaxPushAllTags) {
        if (e.type != 'tm:pushed' || $.inArray(tag, tagManagerOptions.prefilled) == -1) {
          var tlis = obj.data("tlis");
          $.post(tagManagerOptions.AjaxPush, { tags: tlis.join(baseDelimiter) });
        }
      }
    };

    var pushTag = function (tag, ignore_events) {
      tag = trimTag(tag);

      if (!tag || tag.length <= 0) return;

      if (tagManagerOptions.CapitalizeFirstLetter && tag.length > 1) {
        tag = tag.charAt(0).toUpperCase() + tag.slice(1).toLowerCase();
      }

      // call the validator (if any) and do not let the tag pass if invalid
      if (obj.data('validator') && !obj.data('validator')(tag)) return;

      var tlis = obj.data("tlis");
      var tlid = obj.data("tlid");

      // dont accept new tags beyond the defined maximum
      if (tagManagerOptions.maxTags > 0 && tlis.length >= tagManagerOptions.maxTags) return;

      var alreadyInList = false;
      var tlisLowerCase = tlis.map(function (elem) { return elem.toLowerCase(); });
      var p = $.inArray(tag.toLowerCase(), tlisLowerCase);
      if (-1 != p) {
        // console.log("tag:" + tag + " !!already in list!!");
        alreadyInList = true;
      }

      if (alreadyInList) {
        var pTagId = tlid[p];
        $("#" + rndid + "_" + pTagId).stop()
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_1 }, 100)
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_2 }, 100)
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_1 }, 100)
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_2 }, 100)
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_1 }, 100)
          .animate({ backgroundColor: tagManagerOptions.blinkBGColor_2 }, 100);
      } else {
        if(!ignore_events)
          obj.trigger('tm:pushing', tag);

        var max = Math.max.apply(null, tlid);
        max = max == -Infinity ? 0 : max;

        var tagId = ++max;
        tlis.push(tag);
        tlid.push(tagId);

        if (!ignore_events)
          if (tagManagerOptions.AjaxPush != null) {
            if ($.inArray(tag, tagManagerOptions.prefilled) == -1) {
              $.post(tagManagerOptions.AjaxPush, $.extend({ tag: tag }, tagManagerOptions.AjaxPushParameters));
            }
          }

        // console.log("tagList: " + tlis);

        var newTagId = rndid + '_' + tagId;
        var newTagRemoveId = rndid + '_Remover_' + tagId;
        var escaped = $("").text(tag).html();

        var html = '';
        html += '' + escaped + '';
        html += '';
        html += tagManagerOptions.tagCloseIcon + ' ';
        var $el = $(html);

        if (tagManagerOptions.tagsContainer != null) {
          $(tagManagerOptions.tagsContainer).append($el);
        } else {
          obj.before($el);
        }

        $el.find("#" + newTagRemoveId).on("click", obj, function (e) {
          e.preventDefault();
          var TagIdToRemove = parseInt($(this).attr("TagIdToRemove"));
          spliceTag(TagIdToRemove, e.data);
        });

        refreshHiddenTagList();

        if (!ignore_events)
          obj.trigger('tm:pushed', tag);

        if (tagManagerOptions.maxTags > 0 && tlis.length >= tagManagerOptions.maxTags) {
          obj.hide();
        }
      }
      obj.val("");
    };

    var prefill = function (pta) {
      $.each(pta, function (key, val) {
        pushTag(val,true);
      });
    };

    var killEvent = function (e) {
      e.cancelBubble = true;
      e.returnValue = false;
      e.stopPropagation();
      e.preventDefault();
    };

    var keyInArray = function (e, ary) {
      return $.inArray(e.which, ary) != -1
    };

    var applyDelimiter = function (e) {
      pushTag(obj.val());
      e.preventDefault();
    };

    var returnValue = null;
    this.each(function () {

      if (typeof options == 'string') {
        switch (options) {
          case "empty":
            empty();
            break;
          case "popTag":
            popTag();
            break;
          case "pushTag":
            pushTag(tagToManipulate);
            break;
          case "tags":
            returnValue = { tags: obj.data("tlis") };
            break;
        }
        return;
      }

      // prevent double-initialization of TagManager
      if ($(this).data('tagManager')) { return false; }
      $(this).data('tagManager', true);

      // store instance-specific data in the DOM object
      var tlis = new Array();
      var tlid = new Array();
      obj.data("tlis", tlis); //list of string tags
      obj.data("tlid", tlid); //list of ID of the string tags

      if (tagManagerOptions.hiddenTagListId == null) { /* if hidden input not given default activity */
        var hiddenTag = $("input[name='" + tagManagerOptions.hiddenTagListName + "']");
        if (hiddenTag.length > 0) {
          hiddenTag.remove();
        }

        var html = "";
        html += "<input name='" + tagManagerOptions.hiddenTagListName + "' type='hidden' value=''/>";
        obj.after(html);
        obj.data("lhiddenTagList",
          obj.siblings("input[name='" + tagManagerOptions.hiddenTagListName + "']")[0]
        );
      } else {
        obj.data("lhiddenTagList", $('#' + tagManagerOptions.hiddenTagListId))
      }

      if (tagManagerOptions.AjaxPushAllTags) {
        obj.on('tm:spliced', pushAllTags);
        obj.on('tm:popped', pushAllTags);
        obj.on('tm:pushed', pushAllTags);
      }

      // hide popovers on focus and keypress events
      obj.on('focus keypress', function (e) {
        if ($(this).popover) {
          $(this).popover('hide');
        }
      });

      // handle ESC (keyup used for browser compatibility)
      if (tagManagerOptions.isClearInputOnEsc) {
        obj.on('keyup', function (e) {
          if (e.which == 27) {
            // console.log('esc detected');
            $(this).val('');
            killEvent(e);
          }
        });
      }

      obj.on('keypress', function (e) {
        // push ASCII-based delimiters
        if (keyInArray(e, delimiterChars)) {
          applyDelimiter(e);
        }
      });

      obj.on('keydown', function (e) {
        // disable ENTER
        if (e.which == 13) {
          if (tagManagerOptions.preventSubmitOnEnter) {
            killEvent(e);
          }
        }

        // push key-based delimiters (includes <enter> by default)
        if (keyInArray(e, delimiterKeys)) {
          applyDelimiter(e);
        }
      });

      // BACKSPACE (keydown used for browser compatibility)
      if (tagManagerOptions.deleteTagsOnBackspace) {
        obj.on('keydown', function (e) {
          if (keyInArray(e, backspace)) {
            // console.log("backspace detected");
            if ($(this).val().length <= 0) {
              popTag();
              killEvent(e);
            }
          }
        });
      }

      obj.change(function (e) {

        if (!/webkit/.test(navigator.userAgent.toLowerCase())) { $(this).focus(); } // why?

        /* unimplemented mode to push tag on blur
         else if (tagManagerOptions.pushTagOnBlur) {
         console.log('change: pushTagOnBlur ' + tag);
         pushTag($(this).val());
         } */
        killEvent(e);
      });

      if (tagManagerOptions.prefilled != null) {
        if (typeof (tagManagerOptions.prefilled) == "object") {
          prefill(tagManagerOptions.prefilled);
        } else if (typeof (tagManagerOptions.prefilled) == "string") {
          prefill(tagManagerOptions.prefilled.split(baseDelimiter));
        } else if (typeof (tagManagerOptions.prefilled) == "function") {
          prefill(tagManagerOptions.prefilled());
        }
      } else if (tagManagerOptions.hiddenTagListId != null) {
        prefill($('#' + tagManagerOptions.hiddenTagListId).val().split(baseDelimiter));
      }
    });

    if (!returnValue)
      returnValue = this;

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


<commit_before>
const hooks = require('hooks');
const {MongoClient} = require('mongodb');

let db;

// Fixtures
const gist = {
  '_id': '42',
  'created_at': new Date().toISOString(),
  'description': 'Gist description...',
  'content': 'String content...',
}

const star = {
  'gist_id': '42',
  'starred': true,
}

// Setup database connection before Dredd starts testing
hooks.beforeAll((transactions, done) => {
  MongoClient.connect('mongodb://localhost/dredd-example', function(err, conn) {
    db = conn;
    done(err);
  });
});

// Close database connection after Dredd finishes testing
hooks.afterAll((transactions, done) => {
  db.close();
  done();
});

// Before each test let's fix the media type - unfortunately current support
// of Swagger in Dredd works only for application/json as of now
hooks.beforeEach((transaction, done) => {
  let contentType;

  if (transaction.expected.statusCode == '204') {
    transaction.request.headers['Accept'] = 'text/plain';
    delete transaction.expected.headers['Content-Type'];
  } else {
    transaction.request.headers['Accept'] = 'application/hal+json';
    transaction.expected.headers['Content-Type'] = 'application/hal+json';
  }
  done();
});

// After each test clear contents of the database (we want isolated tests)
hooks.afterEach((transaction, done) => {
  db.collection('gists').remove({}, (err) => {
    if (err) { return done(err) }

    db.collection('stars').remove({}, (err) => {
      done(err);
    });
  });
});

// To test work with Gists and Stars in isolation, we need to add some prior
// to certain HTTP transactions Dredd is about to make
hooks.before('/gists/{id} > GET', (transaction, done) => {
  db.collection('gists').insert(gist, (err) => {
    done(err);
  });
});

hooks.before('/gists/{id} > PATCH', (transaction, done) => {
  db.collection('gists').insert(gist, (err) => {
    done(err);
  });
});

hooks.before('/gists > GET', (transaction, done) => {
  db.collection('gists').insert(gist, (err) => {
    done(err);
  });
});

hooks.before('/gists/{id}/star > GET', (transaction, done) => {
  db.collection('stars').insert(star, (err) => {
    done(err);
  });
});
<commit_msg>Update transaction names in Swagger hooks<commit_after>
const hooks = require('hooks');
const {MongoClient} = require('mongodb');

let db;

// Fixtures
const gist = {
  '_id': '42',
  'created_at': new Date().toISOString(),
  'description': 'Gist description...',
  'content': 'String content...',
}

const star = {
  'gist_id': '42',
  'starred': true,
}

// Setup database connection before Dredd starts testing
hooks.beforeAll((transactions, done) => {
  MongoClient.connect('mongodb://localhost/dredd-example', function(err, conn) {
    db = conn;
    done(err);
  });
});

// Close database connection after Dredd finishes testing
hooks.afterAll((transactions, done) => {
  db.close();
  done();
});

// Before each test let's fix the media type - unfortunately current support
// of Swagger in Dredd works only for application/json as of now
hooks.beforeEach((transaction, done) => {
  let contentType;

  if (transaction.expected.statusCode == '204') {
    transaction.request.headers['Accept'] = 'text/plain';
    delete transaction.expected.headers['Content-Type'];
  } else {
    transaction.request.headers['Accept'] = 'application/hal+json';
    transaction.expected.headers['Content-Type'] = 'application/hal+json';
  }
  done();
});

// After each test clear contents of the database (we want isolated tests)
hooks.afterEach((transaction, done) => {
  db.collection('gists').remove({}, (err) => {
    if (err) { return done(err) }

    db.collection('stars').remove({}, (err) => {
      done(err);
    });
  });
});

// To test work with Gists and Stars in isolation, we need to add some prior
// to certain HTTP transactions Dredd is about to make
hooks.before('/gists/{id} > GET > 200 > application/json', (transaction, done) => {
  db.collection('gists').insert(gist, (err) => {
    done(err);
  });
});

hooks.before('/gists/{id} > PATCH > 200 > application/json', (transaction, done) => {
  db.collection('gists').insert(gist, (err) => {
    done(err);
  });
});

hooks.before('/gists > GET > 200 > application/json', (transaction, done) => {
  db.collection('gists').insert(gist, (err) => {
    done(err);
  });
});

hooks.before('/gists/{id}/star > GET > 200 > application/json', (transaction, done) => {
  db.collection('stars').insert(star, (err) => {
    done(err);
  });
});
<|endoftext|>


<commit_before>"use strict";

/**
* Global function that tracks a click on an outbound link in Google Analytics.
* This function takes a valid URL string as an argument, and uses that URL string
* as the event label.
*/
var trackOutboundLink = function(url) {
  ga('send', 'event', 'outbound', 'click', url);
};

(function ($) {

  $(document).on('lbbready', function () {

    // Only init a map if its container is visible because Leaflet
    // has a hard time initializing maps in hidden elements.
    $('.js-map-container:visible').initMap();
    $('.js-result-toggle-details').toggleDetails();
    //updateTravelDurations();
    $('#shown-search-form').checkChanges();

    var eventLabel;
    if ($('.ga-no-results').length) {
      var eventLabel = "job:" + $('input[name="job"]').val() + ";location:" + $('input[name="location"]').val();
      ga('send', 'event', 'Resultats', 'no results', eventLabel);
    }

    $('.ga-pdf-download-link').click(function (e) {
      ga('send', 'event', 'Download', 'fiche entreprise', 1);
    });

  });

  /*
   * .initMap()
   *
   * Description:
   *   Init a Leaflet map.
   *
   * Usage:
   *   $('map-container-selector').initMap();
   *
   * Expected markup:
   *   - The container must contain a div with a `.map` class that is the map placeholder.
   *   - The container must contain some hidden inputs that will be used for building the map:
   *       * company-latitude
   *       * company-longitude
   *       * company-name
   *
   * Expected markup example:
   *

   *     <input name="company-name" type="hidden" value="{{ company.name }}">
   *     <input name="company-longitude" type="hidden" value="{{ company.x }}">
   *     <input name="company-latitude" type="hidden" value="{{ company.y }}">
   *

   *

   */
  $.fn.initMap = function () {

    if (!this.length) {
      return;  // Stop here when no container is found.
    }

    this.each(function (index) {
      var $mapContainer = $(this);
      var companyName = $mapContainer.find('input[name="company-name"]').val();
      var lat = $mapContainer.find('input[name="company-latitude"]').val();
      var lng = $mapContainer.find('input[name="company-longitude"]').val();
      var coords = [lng, lat];
      var map = createMap($mapContainer.find('.map')[0], coords, 13);
      map.on("load", function() {
        var popup = new mapboxgl.Popup()
          .setHTML(companyName);
        var marker = new mapboxgl.Marker()
          .setLngLat(coords)
          .setPopup(popup)
          .addTo(map);
      });
      $mapContainer.find('.map').click(function (e) {
        e.stopPropagation();
      });
    });
  };

  /*
   * .toggleDetails()
   *
   * Description:
   *   Show or hide the details of a search result.
   */
  $.fn.toggleDetails = function () {

    var maps = [];

    $(this).click(function (e) {

      e.preventDefault();

      var $resultContainer = $(this).closest('.lbb-result');

      // If the clicked element is already active, hide it.
      if ($resultContainer.hasClass('active')) {
        $resultContainer.removeClass('active');
        return;
      }

      // Otherwise, show it.
      $resultContainer.addClass('active');

      // Track the click.
      var eventLabel = [
        'position:', $resultContainer.find('input[name="position"]').val(),
        ',effectif:', $resultContainer.find('input[name="headcount"]').val(),
        ',distance:', $resultContainer.find('input[name="company_distance"]').val(),
      ].join('');
      ga('send', 'event', 'Fiche Entreprise', 'Deplier', eventLabel);

      // Init the map.
      var siret = $resultContainer.find('input[name="company-siret"]').val();
      if ($.inArray(siret, maps) === -1) {
        $resultContainer.find('.js-map-container').initMap();  // Here, we know that the container is visible.
        maps.push(siret);
      }

      // Hide any already opened company block.
      $('.lbb-result')
        .not($resultContainer)
        .removeClass('active');

      // Trigger event
      $.post('/events/toggle-details/' + siret, {'csrf_token': CSRF_TOKEN});
    });

  };

  function updateTravelDurations() {
    // Compute travel durations asynchronously for each company
    var companySirets = [];
    var companyCoordinates = [];
    var latitude = $("#lat").val();
    var longitude = $("#lon").val();
    var travelMode = $("#tr").val() || "car";
    $(".travel-duration").each(function(){
        companySirets.push($(this).attr("data-siret"));
        companyCoordinates.push($(this).attr("data-latitude") + "," + $(this).attr("data-longitude"));
    });
    if (!companySirets.length) {
        return;
    }
    // Load durations 5 by 5 to avoid timeouts from upstream servers
    var sampleSize = 5;
    for (var i=0; i < companyCoordinates.length; i += sampleSize) {
      loadTravelDurations(
        travelMode, latitude, longitude,
        companyCoordinates.slice(i, i+sampleSize),
        companySirets.slice(i, i+sampleSize)
      );
    }
  }

  function loadTravelDurations(travelMode, latitude, longitude, companyCoordinates, companySirets) {
    $.ajax({
        url: "/maps/durations",
        method: 'POST',
        data: JSON.stringify({
          origin: latitude + "," + longitude,
          destinations: companyCoordinates,
          travel_mode: travelMode,
        }),
        contentType: 'application/json',
        dataType: 'json',
        headers: {
          "X-CSRFToken": CSRF_TOKEN,
        }
    }).success(function(durations) {
        // Fill durations
        for(var i = 0; i < durations.length; i += 1) {
            if(durations[i]) {
                // Convert duration from seconds to minutes
                var duration = durations[i] / 60;
                if(Math.floor(duration) < duration) {
                  duration += 1;
                }
                duration = Math.floor(duration);

                // Fill html
                var modes = {
                  'car': "en voiture",
                  'public': "en transports en commun",
                }
                var html = ' ' + duration + ' min ' + modes[travelMode];
                $(".travel-duration[data-siret='" + companySirets[i] + "']").html(html);
            }
        }
    }).fail(function(e) {
      // In case of error, don't do anything
    });
  }

  $.fn.checkChanges = function () {
    var shown_form = this;
    var hidden_form = $('.js-search-form');
    var send_button = shown_form.find('button[type="submit"]');

    send_button.on('click', function (e) {
      e.preventDefault();
      e.stopPropagation();

      // Make sure new fields are displayed in the shown form.
      var location = shown_form.find('#l');
      var occupation = shown_form.find('#j');
      location.attr('value', location.prop('value'));
      occupation.attr('value', occupation.prop('value'));
      shown_form.submit();
    });

    shown_form.on('submit', function(e) {
      e.preventDefault();
      e.stopPropagation();
      var html_form = shown_form.html();
      hidden_form.find('#hidden-search-form').html(html_form);
      hidden_form.submit();
    });
  }

})(jQuery);<commit_msg>Revert "Disable travel durations just to be safe"<commit_after>"use strict";

/**
* Global function that tracks a click on an outbound link in Google Analytics.
* This function takes a valid URL string as an argument, and uses that URL string
* as the event label.
*/
var trackOutboundLink = function(url) {
  ga('send', 'event', 'outbound', 'click', url);
};

(function ($) {

  $(document).on('lbbready', function () {

    // Only init a map if its container is visible because Leaflet
    // has a hard time initializing maps in hidden elements.
    $('.js-map-container:visible').initMap();
    $('.js-result-toggle-details').toggleDetails();
    updateTravelDurations();
    $('#shown-search-form').checkChanges();

    var eventLabel;
    if ($('.ga-no-results').length) {
      var eventLabel = "job:" + $('input[name="job"]').val() + ";location:" + $('input[name="location"]').val();
      ga('send', 'event', 'Resultats', 'no results', eventLabel);
    }

    $('.ga-pdf-download-link').click(function (e) {
      ga('send', 'event', 'Download', 'fiche entreprise', 1);
    });

  });

  /*
   * .initMap()
   *
   * Description:
   *   Init a Leaflet map.
   *
   * Usage:
   *   $('map-container-selector').initMap();
   *
   * Expected markup:
   *   - The container must contain a div with a `.map` class that is the map placeholder.
   *   - The container must contain some hidden inputs that will be used for building the map:
   *       * company-latitude
   *       * company-longitude
   *       * company-name
   *
   * Expected markup example:
   *

   *     <input name="company-name" type="hidden" value="{{ company.name }}">
   *     <input name="company-longitude" type="hidden" value="{{ company.x }}">
   *     <input name="company-latitude" type="hidden" value="{{ company.y }}">
   *

   *

   */
  $.fn.initMap = function () {

    if (!this.length) {
      return;  // Stop here when no container is found.
    }

    this.each(function (index) {
      var $mapContainer = $(this);
      var companyName = $mapContainer.find('input[name="company-name"]').val();
      var lat = $mapContainer.find('input[name="company-latitude"]').val();
      var lng = $mapContainer.find('input[name="company-longitude"]').val();
      var coords = [lng, lat];
      var map = createMap($mapContainer.find('.map')[0], coords, 13);
      map.on("load", function() {
        var popup = new mapboxgl.Popup()
          .setHTML(companyName);
        var marker = new mapboxgl.Marker()
          .setLngLat(coords)
          .setPopup(popup)
          .addTo(map);
      });
      $mapContainer.find('.map').click(function (e) {
        e.stopPropagation();
      });
    });
  };

  /*
   * .toggleDetails()
   *
   * Description:
   *   Show or hide the details of a search result.
   */
  $.fn.toggleDetails = function () {

    var maps = [];

    $(this).click(function (e) {

      e.preventDefault();

      var $resultContainer = $(this).closest('.lbb-result');

      // If the clicked element is already active, hide it.
      if ($resultContainer.hasClass('active')) {
        $resultContainer.removeClass('active');
        return;
      }

      // Otherwise, show it.
      $resultContainer.addClass('active');

      // Track the click.
      var eventLabel = [
        'position:', $resultContainer.find('input[name="position"]').val(),
        ',effectif:', $resultContainer.find('input[name="headcount"]').val(),
        ',distance:', $resultContainer.find('input[name="company_distance"]').val(),
      ].join('');
      ga('send', 'event', 'Fiche Entreprise', 'Deplier', eventLabel);

      // Init the map.
      var siret = $resultContainer.find('input[name="company-siret"]').val();
      if ($.inArray(siret, maps) === -1) {
        $resultContainer.find('.js-map-container').initMap();  // Here, we know that the container is visible.
        maps.push(siret);
      }

      // Hide any already opened company block.
      $('.lbb-result')
        .not($resultContainer)
        .removeClass('active');

      // Trigger event
      $.post('/events/toggle-details/' + siret, {'csrf_token': CSRF_TOKEN});
    });

  };

  function updateTravelDurations() {
    // Compute travel durations asynchronously for each company
    var companySirets = [];
    var companyCoordinates = [];
    var latitude = $("#lat").val();
    var longitude = $("#lon").val();
    var travelMode = $("#tr").val() || "car";
    $(".travel-duration").each(function(){
        companySirets.push($(this).attr("data-siret"));
        companyCoordinates.push($(this).attr("data-latitude") + "," + $(this).attr("data-longitude"));
    });
    if (!companySirets.length) {
        return;
    }
    // Load durations 5 by 5 to avoid timeouts from upstream servers
    var sampleSize = 5;
    for (var i=0; i < companyCoordinates.length; i += sampleSize) {
      loadTravelDurations(
        travelMode, latitude, longitude,
        companyCoordinates.slice(i, i+sampleSize),
        companySirets.slice(i, i+sampleSize)
      );
    }
  }

  function loadTravelDurations(travelMode, latitude, longitude, companyCoordinates, companySirets) {
    $.ajax({
        url: "/maps/durations",
        method: 'POST',
        data: JSON.stringify({
          origin: latitude + "," + longitude,
          destinations: companyCoordinates,
          travel_mode: travelMode,
        }),
        contentType: 'application/json',
        dataType: 'json',
        headers: {
          "X-CSRFToken": CSRF_TOKEN,
        }
    }).success(function(durations) {
        // Fill durations
        for(var i = 0; i < durations.length; i += 1) {
            if(durations[i]) {
                // Convert duration from seconds to minutes
                var duration = durations[i] / 60;
                if(Math.floor(duration) < duration) {
                  duration += 1;
                }
                duration = Math.floor(duration);

                // Fill html
                var modes = {
                  'car': "en voiture",
                  'public': "en transports en commun",
                }
                var html = ' ' + duration + ' min ' + modes[travelMode];
                $(".travel-duration[data-siret='" + companySirets[i] + "']").html(html);
            }
        }
    }).fail(function(e) {
      // In case of error, don't do anything
    });
  }

  $.fn.checkChanges = function () {
    var shown_form = this;
    var hidden_form = $('.js-search-form');
    var send_button = shown_form.find('button[type="submit"]');

    send_button.on('click', function (e) {
      e.preventDefault();
      e.stopPropagation();

      // Make sure new fields are displayed in the shown form.
      var location = shown_form.find('#l');
      var occupation = shown_form.find('#j');
      location.attr('value', location.prop('value'));
      occupation.attr('value', occupation.prop('value'));
      shown_form.submit();
    });

    shown_form.on('submit', function(e) {
      e.preventDefault();
      e.stopPropagation();
      var html_form = shown_form.html();
      hidden_form.find('#hidden-search-form').html(html_form);
      hidden_form.submit();
    });
  }

})(jQuery);<|endoftext|>


<commit_before>/********************************
* EZ-TC Plugin by jd            *
* Makes adding trainer cards EZ *
********************************/

var fs = require('fs');
var serialize = require('node-serialize');
var trainerCards = {};

function loadTrainerCards() {
	try {
		trainerCards = serialize.unserialize(fs.readFileSync('config/trainercards.json', 'utf8'));
		Object.merge(CommandParser.commands, trainerCards);
	} catch (e) {};
}
setTimeout(function(){loadTrainerCards();},1000);

function saveTrainerCards() {
	fs.writeFileSync('config/trainercards.json', serialize.serialize(trainerCards));
	Object.merge(CommandParser.commands, trainerCards);
}

exports.commands = {
	eztc: 'trainercard',
	trainercards: 'trainercard',
	tc: 'trainercard',
	trainercard: function (target, room, user) {
		if (!target) target = 'help';
		var parts = target.split(',');
		for (var u in parts) parts[u] = parts[u].trim();

		switch (parts[0]) {
			case 'add':
				if (!this.can('pban')) return false;
				if (!parts[2]) return this.sendReply("Usage: /trainercard add, [command name], [html]");
				var commandName = toId(parts[1]);
				if (CommandParser.commands[commandName]) return this.sendReply("/trainercards - The command \"" + commandName + "\" already exists.");
				try {
					var html = parts.splice(2, parts.length).join(',');
					trainerCards[commandName] = new Function('target', 'room', 'user', "if (!room.disableTrainerCards) if (!this.canBroadcast()) return; this.sendReplyBox('" + html.replace(/'/g, "\\'") + "');");
					saveTrainerCards();
					this.sendReply("The trainer card \"" + commandName + "\" has been added.");
					this.logModCommand(user.name + " added the trainer card " + commandName);
					Rooms.get('staff').add(user.name + " added the trainer card " + commandName);
				} catch (e) {
					this.errorReply("Something went wrong when trying to add this command.  Did you use a backwards slash mark?  If so, try it again without using this.");
				}
				break;

			case 'rem':
			case 'del':
			case 'delete':
			case 'remove':
				if (!this.can('pban')) return false;
				if (!parts[1]) return this.sendReply("Usage: /trainercard remove, [command name]");
				var commandName = toId(parts[1]);
				if (!trainerCards[commandName]) return this.sendReply("/trainercards - The command \"" + commandName + "\" does not exist, or was added manually.");
				delete CommandParser.commands[commandName];
				delete trainerCards[commandName];
				saveTrainerCards();
				this.sendReply("The trainer card \"" + commandName + "\" has been removed.");
				this.logModCommand(user.name + " removed the trainer card " + commandName);
				try {
					Rooms.rooms.staff.add(user.name + " removed the trainer card " + commandName);
				} catch (e) {};
				break;

			case 'list':
				if (!this.can('trainercard')) return false;
				var output = "There's a total of " + Object.size(trainerCards) + " trainer cards added with this command:
";
				for (var tc in trainerCards) {
					output += tc + "
";
				}
				this.sendReplyBox(output);
				break;

			case 'off':
				if (!this.can('roommod', null, room)) return false;
				if (room.disableTrainerCards) return this.sendReply("Broadcasting trainer cards is already disabled in this room.");
				room.disableTrainerCards = true;
				room.chatRoomData.disableTrainerCards = true;
				Rooms.global.writeChatRoomData();
				this.privateModCommand("(" + user.name + " has disabled broadcasting trainer cards in this room.)");
				break;

			case 'on':
				if (!this.can('roommod', null, room)) return false;
				if (!room.disableTrainerCards) return this.sendReply("Broadcasing trainer cards is already enabled in this room.");
				delete room.disableTrainerCards;
				delete room.chatRoomData.disableTrainerCards;
				Rooms.global.writeChatRoomData();
				this.privateModCommand("(" + user.name + " has enabled broadcasting trainer cards in this room.)");
				break;

			case 'reload':
				if (!this.can('pban')) return false;
				return this.sendReply("Trainer cards have been reloaded.");
				loadTrainerCards();
				break;

			case 'source':
				if (!this.can('pban')) return false;
				if (!parts[1]) return this.errorReply("Usage: /tc source, [commands] - displays the source code of a trainer card.");
				if (!CommandParser.commands[parts[1]]) return this.errorReply("Command not found.  Check spelling?");
				return this.sendReply(CommandParser.commands[parts[1]]);
				break;

			case 'info':
			case 'help':
			default:
				if (!this.canBroadcast()) return;
				this.sendReplyBox(
					"EZ-TC Commands:
" +
					"/trainercard add, [command name], [html] - Adds a trainer card.
" +
					"/trainercard remove, [command name] - Removes a trainer card.
" +
					"/trainercard list - Shows a list of all trainer cards added with this command.
" +
					"/trainercard off - Disables broadcasting trainer cards in the current room.
" +
					"/trainercard on - Enables broadcasting trainer cards in the current room.
" +
					"/trainercard reload - Reloads trainer cards if they break (shouldn't happen).
" +
					"/trainercard source, [command] - Displays the source code for a trainer card.
" +
					"/trainercard help - Shows this help command.
" +
					"EZ-TC Plugin by jd"
				);
		}
	}
};<commit_msg>Add a message for when loading TCs fails<commit_after>/********************************
* EZ-TC Plugin by jd            *
* Makes adding trainer cards EZ *
********************************/

var fs = require('fs');
var serialize = require('node-serialize');
var trainerCards = {};

function loadTrainerCards() {
	try {
		trainerCards = serialize.unserialize(fs.readFileSync('config/trainercards.json', 'utf8'));
		Object.merge(CommandParser.commands, trainerCards);
	} catch (e) {
		Rooms('staff').add("TRAINER CARDS failed to be loaded.").update();
	}
}
setTimeout(function(){loadTrainerCards();},1000);

function saveTrainerCards() {
	fs.writeFileSync('config/trainercards.json', serialize.serialize(trainerCards));
	Object.merge(CommandParser.commands, trainerCards);
}

exports.commands = {
	eztc: 'trainercard',
	trainercards: 'trainercard',
	tc: 'trainercard',
	trainercard: function (target, room, user) {
		if (!target) target = 'help';
		var parts = target.split(',');
		for (var u in parts) parts[u] = parts[u].trim();

		switch (parts[0]) {
			case 'add':
				if (!this.can('pban')) return false;
				if (!parts[2]) return this.sendReply("Usage: /trainercard add, [command name], [html]");
				var commandName = toId(parts[1]);
				if (CommandParser.commands[commandName]) return this.sendReply("/trainercards - The command \"" + commandName + "\" already exists.");
				try {
					var html = parts.splice(2, parts.length).join(',');
					trainerCards[commandName] = new Function('target', 'room', 'user', "if (!room.disableTrainerCards) if (!this.canBroadcast()) return; this.sendReplyBox('" + html.replace(/'/g, "\\'") + "');");
					saveTrainerCards();
					this.sendReply("The trainer card \"" + commandName + "\" has been added.");
					this.logModCommand(user.name + " added the trainer card " + commandName);
					Rooms.get('staff').add(user.name + " added the trainer card " + commandName);
				} catch (e) {
					this.errorReply("Something went wrong when trying to add this command.  Did you use a backwards slash mark?  If so, try it again without using this.");
				}
				break;

			case 'rem':
			case 'del':
			case 'delete':
			case 'remove':
				if (!this.can('pban')) return false;
				if (!parts[1]) return this.sendReply("Usage: /trainercard remove, [command name]");
				var commandName = toId(parts[1]);
				if (!trainerCards[commandName]) return this.sendReply("/trainercards - The command \"" + commandName + "\" does not exist, or was added manually.");
				delete CommandParser.commands[commandName];
				delete trainerCards[commandName];
				saveTrainerCards();
				this.sendReply("The trainer card \"" + commandName + "\" has been removed.");
				this.logModCommand(user.name + " removed the trainer card " + commandName);
				try {
					Rooms.rooms.staff.add(user.name + " removed the trainer card " + commandName);
				} catch (e) {};
				break;

			case 'list':
				if (!this.can('trainercard')) return false;
				var output = "There's a total of " + Object.size(trainerCards) + " trainer cards added with this command:
";
				for (var tc in trainerCards) {
					output += tc + "
";
				}
				this.sendReplyBox(output);
				break;

			case 'off':
				if (!this.can('roommod', null, room)) return false;
				if (room.disableTrainerCards) return this.sendReply("Broadcasting trainer cards is already disabled in this room.");
				room.disableTrainerCards = true;
				room.chatRoomData.disableTrainerCards = true;
				Rooms.global.writeChatRoomData();
				this.privateModCommand("(" + user.name + " has disabled broadcasting trainer cards in this room.)");
				break;

			case 'on':
				if (!this.can('roommod', null, room)) return false;
				if (!room.disableTrainerCards) return this.sendReply("Broadcasing trainer cards is already enabled in this room.");
				delete room.disableTrainerCards;
				delete room.chatRoomData.disableTrainerCards;
				Rooms.global.writeChatRoomData();
				this.privateModCommand("(" + user.name + " has enabled broadcasting trainer cards in this room.)");
				break;

			case 'reload':
				if (!this.can('pban')) return false;
				return this.sendReply("Trainer cards have been reloaded.");
				loadTrainerCards();
				break;

			case 'source':
				if (!this.can('pban')) return false;
				if (!parts[1]) return this.errorReply("Usage: /tc source, [commands] - displays the source code of a trainer card.");
				if (!CommandParser.commands[parts[1]]) return this.errorReply("Command not found.  Check spelling?");
				return this.sendReply(CommandParser.commands[parts[1]]);
				break;

			case 'info':
			case 'help':
			default:
				if (!this.canBroadcast()) return;
				this.sendReplyBox(
					"EZ-TC Commands:
" +
					"/trainercard add, [command name], [html] - Adds a trainer card.
" +
					"/trainercard remove, [command name] - Removes a trainer card.
" +
					"/trainercard list - Shows a list of all trainer cards added with this command.
" +
					"/trainercard off - Disables broadcasting trainer cards in the current room.
" +
					"/trainercard on - Enables broadcasting trainer cards in the current room.
" +
					"/trainercard reload - Reloads trainer cards if they break (shouldn't happen).
" +
					"/trainercard source, [command] - Displays the source code for a trainer card.
" +
					"/trainercard help - Shows this help command.
" +
					"EZ-TC Plugin by jd"
				);
		}
	}
};<|endoftext|>


<commit_before>/****************************
 * Offline Messaging System *
 *       by: panpawn        *
 ***************************/
"use strict";

const MAX_TELLS_IN_QUEUE = 10;
const MAX_TELL_LENGTH = 600;

exports.commands = {
	tell: function (target, room, user) {
		if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
		if (!target) return this.parse('/help tell');
		let commaIndex = target.indexOf(',');
		if (commaIndex < 0) return this.errorReply("You forgot the comma.");
		let targetUser = toId(target.slice(0, commaIndex)), origUser = target.slice(0, commaIndex);
		let message = target.slice(commaIndex + 1).trim();
		if (Users(targetUser) && Users(targetUser).ignorePMs && !user.can('hotpatch')) return this.errorReply("Because this user is currently blocking PMs, this tell has failed to be added to their offline messaging queue.");
		if (message.length > MAX_TELL_LENGTH && !user.can('hotpatch')) return this.errorReply("This tell is too large, it cannot exceed " + MAX_TELL_LENGTH + " characters.");
		if (targetUser.length < 1 || targetUser.length > 18) return this.errorReply("Usernames cannot be this length.  Check spelling?");
		if (!message || message.length < 1) return this.errorReply("Tell messages must be at least one character.");
		if (Gold.userData[targetUser] && Gold.userData[targetUser].tells && Gold.userData[targetUser].tells.length >= MAX_TELLS_IN_QUEUE && !user.can('hotpatch')) return this.errorReply("This user has too many tells queued, try again later.");
		Gold.createTell(user.name, targetUser, message); // function saves when tell is created automatically
		//createTell(user.name, targetUser, message); // function saves when tell is created automatically
		return this.sendReply("|raw|Your tell to " + Gold.nameColor(origUser, true) + " has been added to their offline messaging queue." + (Users(targetUser) && Users(targetUser).connected && user.userid !== targetUser ? "
However, this user is currently online if you would like to private message them." : ""));
	},
	tellhelp: ["/tell [user], [message] - sends a user an offline message to be recieved when they next log on."],

	mailbox: function (target, room, user) {
		if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");

		if (!target) {
			let data = Object.keys(Gold.userData), buff = Object.create(null);
			let keys, results = Object.create(null), userIdRegEx = new RegExp(user.userid + "#.*", "g");
			let tableTop = 'Current pending tells queued:

';
			tableTop += '
Tell ID:	Tell to:	Message:
';
			let midTable = '', displayedIds = [];

			data.forEach(name => {
				if (!Gold.userData[name].tells || Gold.userData[name].tells.length > 0) return;
				keys = Object.keys(Gold.userData[name].tells);
				keys.find(arr => {
					if (arr.match(userIdRegEx)) {
						results[arr.match(userIdRegEx)] = name;
					}
				});
				if (Object.keys(results).length > 0) Object.keys(results).forEach(tellid => {
					if (!Gold.userData[results[tellid]].tells[tellid]) return;
					if (displayedIds.includes(tellid)) return;
					midTable += '
' + tellid + '	' + Gold.nameColor(name, true) + '	' + Gold.userData[name].tells[tellid] + '	<button name="send" value="/mailbox ' + name + ',' + tellid + '">Delete Pending Message</button>
';
					displayedIds.push(tellid);
				});
			});
			if (!midTable) return this.errorReply("You do not currently have any tells pending to be sent at this time.");
			user.send('|popup||wide||html|' + tableTop + midTable + '
');
		} else {
			target = target.split(',');
			for (let u in target) target[u] = target[u].trim();
			if (!target[1]) return false;
			if (!Gold.userData[target[0]]) return false;
			if (!Gold.userData[target[0]].tells[target[1]]) return false;
			delete Gold.userData[target[0]].tells[target[1]];
			Gold.saveData();
			user.send('|popup||wide||html| You have deleted the pending tell to ' + Gold.nameColor(target[0], true) + ' with tell ID ' + target[1] + '.

<button name="send" value="/mailbox">Back to mailbox</button>');
		}
	},
};
<commit_msg>Change an errorReply to popup in /mailbox<commit_after>/****************************
 * Offline Messaging System *
 *       by: panpawn        *
 ***************************/
"use strict";

const MAX_TELLS_IN_QUEUE = 10;
const MAX_TELL_LENGTH = 600;

exports.commands = {
	tell: function (target, room, user) {
		if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
		if (!target) return this.parse('/help tell');
		let commaIndex = target.indexOf(',');
		if (commaIndex < 0) return this.errorReply("You forgot the comma.");
		let targetUser = toId(target.slice(0, commaIndex)), origUser = target.slice(0, commaIndex);
		let message = target.slice(commaIndex + 1).trim();
		if (Users(targetUser) && Users(targetUser).ignorePMs && !user.can('hotpatch')) return this.errorReply("Because this user is currently blocking PMs, this tell has failed to be added to their offline messaging queue.");
		if (message.length > MAX_TELL_LENGTH && !user.can('hotpatch')) return this.errorReply("This tell is too large, it cannot exceed " + MAX_TELL_LENGTH + " characters.");
		if (targetUser.length < 1 || targetUser.length > 18) return this.errorReply("Usernames cannot be this length.  Check spelling?");
		if (!message || message.length < 1) return this.errorReply("Tell messages must be at least one character.");
		if (Gold.userData[targetUser] && Gold.userData[targetUser].tells && Gold.userData[targetUser].tells.length >= MAX_TELLS_IN_QUEUE && !user.can('hotpatch')) return this.errorReply("This user has too many tells queued, try again later.");
		Gold.createTell(user.name, targetUser, message); // function saves when tell is created automatically
		//createTell(user.name, targetUser, message); // function saves when tell is created automatically
		return this.sendReply("|raw|Your tell to " + Gold.nameColor(origUser, true) + " has been added to their offline messaging queue." + (Users(targetUser) && Users(targetUser).connected && user.userid !== targetUser ? "
However, this user is currently online if you would like to private message them." : ""));
	},
	tellhelp: ["/tell [user], [message] - sends a user an offline message to be recieved when they next log on."],

	mailbox: function (target, room, user) {
		if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");

		if (!target) {
			let data = Object.keys(Gold.userData), buff = Object.create(null);
			let keys, results = Object.create(null), userIdRegEx = new RegExp(user.userid + "#.*", "g");
			let tableTop = 'Current pending tells queued:

';
			tableTop += '
Tell ID:	Tell to:	Message:
';
			let midTable = '', displayedIds = [];

			data.forEach(name => {
				if (!Gold.userData[name].tells || Gold.userData[name].tells.length > 0) return;
				keys = Object.keys(Gold.userData[name].tells);
				keys.find(arr => {
					if (arr.match(userIdRegEx)) {
						results[arr.match(userIdRegEx)] = name;
					}
				});
				if (Object.keys(results).length > 0) Object.keys(results).forEach(tellid => {
					if (!Gold.userData[results[tellid]].tells[tellid]) return;
					if (displayedIds.includes(tellid)) return;
					midTable += '
' + tellid + '	' + Gold.nameColor(name, true) + '	' + Gold.userData[name].tells[tellid] + '	<button name="send" value="/mailbox ' + name + ',' + tellid + '">Delete Pending Message</button>
';
					displayedIds.push(tellid);
				});
			});
			if (!midTable) return user.send('|popup||wide||html|You do not currently have any tells pending to be sent at this time.');
			user.send('|popup||wide||html|' + tableTop + midTable + '
');
		} else {
			target = target.split(',');
			for (let u in target) target[u] = target[u].trim();
			if (!target[1]) return false;
			if (!Gold.userData[target[0]]) return false;
			if (!Gold.userData[target[0]].tells[target[1]]) return false;
			delete Gold.userData[target[0]].tells[target[1]];
			Gold.saveData();
			user.send('|popup||wide||html| You have deleted the pending tell to ' + Gold.nameColor(target[0], true) + ' with tell ID ' + target[1] + '.

<button name="send" value="/mailbox">Back to mailbox</button>');
		}
	},
};
<|endoftext|>


<commit_before>// Load in our dependencies
var $ = window.$ = window.jQuery = require('jquery');
var assert = require('assert');
var _ = require('underscore');
var h = require('hyperscript-helpers')(require('hyperscript'));
var classnames = require('classnames');
var BaseComponent = require('./base-component');
var GlobalState = require('./global-state');
var Overlay = require('./overlay');
var SimilarImageResults = require('./similar-image-results');
var utils = require('./utils');

// Define our constants
var IN_PROGRESS = 'IN_PROGRESS';
var NONE = 'NONE';

// Define our constructor
var ImageSet = BaseComponent.extend({
  initialize: function (options) {
    // Save parameters for later
    var imageSetInfo = this.imageSetInfo = options.imageSetInfo; assert(imageSetInfo);

    // Create one-off quick-reference variables
    this.id = imageSetInfo.id;
    this.humanName = imageSetInfo.id;

    // Save our state
    this.setState({
      eagerStatus: null,
      findSimilarImages: false,
      imagesEqual: imageSetInfo.imagesEqual,
      isNew: imageSetInfo.isNew,
      xhrState: NONE,
      targetArea: null
    });

    // Run our render method
    // TODO: Add destroy method so we can unreference element
    this.render();

    // Register our image set to the global state
    GlobalState.addImageSet(this);
  }
});

// Define class level helpers
ImageSet.bindOnce = function () {
  // Set up image acceptance binding
  $('body').on('click', 'button[data-action="accept-changes"]', function handleClick (evt) {
    // Resolve our image set
    var imageSet = ImageSet.fetchByEvent(evt);

    // Extract and accept base64 content for image
    var base64Data = utils.getBase64Content(imageSet.currentImg);
    imageSet.acceptChanges(base64Data);
  });

  $('body').on('click', 'button[data-action="find-similar-images"]', function handleClick (evt) {
    // Find our image set's similar images
    var imageSet = ImageSet.fetchByEvent(evt);
    imageSet.findSimilarImages();
  });
};
ImageSet.cachebustImg = function (imgEl) {
  var originalSrc = imgEl.getAttribute('src');
  var newSrc = originalSrc.match(/\?1$/) ? originalSrc + '1' : originalSrc + '?1';
  imgEl.setAttribute('src', newSrc);
};
ImageSet.fetchByEvent = function (evt) {
  // Resolve our id
  var targetEl = evt.target;
  var $imageSet = $(targetEl).closest('[data-image-set]');
  var imageSetId = $imageSet.data('image-set');

  // Return our image set
  return GlobalState.fetchImageSetById(imageSetId);
};

// Define prototype methods
ImageSet.prototype = _.extend(ImageSet.prototype, {
  render: function () {
    // Create local variables for our image set
    var imageSetId = this.id;
    var imageSetHumanName = this.humanName;
    this.renderedAlready = true;
    this.el.setAttribute('data-image-set', imageSetId);

    // Row title
    // TODO: Move from link to button styled as link so we can ditch `href`
    this.el.appendChild(this.saveEl('titleEl', h.a({
      className: 'image-set__title',
      href: 'javascript:void 0;', 'data-toggle': 'collapse',
      'data-target': '[data-image-set="' + imageSetId + '"] .image-set__collapse',
      'data-images-equal': this.getState('imagesEqual'),
      attrs: {'aria-controls': imageSetId}
    }, imageSetHumanName)));

    // Collapsable container for row
    // DEV: We use `data-id` as `id` has restrictions on characters
    this.el.appendChild(this.saveEl('contentsEl', h.div({
      // Make our first image set visible
      className: classnames('image-set__collapse', 'collapse', 'well', {in: !this.getState('imagesEqual')})
    }, [
      // Action buttons
      h.div([
        h.button({
          className: 'btn btn-default',
          'data-action': 'accept-changes'
        }, '✓ Accept changes'),
        ' ',
        h.button({
          className: 'btn btn-default',
          'data-action': 'find-similar-images'
          // TODO: Make our button disabled and only enable when an overlay is drawn
          // TODO: When changes are accepted, reset all affected overlays and buttons
          // disabled: 'disabled'
        }, 'Find similar images with selection')
      ]),

      // Image set
      h.table({className: 'table'}, [
        h.thead([
          h.tr([
            h.th(null, 'Current:'),
            h.th(null, 'Diff:'),
            h.th(null, 'Ref:')
          ])
        ]),
        h.tbody([
          h.tr([
            // DEV: We use `width: 33%` to guarantee no widths change when images are loading
            h.td({style: 'width: 33%'}, [
              this.saveEl('currentImg', h.img({
                'data-compare-type': 'current',
                src: this.imageSetInfo.currentImageUrl,
                style: 'max-width: 100%'
              }))
            ]),
            h.td({style: 'width: 33%'}, [
              !this.getState('isNew') ? this.saveEl('diffImg', h.img({
                'data-compare-type': 'diff',
                src: this.imageSetInfo.diffImageUrl,
                style: 'max-width: 100%'
              })) : h.em('No diff image yet, image set is new. Please accept changes first')
            ]),
            h.td({style: 'width: 33%'}, [
              !this.getState('isNew') ? this.saveEl('refImg', h.img({
                'data-compare-type': 'ref',
                src: this.imageSetInfo.refImageUrl,
                style: 'max-width: 100%'
              })) : h.em('No ref image yet, image set is new. Please accept changes first')
            ])
          ])
        ])
      ])
    ])));

    // Bind an overlay to diff image
    // TODO: Explore binding overlay to each of images (that jumps between them)
    // DEV: We use our collapse as a container so it hides on collapse
    var imgOverlay = new Overlay(this.diffImg, {
      containerEl: this.contentsEl
    });
    this.imgOverlay = imgOverlay;

    // When our XHR state changes (e.g. updating, completed updating)
    this.onStateChange('xhrState', function handleXHRChange (prevXhrState, xhrState) {
      // If our image set is updating
      if (xhrState === IN_PROGRESS) {
        // Fade out diff and reference images to "loading" state
        this.diffImg.classList.add('loading');
        this.refImg.classList.add('loading');

        // Update our status to eager status
        this.titleEl.setAttribute('data-images-equal', this.getState('eagerStatus'));
      // Otherwise (update completed)
      } else {
        // Remove loading classes
        this.diffImg.classList.remove('loading');
        this.refImg.classList.remove('loading');

        // Use actual status as status
        this.titleEl.setAttribute('data-images-equal', this.getState('imagesEqual'));

        // Cachebust our images
        ImageSet.cachebustImg(this.diffImg);
        ImageSet.cachebustImg(this.refImg);
      }
    });

    // When we toggle finding similar images/not
    this.onStateChange('findSimilarImages', function handleStateChange (prevVal, newVal) {
      // Remove previously existing content
      // TODO: Relocate removal of results el outside of `this.state` check -- it should be always
      if (this._resultsEl) {
        this.contentsEl.removeChild(this._resultsEl);
        delete this._resultsEl;
      }

      // If we are generating results, then populate them
      // DEV: We could generate similar image sets separately but this is to keep performance issues contained
      if (newVal !== false) {
        var resultsEl = this._resultsEl = h.div({className: 'results'});
        this.contentsEl.appendChild(resultsEl);
        void new SimilarImageResults({
          el: resultsEl,
          targetArea: this.getState('targetArea'),
          expectedImageSet: this
        });
      }
    });
  },
  acceptChanges: function (imgBase64) {
    this._updateReferenceImage(imgBase64, 'true');
  },
  findSimilarImages: function () {
    // Localize our expected diff img
    var expectedDiffImg = this.diffImg;

    // Find and adjust our target area based on scaling
    // TODO: Be sure to test target area scaling for both matching and updated diffs
    // TODO: Move `targetArea` to `getTargetArea` method
    var targetArea = this.imgOverlay.overlayInfo.relative;
    var scaleRatio = expectedDiffImg.naturalWidth / expectedDiffImg.width;
    targetArea = {
      width: Math.min(Math.ceil(scaleRatio * targetArea.width), expectedDiffImg.naturalWidth),
      height: Math.min(Math.ceil(scaleRatio * targetArea.height), expectedDiffImg.naturalHeight),
      left: Math.max(Math.floor(scaleRatio * targetArea.left), 0),
      top: Math.max(Math.floor(scaleRatio * targetArea.top), 0)
    };

    // Save our image set info and trigger results rendering
    // TODO: Unset `targetArea` values on destroy/results removal
    // DEV: We use `Date.now()` to guarantee a different value on multiple presses
    this.setState({
      targetArea: targetArea,
      findSimilarImages: Date.now()
    });
  },
  _updateReferenceImage: function (imgBase64, eagerStatus) {
    // Set our state as loading and an eager status
    // DEV: For an acceptance request, we will eagerly update to 'imagesEqual: true'
    this.setState({
      eagerStatus: eagerStatus || 'loading',
      xhrState: IN_PROGRESS
    });

    // Make an AJAX call to accept our image
    // http://api.jquery.com/jQuery.ajax/#jqXHR
    var jqXHR = $.post('/update-image-set/' + encodeURIComponent(this.id), {
      ref: imgBase64
    });

    // If there is an error
    var that = this;
    jqXHR.fail(function handleFail (jqXHR, textStatus, errorThrown) {
      // TODO: Expose error to user so they can retry
      console.error('Error encountered "' + errorThrown + '" when updating image "' + that.id + '"');

      // Remove loading state (triggers re-render)
      that.setState({
        eagerStatus: null,
        xhrState: NONE
      });
    });

    // When we complete updating
    jqXHR.done(function handleDone (data, textStatus, jqXHR) {
      // Save our new state (triggers re-render)
      // data = {imagesEqual: true}
      that.setState({
        eagerStatus: null,
        imagesEqual: data.imagesEqual,
        isNew: false,
        xhrState: NONE
      });
    });
  },
  updateReferenceImage: function (imgBase64) {
    this._updateReferenceImage(imgBase64);
  }
});

// Export our constructor
module.exports = ImageSet;
<commit_msg>Added saveSection concept<commit_after>// Load in our dependencies
var $ = window.$ = window.jQuery = require('jquery');
var assert = require('assert');
var _ = require('underscore');
var h = require('hyperscript-helpers')(require('hyperscript'));
var classnames = require('classnames');
var BaseComponent = require('./base-component');
var GlobalState = require('./global-state');
var Overlay = require('./overlay');
var SimilarImageResults = require('./similar-image-results');
var utils = require('./utils');

// Define our constants
var IN_PROGRESS = 'IN_PROGRESS';
var NONE = 'NONE';

// Define our constructor
var ImageSet = BaseComponent.extend({
  initialize: function (options) {
    // Save parameters for later
    var imageSetInfo = this.imageSetInfo = options.imageSetInfo; assert(imageSetInfo);

    // Create one-off quick-reference variables
    this.id = imageSetInfo.id;
    this.humanName = imageSetInfo.id;

    // Save our state
    this.setState({
      eagerStatus: null,
      findSimilarImages: false,
      imagesEqual: imageSetInfo.imagesEqual,
      isNew: imageSetInfo.isNew,
      xhrState: NONE,
      targetArea: null
    });

    // Run our render method
    // TODO: Add destroy method so we can unreference element
    this.render();

    // Register our image set to the global state
    GlobalState.addImageSet(this);
  }
});

// Define class level helpers
ImageSet.bindOnce = function () {
  // Set up image acceptance binding
  $('body').on('click', 'button[data-action="accept-changes"]', function handleClick (evt) {
    // Resolve our image set
    var imageSet = ImageSet.fetchByEvent(evt);

    // Extract and accept base64 content for image
    var base64Data = utils.getBase64Content(imageSet.currentImg);
    imageSet.acceptChanges(base64Data);
  });

  $('body').on('click', 'button[data-action="find-similar-images"]', function handleClick (evt) {
    // Find our image set's similar images
    var imageSet = ImageSet.fetchByEvent(evt);
    imageSet.findSimilarImages();
  });
};
ImageSet.cachebustImg = function (imgEl) {
  var originalSrc = imgEl.getAttribute('src');
  var newSrc = originalSrc.match(/\?1$/) ? originalSrc + '1' : originalSrc + '?1';
  imgEl.setAttribute('src', newSrc);
};
ImageSet.fetchByEvent = function (evt) {
  // Resolve our id
  var targetEl = evt.target;
  var $imageSet = $(targetEl).closest('[data-image-set]');
  var imageSetId = $imageSet.data('image-set');

  // Return our image set
  return GlobalState.fetchImageSetById(imageSetId);
};

// Define prototype methods
ImageSet.prototype = _.extend(ImageSet.prototype, {
  render: function () {
    // Create local variables for our image set
    var imageSetId = this.id;
    var imageSetHumanName = this.humanName;
    this.renderedAlready = true;
    this.el.setAttribute('data-image-set', imageSetId);

    // Row title
    // TODO: Move from link to button styled as link so we can ditch `href`
    this.el.appendChild(this.saveEl('titleEl', h.a({
      className: 'image-set__title',
      href: 'javascript:void 0;', 'data-toggle': 'collapse',
      'data-target': '[data-image-set="' + imageSetId + '"] .image-set__collapse',
      'data-images-equal': this.getState('imagesEqual'),
      attrs: {'aria-controls': imageSetId}
    }, imageSetHumanName)));

    // Collapsable container for row
    // DEV: We use `data-id` as `id` has restrictions on characters
    this.el.appendChild(this.saveEl('contentsEl', h.div({
      // Make our first image set visible
      className: classnames('image-set__collapse', 'collapse', 'well', {in: !this.getState('imagesEqual')})
    }, [
      // Action buttons
      h.div([
        h.button({
          className: 'btn btn-default',
          'data-action': 'accept-changes'
        }, '✓ Accept changes'),
        ' ',
        h.button({
          className: 'btn btn-default',
          'data-action': 'find-similar-images'
          // TODO: Make our button disabled and only enable when an overlay is drawn
          // TODO: When changes are accepted, reset all affected overlays and buttons
          // disabled: 'disabled'
        }, 'Find similar images with selection')
      ]),

      // Image set
      h.table({className: 'table'}, [
        h.thead([
          h.tr([
            h.th(null, 'Current:'),
            h.th(null, 'Diff:'),
            h.th(null, 'Ref:')
          ])
        ]),
        h.tbody([
          h.tr([
            // DEV: We use `width: 33%` to guarantee no widths change when images are loading
            h.td({style: 'width: 33%'}, [
              this.saveEl('currentImg', h.img({
                'data-compare-type': 'current',
                src: this.imageSetInfo.currentImageUrl,
                style: 'max-width: 100%'
              }))
            ]),
            h.td({style: 'width: 33%'}, [
              this.saveSection('diffImg', function () {
                if (this.getState('isNew')) {
                  return h.em('No diff image yet, image set is new. Please accept changes first');
                } else {
                  // Generate our image
                  var retVal = this.saveEl('diffImg', h.img({
                    'data-compare-type': 'diff',
                    src: this.imageSetInfo.diffImageUrl,
                    style: 'max-width: 100%'
                  }));

                  // Bind an overlay to diff image
                  // TODO: Explore binding overlay to each of images (that jumps between them)
                  // DEV: We use our collapse as a container so it hides on collapse
                  this.imgOverlay = new Overlay(this.diffImg, {
                    containerEl: this.contentsEl
                  });

                  // Return our retVal
                  return retVal;
                }
              })
            ]),
            h.td({style: 'width: 33%'}, [
              this.saveSection('refImg', function () {
                if (this.getState('isNew')) {
                  return h.em('No ref image yet, image set is new. Please accept changes first');
                } else {
                  return this.saveEl('refImg', h.img({
                    'data-compare-type': 'ref',
                    src: this.imageSetInfo.refImageUrl,
                    style: 'max-width: 100%'
                  }));
                }
              })
            ])
          ])
        ])
      ])
    ])));

    // When our XHR state changes (e.g. updating, completed updating)
    this.onStateChange('xhrState', function handleXHRChange (prevXhrState, xhrState) {
      // If our image set is updating
      if (xhrState === IN_PROGRESS) {
        // Fade out diff and reference images to "loading" state
        this.diffImg.classList.add('loading');
        this.refImg.classList.add('loading');

        // Update our status to eager status
        this.titleEl.setAttribute('data-images-equal', this.getState('eagerStatus'));
      // Otherwise (update completed)
      } else {
        // Remove loading classes
        this.diffImg.classList.remove('loading');
        this.refImg.classList.remove('loading');

        // Use actual status as status
        this.titleEl.setAttribute('data-images-equal', this.getState('imagesEqual'));

        // Cachebust our images
        ImageSet.cachebustImg(this.diffImg);
        ImageSet.cachebustImg(this.refImg);
      }
    });

    // When we toggle finding similar images/not
    this.onStateChange('findSimilarImages', function handleStateChange (prevVal, newVal) {
      // Remove previously existing content
      // TODO: Relocate removal of results el outside of `this.state` check -- it should be always
      if (this._resultsEl) {
        this.contentsEl.removeChild(this._resultsEl);
        delete this._resultsEl;
      }

      // If we are generating results, then populate them
      // DEV: We could generate similar image sets separately but this is to keep performance issues contained
      if (newVal !== false) {
        var resultsEl = this._resultsEl = h.div({className: 'results'});
        this.contentsEl.appendChild(resultsEl);
        void new SimilarImageResults({
          el: resultsEl,
          targetArea: this.getState('targetArea'),
          expectedImageSet: this
        });
      }
    });
  },
  acceptChanges: function (imgBase64) {
    this._updateReferenceImage(imgBase64, 'true');
  },
  findSimilarImages: function () {
    // Localize our expected diff img
    var expectedDiffImg = this.diffImg;

    // Find and adjust our target area based on scaling
    // TODO: Be sure to test target area scaling for both matching and updated diffs
    // TODO: Move `targetArea` to `getTargetArea` method
    var targetArea = this.imgOverlay.overlayInfo.relative;
    var scaleRatio = expectedDiffImg.naturalWidth / expectedDiffImg.width;
    targetArea = {
      width: Math.min(Math.ceil(scaleRatio * targetArea.width), expectedDiffImg.naturalWidth),
      height: Math.min(Math.ceil(scaleRatio * targetArea.height), expectedDiffImg.naturalHeight),
      left: Math.max(Math.floor(scaleRatio * targetArea.left), 0),
      top: Math.max(Math.floor(scaleRatio * targetArea.top), 0)
    };

    // Save our image set info and trigger results rendering
    // TODO: Unset `targetArea` values on destroy/results removal
    // DEV: We use `Date.now()` to guarantee a different value on multiple presses
    this.setState({
      targetArea: targetArea,
      findSimilarImages: Date.now()
    });
  },
  _updateReferenceImage: function (imgBase64, eagerStatus) {
    // Set our state as loading and an eager status
    // DEV: For an acceptance request, we will eagerly update to 'imagesEqual: true'
    this.setState({
      eagerStatus: eagerStatus || 'loading',
      xhrState: IN_PROGRESS
    });

    // Make an AJAX call to accept our image
    // http://api.jquery.com/jQuery.ajax/#jqXHR
    var jqXHR = $.post('/update-image-set/' + encodeURIComponent(this.id), {
      ref: imgBase64
    });

    // If there is an error
    var that = this;
    jqXHR.fail(function handleFail (jqXHR, textStatus, errorThrown) {
      // TODO: Expose error to user so they can retry
      console.error('Error encountered "' + errorThrown + '" when updating image "' + that.id + '"');

      // Remove loading state (triggers re-render)
      that.setState({
        eagerStatus: null,
        xhrState: NONE
      });
    });

    // When we complete updating
    jqXHR.done(function handleDone (data, textStatus, jqXHR) {
      // Save our new state (triggers re-render)
      // data = {imagesEqual: true}
      that.setState({
        eagerStatus: null,
        imagesEqual: data.imagesEqual,
        isNew: false,
        xhrState: NONE
      });
    });
  },
  updateReferenceImage: function (imgBase64) {
    this._updateReferenceImage(imgBase64);
  }
});

// Export our constructor
module.exports = ImageSet;
<|endoftext|>