"use strict";

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

//---------------------------------------------------------------------
// Source file: ../srcjs/_start.js

(function () {
  var $ = jQuery;

  var exports = window.Shiny = window.Shiny || {};

  exports.version = "1.4.0"; // Version number inserted by Grunt

  var origPushState = window.history.pushState;
  window.history.pushState = function () {
    var result = origPushState.apply(this, arguments);
    $(document).trigger("pushstate");
    return result;
  };

  $(document).on('submit', 'form:not([action])', function (e) {
    e.preventDefault();
  });

  //---------------------------------------------------------------------
  // Source file: ../srcjs/utils.js

  function escapeHTML(str) {
    var escaped = {
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
      "'": "&#039;",
      "/": "&#x2F;"
    };

    return str.replace(/[&<>'"\/]/g, function (m) {
      return escaped[m];
    });
  }

  function randomId() {
    return Math.floor(0x100000000 + Math.random() * 0xF00000000).toString(16);
  }

  function strToBool(str) {
    if (!str || !str.toLowerCase) return undefined;

    switch (str.toLowerCase()) {
      case 'true':
        return true;
      case 'false':
        return false;
      default:
        return undefined;
    }
  }

  // A wrapper for getComputedStyle that is compatible with older browsers.
  // This is significantly faster than jQuery's .css() function.
  function getStyle(el, styleProp) {
    var x;
    if (el.currentStyle) x = el.currentStyle[styleProp];else if (window.getComputedStyle) {
      // getComputedStyle can return null when we're inside a hidden iframe on
      // Firefox; don't attempt to retrieve style props in this case.
      // https://bugzilla.mozilla.org/show_bug.cgi?id=548397
      var style = document.defaultView.getComputedStyle(el, null);
      if (style) x = style.getPropertyValue(styleProp);
    }
    return x;
  }

  // Convert a number to a string with leading zeros
  function padZeros(n, digits) {
    var str = n.toString();
    while (str.length < digits) {
      str = "0" + str;
    }return str;
  }

  // Round to a specified number of significant digits.
  function roundSignif(x) {
    var digits = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;

    if (digits < 1) throw "Significant digits must be at least 1.";

    // This converts to a string and back to a number, which is inelegant, but
    // is less prone to FP rounding error than an alternate method which used
    // Math.round().
    return parseFloat(x.toPrecision(digits));
  }

  // Take a string with format "YYYY-MM-DD" and return a Date object.
  // IE8 and QTWebKit don't support YYYY-MM-DD, but they support YYYY/MM/DD
  function parseDate(dateString) {
    var date = new Date(dateString);
    if (isNaN(date)) date = new Date(dateString.replace(/-/g, "/"));
    return date;
  }

  // Given a Date object, return a string in yyyy-mm-dd format, using the
  // UTC date. This may be a day off from the date in the local time zone.
  function formatDateUTC(date) {
    if (date instanceof Date) {
      return date.getUTCFullYear() + '-' + padZeros(date.getUTCMonth() + 1, 2) + '-' + padZeros(date.getUTCDate(), 2);
    } else {
      return null;
    }
  }

  // Given an element and a function(width, height), returns a function(). When
  // the output function is called, it calls the input function with the offset
  // width and height of the input element--but only if the size of the element
  // is non-zero and the size is different than the last time the output
  // function was called.
  //
  // Basically we are trying to filter out extraneous calls to func, so that
  // when the window size changes or whatever, we don't run resize logic for
  // elements that haven't actually changed size or aren't visible anyway.
  function makeResizeFilter(el, func) {
    var lastSize = {};
    return function () {
      var size = { w: el.offsetWidth, h: el.offsetHeight };
      if (size.w === 0 && size.h === 0) return;
      if (size.w === lastSize.w && size.h === lastSize.h) return;
      lastSize = size;
      func(size.w, size.h);
    };
  }

  var _BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;

  function makeBlob(parts) {

    // Browser compatibility is a mess right now. The code as written works in
    // a variety of modern browsers, but sadly gives a deprecation warning
    // message on the console in current versions (as of this writing) of
    // Chrome.

    // Safari 6.0 (8536.25) on Mac OS X 10.8.1:
    // Has Blob constructor but it doesn't work with ArrayBufferView args

    // Google Chrome 21.0.1180.81 on Xubuntu 12.04:
    // Has Blob constructor, accepts ArrayBufferView args, accepts ArrayBuffer
    // but with a deprecation warning message

    // Firefox 15.0 on Xubuntu 12.04:
    // Has Blob constructor, accepts both ArrayBuffer and ArrayBufferView args

    // Chromium 18.0.1025.168 (Developer Build 134367 Linux) on Xubuntu 12.04:
    // No Blob constructor. Has WebKitBlobBuilder.

    try {
      return new Blob(parts);
    } catch (e) {
      var blobBuilder = new _BlobBuilder();
      $.each(parts, function (i, part) {
        blobBuilder.append(part);
      });
      return blobBuilder.getBlob();
    }
  }

  function pixelRatio() {
    if (window.devicePixelRatio) {
      return window.devicePixelRatio;
    } else {
      return 1;
    }
  }

  // Takes a string expression and returns a function that takes an argument.
  //
  // When the function is executed, it will evaluate that expression using
  // "with" on the argument value, and return the result.
  function scopeExprToFunc(expr) {
    /*jshint evil: true */
    var expr_escaped = expr.replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0').replace(/\n/g, '\\n').replace(/\r/g, '\\r')
    // \b has a special meaning; need [\b] to match backspace char.
    .replace(/[\b]/g, '\\b');

    try {
      var func = new Function("with (this) {\n        try {\n          return (" + expr + ");\n        } catch (e) {\n          console.error('Error evaluating expression: " + expr_escaped + "');\n          throw e;\n        }\n      }");
    } catch (e) {
      console.error("Error parsing expression: " + expr);
      throw e;
    }

    return function (scope) {
      return func.call(scope);
    };
  }

  function asArray(value) {
    if (value === null || value === undefined) return [];
    if ($.isArray(value)) return value;
    return [value];
  }

  // We need a stable sorting algorithm for ordering
  // bindings by priority and insertion order.
  function mergeSort(list, sortfunc) {
    function merge(sortfunc, a, b) {
      var ia = 0;
      var ib = 0;
      var sorted = [];
      while (ia < a.length && ib < b.length) {
        if (sortfunc(a[ia], b[ib]) <= 0) {
          sorted.push(a[ia++]);
        } else {
          sorted.push(b[ib++]);
        }
      }
      while (ia < a.length) {
        sorted.push(a[ia++]);
      }while (ib < b.length) {
        sorted.push(b[ib++]);
      }return sorted;
    }

    // Don't mutate list argument
    list = list.slice(0);

    for (var chunkSize = 1; chunkSize < list.length; chunkSize *= 2) {
      for (var i = 0; i < list.length; i += chunkSize * 2) {
        var listA = list.slice(i, i + chunkSize);
        var listB = list.slice(i + chunkSize, i + chunkSize * 2);
        var merged = merge(sortfunc, listA, listB);
        var args = [i, merged.length];
        Array.prototype.push.apply(args, merged);
        Array.prototype.splice.apply(list, args);
      }
    }

    return list;
  }

  // Escape jQuery selector metacharacters: !"#$%&'()*+,./:;<=>?@[\]^`{|}~
  var $escape = exports.$escape = function (val) {
    return val.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, '\\$1');
  };

  // Maps a function over an object, preserving keys. Like the mapValues
  // function from lodash.
  function mapValues(obj, f) {
    var newObj = {};
    for (var key in obj) {
      if (obj.hasOwnProperty(key)) newObj[key] = f(obj[key], key, obj);
    }
    return newObj;
  }

  // This is does the same as Number.isNaN, but that function unfortunately does
  // not exist in any version of IE.
  function isnan(x) {
    return typeof x === 'number' && isNaN(x);
  }

  // Binary equality function used by the equal function.
  function _equal(x, y) {
    if ($.type(x) === "object" && $.type(y) === "object") {
      if (Object.keys(x).length !== Object.keys(y).length) return false;
      for (var prop in x) {
        if (!y.hasOwnProperty(prop) || !_equal(x[prop], y[prop])) return false;
      }return true;
    } else if ($.type(x) === "array" && $.type(y) === "array") {
      if (x.length !== y.length) return false;
      for (var i = 0; i < x.length; i++) {
        if (!_equal(x[i], y[i])) return false;
      }return true;
    } else {
      return x === y;
    }
  }

  // Structural or "deep" equality predicate. Tests two or more arguments for
  // equality, traversing arrays and objects (as determined by $.type) as
  // necessary.
  //
  // Objects other than objects and arrays are tested for equality using ===.
  function equal() {
    for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }

    if (args.length < 2) throw new Error("equal requires at least two arguments.");
    for (var i = 0; i < args.length - 1; i++) {
      if (!_equal(args[i], args[i + 1])) return false;
    }
    return true;
  };

  // Compare version strings like "1.0.1", "1.4-2". `op` must be a string like
  // "==" or "<".
  exports.compareVersion = function (a, op, b) {
    function versionParts(ver) {
      return (ver + "").replace(/-/, ".").replace(/(\.0)+[^\.]*$/, "").split(".");
    }

    function cmpVersion(a, b) {
      a = versionParts(a);
      b = versionParts(b);
      var len = Math.min(a.length, b.length);
      var cmp;

      for (var i = 0; i < len; i++) {
        cmp = parseInt(a[i], 10) - parseInt(b[i], 10);
        if (cmp !== 0) {
          return cmp;
        }
      }
      return a.length - b.length;
    }

    var diff = cmpVersion(a, b);

    if (op === "==") return diff === 0;else if (op === ">=") return diff >= 0;else if (op === ">") return diff > 0;else if (op === "<=") return diff <= 0;else if (op === "<") return diff < 0;else throw "Unknown operator: " + op;
  };

  function updateLabel(labelTxt, labelNode) {
    // Only update if label was specified in the update method
    if (typeof labelTxt === "undefined") return;
    if (labelNode.length !== 1) {
      throw new Error("labelNode must be of length 1");
    }

    // Should the label be empty?
    var emptyLabel = $.isArray(labelTxt) && labelTxt.length === 0;

    if (emptyLabel) {
      labelNode.addClass("shiny-label-null");
    } else {
      labelNode.text(labelTxt);
      labelNode.removeClass("shiny-label-null");
    }
  }

  //---------------------------------------------------------------------
  // Source file: ../srcjs/browser.js

  var browser = function () {

    var isQt = false;
    // For easy handling of Qt quirks using CSS
    if (/\bQt\//.test(window.navigator.userAgent)) {
      $(document.documentElement).addClass('qt');
      isQt = true;
    }

    // Enable special treatment for Qt 5 quirks on Linux
    if (/\bQt\/5/.test(window.navigator.userAgent) && /Linux/.test(window.navigator.userAgent)) {
      $(document.documentElement).addClass('qt5');
    }

    // Detect IE information
    var isIE = navigator.appName === 'Microsoft Internet Explorer';

    function getIEVersion() {
      var rv = -1;
      if (isIE) {
        var ua = navigator.userAgent;
        var re = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})");
        if (re.exec(ua) !== null) rv = parseFloat(RegExp.$1);
      }
      return rv;
    }

    return {
      isQt: isQt,
      isIE: isIE,
      IEVersion: getIEVersion()
    };
  }();

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_rate.js

  var Invoker = function Invoker(target, func) {
    this.target = target;
    this.func = func;
  };

  (function () {
    this.normalCall = this.immediateCall = function () {
      this.func.apply(this.target, arguments);
    };
  }).call(Invoker.prototype);

  var Debouncer = function Debouncer(target, func, delayMs) {
    this.target = target;
    this.func = func;
    this.delayMs = delayMs;

    this.timerId = null;
    this.args = null;
  };

  (function () {
    this.normalCall = function () {
      var self = this;

      this.$clearTimer();
      this.args = arguments;

      this.timerId = setTimeout(function () {
        // IE8 doesn't reliably clear timeout, so this additional
        // check is needed
        if (self.timerId === null) return;
        self.$clearTimer();
        self.$invoke();
      }, this.delayMs);
    };
    this.immediateCall = function () {
      this.$clearTimer();
      this.args = arguments;
      this.$invoke();
    };
    this.isPending = function () {
      return this.timerId !== null;
    };
    this.$clearTimer = function () {
      if (this.timerId !== null) {
        clearTimeout(this.timerId);
        this.timerId = null;
      }
    };
    this.$invoke = function () {
      this.func.apply(this.target, this.args);
      this.args = null;
    };
  }).call(Debouncer.prototype);

  var Throttler = function Throttler(target, func, delayMs) {
    this.target = target;
    this.func = func;
    this.delayMs = delayMs;

    this.timerId = null;
    this.args = null;
  };

  (function () {
    this.normalCall = function () {
      var self = this;

      this.args = arguments;
      if (this.timerId === null) {
        this.$invoke();
        this.timerId = setTimeout(function () {
          // IE8 doesn't reliably clear timeout, so this additional
          // check is needed
          if (self.timerId === null) return;
          self.$clearTimer();
          if (self.args) self.normalCall.apply(self, self.args);
        }, this.delayMs);
      }
    };
    this.immediateCall = function () {
      this.$clearTimer();
      this.args = arguments;
      this.$invoke();
    };
    this.isPending = function () {
      return this.timerId !== null;
    };
    this.$clearTimer = function () {
      if (this.timerId !== null) {
        clearTimeout(this.timerId);
        this.timerId = null;
      }
    };
    this.$invoke = function () {
      this.func.apply(this.target, this.args);
      this.args = null;
    };
  }).call(Throttler.prototype);

  // Returns a debounced version of the given function.
  // Debouncing means that when the function is invoked,
  // there is a delay of `threshold` milliseconds before
  // it is actually executed, and if the function is
  // invoked again before that threshold has elapsed then
  // the clock starts over.
  //
  // For example, if a function is debounced with a
  // threshold of 1000ms, then calling it 17 times at
  // 900ms intervals will result in a single execution
  // of the underlying function, 1000ms after the 17th
  // call.
  function debounce(threshold, func) {
    var timerId = null;
    var self, args;
    return function () {
      self = this;
      args = arguments;
      if (timerId !== null) {
        clearTimeout(timerId);
        timerId = null;
      }
      timerId = setTimeout(function () {
        // IE8 doesn't reliably clear timeout, so this additional
        // check is needed
        if (timerId === null) return;
        timerId = null;
        func.apply(self, args);
      }, threshold);
    };
  }

  // Returns a throttled version of the given function.
  // Throttling means that the underlying function will
  // be executed no more than once every `threshold`
  // milliseconds.
  //
  // For example, if a function is throttled with a
  // threshold of 1000ms, then calling it 17 times at
  // 900ms intervals will result in something like 15
  // or 16 executions of the underlying function.
  // eslint-disable-next-line no-unused-vars
  function throttle(threshold, func) {
    var executionPending = false;
    var timerId = null;
    var self, args;

    function throttled() {
      self = null;
      args = null;
      if (timerId === null) {
        // Haven't seen a call recently. Execute now and
        // start a timer to buffer any subsequent calls.
        timerId = setTimeout(function () {
          // When time expires, clear the timer; and if
          // there has been a call in the meantime, repeat.
          timerId = null;
          if (executionPending) {
            executionPending = false;
            throttled.apply(self, args);
          }
        }, threshold);
        func.apply(this, arguments);
      } else {
        // Something executed recently. Don't do anything
        // except set up target/arguments to be called later
        executionPending = true;
        self = this;
        args = arguments;
      }
    }
    return throttled;
  }

  // Schedules data to be sent to shinyapp at the next setTimeout(0).
  // Batches multiple input calls into one websocket message.
  var InputBatchSender = function InputBatchSender(shinyapp) {
    this.shinyapp = shinyapp;
    this.timerId = null;
    this.pendingData = {};
    this.reentrant = false;
    this.lastChanceCallback = [];
  };
  (function () {
    this.setInput = function (nameType, value, opts) {
      this.pendingData[nameType] = value;

      if (!this.reentrant) {
        if (opts.priority === "event") {
          this.$sendNow();
        } else if (!this.timerId) {
          this.timerId = setTimeout(this.$sendNow.bind(this), 0);
        }
      }
    };

    this.$sendNow = function () {
      if (this.reentrant) {
        console.trace("Unexpected reentrancy in InputBatchSender!");
      }

      this.reentrant = true;
      try {
        this.timerId = null;
        $.each(this.lastChanceCallback, function (i, callback) {
          callback();
        });
        var currentData = this.pendingData;
        this.pendingData = {};
        this.shinyapp.sendInput(currentData);
      } finally {
        this.reentrant = false;
      }
    };
  }).call(InputBatchSender.prototype);

  var InputNoResendDecorator = function InputNoResendDecorator(target, initialValues) {
    this.target = target;
    this.lastSentValues = this.reset(initialValues);
  };
  (function () {
    this.setInput = function (nameType, value, opts) {
      var _splitInputNameType = splitInputNameType(nameType),
          inputName = _splitInputNameType.name,
          inputType = _splitInputNameType.inputType;

      var jsonValue = JSON.stringify(value);

      if (opts.priority !== "event" && this.lastSentValues[inputName] && this.lastSentValues[inputName].jsonValue === jsonValue && this.lastSentValues[inputName].inputType === inputType) {
        return;
      }
      this.lastSentValues[inputName] = { jsonValue: jsonValue, inputType: inputType };
      this.target.setInput(nameType, value, opts);
    };
    this.reset = function () {
      var values = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

      // Given an object with flat name-value format:
      //   { x: "abc", "y.shiny.number": 123 }
      // Create an object in cache format and save it:
      //   { x: { jsonValue: '"abc"', inputType: "" },
      //     y: { jsonValue: "123", inputType: "shiny.number" } }
      var cacheValues = {};

      for (var inputName in values) {
        if (values.hasOwnProperty(inputName)) {
          var _splitInputNameType2 = splitInputNameType(inputName),
              name = _splitInputNameType2.name,
              inputType = _splitInputNameType2.inputType;

          cacheValues[name] = {
            jsonValue: JSON.stringify(values[inputName]),
            inputType: inputType
          };
        }
      }

      this.lastSentValues = cacheValues;
    };
  }).call(InputNoResendDecorator.prototype);

  var InputEventDecorator = function InputEventDecorator(target) {
    this.target = target;
  };
  (function () {
    this.setInput = function (nameType, value, opts) {
      var evt = jQuery.Event("shiny:inputchanged");

      var input = splitInputNameType(nameType);
      evt.name = input.name;
      evt.inputType = input.inputType;
      evt.value = value;
      evt.binding = opts.binding;
      evt.el = opts.el;
      evt.priority = opts.priority;

      $(opts.el).trigger(evt);

      if (!evt.isDefaultPrevented()) {
        var name = evt.name;
        if (evt.inputType !== '') name += ':' + evt.inputType;

        // Most opts aren't passed along to lower levels in the input decorator
        // stack.
        this.target.setInput(name, evt.value, { priority: opts.priority });
      }
    };
  }).call(InputEventDecorator.prototype);

  var InputRateDecorator = function InputRateDecorator(target) {
    this.target = target;
    this.inputRatePolicies = {};
  };
  (function () {
    // Note that the first argument of setInput() and setRatePolicy()
    // are passed both the input name (i.e., inputId) and type.
    // https://github.com/rstudio/shiny/blob/67d3a/srcjs/init_shiny.js#L111-L126
    // However, $ensureInit() and $doSetInput() are meant to be passed just
    // the input name (i.e., inputId), which is why we distinguish between
    // nameType and name.
    this.setInput = function (nameType, value, opts) {
      var _splitInputNameType3 = splitInputNameType(nameType),
          inputName = _splitInputNameType3.name;

      this.$ensureInit(inputName);

      if (opts.priority !== "deferred") this.inputRatePolicies[inputName].immediateCall(nameType, value, opts);else this.inputRatePolicies[inputName].normalCall(nameType, value, opts);
    };
    this.setRatePolicy = function (nameType, mode, millis) {
      var _splitInputNameType4 = splitInputNameType(nameType),
          inputName = _splitInputNameType4.name;

      if (mode === 'direct') {
        this.inputRatePolicies[inputName] = new Invoker(this, this.$doSetInput);
      } else if (mode === 'debounce') {
        this.inputRatePolicies[inputName] = new Debouncer(this, this.$doSetInput, millis);
      } else if (mode === 'throttle') {
        this.inputRatePolicies[inputName] = new Throttler(this, this.$doSetInput, millis);
      }
    };
    this.$ensureInit = function (name) {
      if (!(name in this.inputRatePolicies)) this.setRatePolicy(name, 'direct');
    };
    this.$doSetInput = function (nameType, value, opts) {
      this.target.setInput(nameType, value, opts);
    };
  }).call(InputRateDecorator.prototype);

  var InputDeferDecorator = function InputDeferDecorator(target) {
    this.target = target;
    this.pendingInput = {};
  };
  (function () {
    this.setInput = function (nameType, value, opts) {
      if (/^\./.test(nameType)) this.target.setInput(nameType, value, opts);else this.pendingInput[nameType] = { value: value, opts: opts };
    };
    this.submit = function () {
      for (var nameType in this.pendingInput) {
        if (this.pendingInput.hasOwnProperty(nameType)) {
          var _pendingInput$nameTyp = this.pendingInput[nameType],
              value = _pendingInput$nameTyp.value,
              opts = _pendingInput$nameTyp.opts;

          this.target.setInput(nameType, value, opts);
        }
      }
    };
  }).call(InputDeferDecorator.prototype);

  var InputValidateDecorator = function InputValidateDecorator(target) {
    this.target = target;
  };
  (function () {
    this.setInput = function (nameType, value, opts) {
      if (!nameType) throw "Can't set input with empty name.";

      opts = addDefaultInputOpts(opts);

      this.target.setInput(nameType, value, opts);
    };
  }).call(InputValidateDecorator.prototype);

  // Merge opts with defaults, and return a new object.
  function addDefaultInputOpts(opts) {

    opts = $.extend({
      priority: "immediate",
      binding: null,
      el: null
    }, opts);

    if (opts && typeof opts.priority !== "undefined") {
      switch (opts.priority) {
        case "deferred":
        case "immediate":
        case "event":
          break;
        default:
          throw new Error("Unexpected input value mode: '" + opts.priority + "'");
      }
    }

    return opts;
  }

  function splitInputNameType(nameType) {
    var name2 = nameType.split(':');
    return {
      name: name2[0],
      inputType: name2.length > 1 ? name2[1] : ''
    };
  }

  //---------------------------------------------------------------------
  // Source file: ../srcjs/shinyapp.js

  var ShinyApp = function ShinyApp() {
    this.$socket = null;

    // Cached input values
    this.$inputValues = {};

    // Input values at initialization (and reconnect)
    this.$initialInput = {};

    // Output bindings
    this.$bindings = {};

    // Cached values/errors
    this.$values = {};
    this.$errors = {};

    // Conditional bindings (show/hide element based on expression)
    this.$conditionals = {};

    this.$pendingMessages = [];
    this.$activeRequests = {};
    this.$nextRequestId = 0;

    this.$allowReconnect = false;
  };

  (function () {

    this.connect = function (initialInput) {
      if (this.$socket) throw "Connect was already called on this application object";

      this.$socket = this.createSocket();
      this.$initialInput = initialInput;
      $.extend(this.$inputValues, initialInput);

      this.$updateConditionals();
    };

    this.isConnected = function () {
      return !!this.$socket;
    };

    var scheduledReconnect = null;
    this.reconnect = function () {
      // This function can be invoked directly even if there's a scheduled
      // reconnect, so be sure to clear any such scheduled reconnects.
      clearTimeout(scheduledReconnect);

      if (this.isConnected()) throw "Attempted to reconnect, but already connected.";

      this.$socket = this.createSocket();
      this.$initialInput = $.extend({}, this.$inputValues);
      this.$updateConditionals();
    };

    this.createSocket = function () {
      var self = this;

      var createSocketFunc = exports.createSocket || function () {
        var protocol = 'ws:';
        if (window.location.protocol === 'https:') protocol = 'wss:';

        var defaultPath = window.location.pathname;
        // some older WebKit browsers return the pathname already decoded;
        // if we find invalid URL characters in the path, encode them
        if (!/^([$#!&-;=?-[\]_a-z~]|%[0-9a-fA-F]{2})+$/.test(defaultPath)) {
          defaultPath = encodeURI(defaultPath);
          // Bizarrely, QtWebKit requires us to encode these characters *twice*
          if (browser.isQt) {
            defaultPath = encodeURI(defaultPath);
          }
        }
        if (!/\/$/.test(defaultPath)) defaultPath += '/';
        defaultPath += 'websocket/';

        var ws = new WebSocket(protocol + '//' + window.location.host + defaultPath);
        ws.binaryType = 'arraybuffer';

        return ws;
      };

      var socket = createSocketFunc();
      var hasOpened = false;
      socket.onopen = function () {
        hasOpened = true;

        $(document).trigger({
          type: 'shiny:connected',
          socket: socket
        });

        self.onConnected();

        socket.send(JSON.stringify({
          method: 'init',
          data: self.$initialInput
        }));

        while (self.$pendingMessages.length) {
          var msg = self.$pendingMessages.shift();
          socket.send(msg);
        }
      };
      socket.onmessage = function (e) {
        self.dispatchMessage(e.data);
      };
      // Called when a successfully-opened websocket is closed, or when an
      // attempt to open a connection fails.
      socket.onclose = function () {
        // These things are needed only if we've successfully opened the
        // websocket.
        if (hasOpened) {
          $(document).trigger({
            type: 'shiny:disconnected',
            socket: socket
          });

          self.$notifyDisconnected();
        }

        self.onDisconnected(); // Must be run before self.$removeSocket()
        self.$removeSocket();
      };
      return socket;
    };

    this.sendInput = function (values) {
      var msg = JSON.stringify({
        method: 'update',
        data: values
      });

      this.$sendMsg(msg);

      $.extend(this.$inputValues, values);
      this.$updateConditionals();
    };

    this.$notifyDisconnected = function () {
      if (window.parent) {
        window.parent.postMessage("disconnected", "*");
      }
    };

    this.$removeSocket = function () {
      this.$socket = null;
    };

    this.$scheduleReconnect = function (delay) {
      var self = this;
      scheduledReconnect = setTimeout(function () {
        self.reconnect();
      }, delay);
    };

    // How long should we wait before trying the next reconnection?
    // The delay will increase with subsequent attempts.
    // .next: Return the time to wait for next connection, and increment counter.
    // .reset: Reset the attempt counter.
    var reconnectDelay = function () {
      var attempts = 0;
      // Time to wait before each reconnection attempt. If we go through all of
      // these values, repeated use the last one. Add 500ms to each one so that
      // in the last 0.5s, it shows "..."
      var delays = [1500, 1500, 2500, 2500, 5500, 5500, 10500];

      return {
        next: function next() {
          var i = attempts;
          // Instead of going off the end, use the last one
          if (i >= delays.length) {
            i = delays.length - 1;
          }

          attempts++;
          return delays[i];
        },
        reset: function reset() {
          attempts = 0;
        }
      };
    }();

    this.onDisconnected = function () {
      // Add gray-out overlay, if not already present
      var $overlay = $('#shiny-disconnected-overlay');
      if ($overlay.length === 0) {
        $(document.body).append('<div id="shiny-disconnected-overlay"></div>');
      }

      // To try a reconnect, both the app (this.$allowReconnect) and the
      // server (this.$socket.allowReconnect) must allow reconnections, or
      // session$allowReconnect("force") was called. The "force" option should
      // only be used for testing.
      if (this.$allowReconnect === true && this.$socket.allowReconnect === true || this.$allowReconnect === "force") {
        var delay = reconnectDelay.next();
        exports.showReconnectDialog(delay);
        this.$scheduleReconnect(delay);
      }
    };

    this.onConnected = function () {
      $('#shiny-disconnected-overlay').remove();
      exports.hideReconnectDialog();
      reconnectDelay.reset();
    };

    // NB: Including blobs will cause IE to break!
    // TODO: Make blobs work with Internet Explorer
    //
    // Websocket messages are normally one-way--i.e. the client passes a
    // message to the server but there is no way for the server to provide
    // a response to that specific message. makeRequest provides a way to
    // do asynchronous RPC over websocket. Each request has a method name
    // and arguments, plus optionally one or more binary blobs can be
    // included as well. The request is tagged with a unique number that
    // the server will use to label the corresponding response.
    //
    // @param method A string that tells the server what logic to run.
    // @param args An array of objects that should also be passed to the
    //   server in JSON-ified form.
    // @param onSuccess A function that will be called back if the server
    //   responds with success. If the server provides a value in the
    //   response, the function will be called with it as the only argument.
    // @param onError A function that will be called back if the server
    //   responds with error, or if the request fails for any other reason.
    //   The parameter to onError will be a string describing the error.
    // @param blobs Optionally, an array of Blob, ArrayBuffer, or string
    //   objects that will be made available to the server as part of the
    //   request. Strings will be encoded using UTF-8.
    this.makeRequest = function (method, args, onSuccess, onError, blobs) {
      var requestId = this.$nextRequestId;
      while (this.$activeRequests[requestId]) {
        requestId = (requestId + 1) % 1000000000;
      }
      this.$nextRequestId = requestId + 1;

      this.$activeRequests[requestId] = {
        onSuccess: onSuccess,
        onError: onError
      };

      var msg = JSON.stringify({
        method: method,
        args: args,
        tag: requestId
      });

      if (blobs) {
        // We have binary data to transfer; form a different kind of packet.
        // Start with a 4-byte signature, then for each blob, emit 4 bytes for
        // the length followed by the blob. The json payload is UTF-8 encoded
        // and used as the first blob.

        var uint32_to_buf = function uint32_to_buf(val) {
          var buffer = new ArrayBuffer(4);
          var view = new DataView(buffer);
          view.setUint32(0, val, true); // little-endian
          return buffer;
        };

        var payload = [];
        payload.push(uint32_to_buf(0x01020202)); // signature

        var jsonBuf = makeBlob([msg]);
        payload.push(uint32_to_buf(jsonBuf.size));
        payload.push(jsonBuf);

        for (var i = 0; i < blobs.length; i++) {
          payload.push(uint32_to_buf(blobs[i].byteLength || blobs[i].size || 0));
          payload.push(blobs[i]);
        }

        msg = makeBlob(payload);
      }

      this.$sendMsg(msg);
    };

    this.$sendMsg = function (msg) {
      if (!this.$socket.readyState) {
        this.$pendingMessages.push(msg);
      } else {
        this.$socket.send(msg);
      }
    };

    this.receiveError = function (name, error) {
      if (this.$errors[name] === error) return;

      this.$errors[name] = error;
      delete this.$values[name];

      var binding = this.$bindings[name];
      var evt = jQuery.Event('shiny:error');
      evt.name = name;
      evt.error = error;
      evt.binding = binding;
      $(binding ? binding.el : document).trigger(evt);
      if (!evt.isDefaultPrevented() && binding && binding.onValueError) {
        binding.onValueError(evt.error);
      }
    };

    this.receiveOutput = function (name, value) {
      var binding = this.$bindings[name];
      var evt = jQuery.Event('shiny:value');
      evt.name = name;
      evt.value = value;
      evt.binding = binding;

      if (this.$values[name] === value) {
        $(binding ? binding.el : document).trigger(evt);
        return undefined;
      }

      this.$values[name] = value;
      delete this.$errors[name];

      $(binding ? binding.el : document).trigger(evt);

      if (!evt.isDefaultPrevented() && binding) {
        binding.onValueChange(evt.value);
      }

      return value;
    };

    this.bindOutput = function (id, binding) {
      if (!id) throw "Can't bind an element with no ID";
      if (this.$bindings[id]) throw "Duplicate binding for ID " + id;
      this.$bindings[id] = binding;

      if (this.$values[id] !== undefined) binding.onValueChange(this.$values[id]);else if (this.$errors[id] !== undefined) binding.onValueError(this.$errors[id]);

      return binding;
    };

    this.unbindOutput = function (id, binding) {
      if (this.$bindings[id] === binding) {
        delete this.$bindings[id];
        return true;
      } else {
        return false;
      }
    };

    // Narrows a scopeComponent -- an input or output object -- to one constrained
    // by nsPrefix. Returns a new object with keys removed and renamed as
    // necessary.
    function narrowScopeComponent(scopeComponent, nsPrefix) {
      return Object.keys(scopeComponent).filter(function (k) {
        return k.indexOf(nsPrefix) === 0;
      }).map(function (k) {
        return _defineProperty({}, k.substring(nsPrefix.length), scopeComponent[k]);
      }).reduce(function (obj, pair) {
        return $.extend(obj, pair);
      }, {});
    }

    // Narrows a scope -- an object with input and output "subComponents" -- to
    // one constrained by the nsPrefix string.
    //
    // If nsPrefix is null or empty, returns scope without modification.
    //
    // Otherwise, returns a new object with keys in subComponents removed and
    // renamed as necessary.
    function narrowScope(scope, nsPrefix) {
      return nsPrefix ? {
        input: narrowScopeComponent(scope.input, nsPrefix),
        output: narrowScopeComponent(scope.output, nsPrefix)
      } : scope;
    }

    this.$updateConditionals = function () {
      $(document).trigger({
        type: 'shiny:conditional'
      });

      var inputs = {};

      // Input keys use "name:type" format; we don't want the user to
      // have to know about the type suffix when referring to inputs.
      for (var name in this.$inputValues) {
        if (this.$inputValues.hasOwnProperty(name)) {
          var shortName = name.replace(/:.*/, '');
          inputs[shortName] = this.$inputValues[name];
        }
      }

      var scope = { input: inputs, output: this.$values };

      var conditionals = $(document).find('[data-display-if]');
      for (var i = 0; i < conditionals.length; i++) {
        var el = $(conditionals[i]);
        var condFunc = el.data('data-display-if-func');

        if (!condFunc) {
          var condExpr = el.attr('data-display-if');
          condFunc = scopeExprToFunc(condExpr);
          el.data('data-display-if-func', condFunc);
        }

        var nsPrefix = el.attr('data-ns-prefix');
        var nsScope = narrowScope(scope, nsPrefix);
        var show = condFunc(nsScope);
        var showing = el.css("display") !== "none";
        if (show !== showing) {
          if (show) {
            el.trigger('show');
            el.show();
            el.trigger('shown');
          } else {
            el.trigger('hide');
            el.hide();
            el.trigger('hidden');
          }
        }
      }
    };

    // Message handler management functions =================================

    // Records insertion order of handlers. Maps number to name. This is so
    // we can dispatch messages to handlers in the order that handlers were
    // added.
    var messageHandlerOrder = [];
    // Keep track of handlers by name. Maps name to handler function.
    var messageHandlers = {};

    // Two categories of message handlers: those that are from Shiny, and those
    // that are added by the user. The Shiny ones handle messages in
    // msgObj.values, msgObj.errors, and so on. The user ones handle messages
    // in msgObj.custom.foo and msgObj.custom.bar.
    var customMessageHandlerOrder = [];
    var customMessageHandlers = {};

    // Adds Shiny (internal) message handler
    function addMessageHandler(type, handler) {
      if (messageHandlers[type]) {
        throw 'handler for message of type "' + type + '" already added.';
      }
      if (typeof handler !== 'function') {
        throw 'handler must be a function.';
      }
      if (handler.length !== 1) {
        throw 'handler must be a function that takes one argument.';
      }
      messageHandlerOrder.push(type);
      messageHandlers[type] = handler;
    }

    // Adds custom message handler - this one is exposed to the user
    function addCustomMessageHandler(type, handler) {
      // Remove any previously defined handlers so that only the most recent one
      // will be called
      if (customMessageHandlers[type]) {
        var typeIdx = customMessageHandlerOrder.indexOf(type);
        if (typeIdx !== -1) {
          customMessageHandlerOrder.splice(typeIdx, 1);
          delete customMessageHandlers[type];
        }
      }
      if (typeof handler !== 'function') {
        throw 'handler must be a function.';
      }
      if (handler.length !== 1) {
        throw 'handler must be a function that takes one argument.';
      }

      customMessageHandlerOrder.push(type);
      customMessageHandlers[type] = handler;
    }

    exports.addCustomMessageHandler = addCustomMessageHandler;

    this.dispatchMessage = function (data) {
      var msgObj = {};
      if (typeof data === "string") {
        msgObj = JSON.parse(data);
      } else {
        // data is arraybuffer
        var len = new DataView(data, 0, 1).getUint8(0);
        var typedv = new DataView(data, 1, len);
        var typebuf = [];
        for (var i = 0; i < len; i++) {
          typebuf.push(String.fromCharCode(typedv.getUint8(i)));
        }
        var type = typebuf.join("");
        data = data.slice(len + 1);
        msgObj.custom = {};
        msgObj.custom[type] = data;
      }

      var evt = jQuery.Event('shiny:message');
      evt.message = msgObj;
      $(document).trigger(evt);
      if (evt.isDefaultPrevented()) return;

      // Send msgObj.foo and msgObj.bar to appropriate handlers
      this._sendMessagesToHandlers(evt.message, messageHandlers, messageHandlerOrder);

      this.$updateConditionals();
    };

    // A function for sending messages to the appropriate handlers.
    // - msgObj: the object containing messages, with format {msgObj.foo, msObj.bar
    this._sendMessagesToHandlers = function (msgObj, handlers, handlerOrder) {
      // Dispatch messages to handlers, if handler is present
      for (var i = 0; i < handlerOrder.length; i++) {
        var msgType = handlerOrder[i];
        if (msgObj.hasOwnProperty(msgType)) {
          // Execute each handler with 'this' referring to the present value of
          // 'this'
          handlers[msgType].call(this, msgObj[msgType]);
        }
      }
    };

    // Message handlers =====================================================

    addMessageHandler('values', function (message) {
      for (var name in this.$bindings) {
        if (this.$bindings.hasOwnProperty(name)) this.$bindings[name].showProgress(false);
      }

      for (var key in message) {
        if (message.hasOwnProperty(key)) this.receiveOutput(key, message[key]);
      }
    });

    addMessageHandler('errors', function (message) {
      for (var key in message) {
        if (message.hasOwnProperty(key)) this.receiveError(key, message[key]);
      }
    });

    addMessageHandler('inputMessages', function (message) {
      // inputMessages should be an array
      for (var i = 0; i < message.length; i++) {
        var $obj = $('.shiny-bound-input#' + $escape(message[i].id));
        var inputBinding = $obj.data('shiny-input-binding');

        // Dispatch the message to the appropriate input object
        if ($obj.length > 0) {
          var el = $obj[0];
          var evt = jQuery.Event('shiny:updateinput');
          evt.message = message[i].message;
          evt.binding = inputBinding;
          $(el).trigger(evt);
          if (!evt.isDefaultPrevented()) inputBinding.receiveMessage(el, evt.message);
        }
      }
    });

    addMessageHandler('javascript', function (message) {
      /*jshint evil: true */
      eval(message);
    });

    addMessageHandler('console', function (message) {
      for (var i = 0; i < message.length; i++) {
        if (console.log) console.log(message[i]);
      }
    });

    addMessageHandler('progress', function (message) {
      if (message.type && message.message) {
        var handler = progressHandlers[message.type];
        if (handler) handler.call(this, message.message);
      }
    });

    addMessageHandler('notification', function (message) {
      if (message.type === 'show') exports.notifications.show(message.message);else if (message.type === 'remove') exports.notifications.remove(message.message);else throw 'Unkown notification type: ' + message.type;
    });

    addMessageHandler('modal', function (message) {
      if (message.type === 'show') exports.modal.show(message.message);else if (message.type === 'remove') exports.modal.remove(); // For 'remove', message content isn't used
      else throw 'Unkown modal type: ' + message.type;
    });

    addMessageHandler('response', function (message) {
      var requestId = message.tag;
      var request = this.$activeRequests[requestId];
      if (request) {
        delete this.$activeRequests[requestId];
        if ('value' in message) request.onSuccess(message.value);else request.onError(message.error);
      }
    });

    addMessageHandler('allowReconnect', function (message) {
      if (message === true || message === false || message === "force") {
        this.$allowReconnect = message;
      } else {
        throw "Invalid value for allowReconnect: " + message;
      }
    });

    addMessageHandler('custom', function (message) {
      // For old-style custom messages - should deprecate and migrate to new
      // method
      if (exports.oncustommessage) {
        exports.oncustommessage(message);
      }

      // Send messages.foo and messages.bar to appropriate handlers
      this._sendMessagesToHandlers(message, customMessageHandlers, customMessageHandlerOrder);
    });

    addMessageHandler('config', function (message) {
      this.config = { workerId: message.workerId, sessionId: message.sessionId };
      if (message.user) exports.user = message.user;
      $(document).trigger('shiny:sessioninitialized');
    });

    addMessageHandler('busy', function (message) {
      if (message === 'busy') {
        $(document.documentElement).addClass('shiny-busy');
        $(document).trigger('shiny:busy');
      } else if (message === 'idle') {
        $(document.documentElement).removeClass('shiny-busy');
        $(document).trigger('shiny:idle');
      }
    });

    addMessageHandler('recalculating', function (message) {
      if (message.hasOwnProperty('name') && message.hasOwnProperty('status')) {
        var binding = this.$bindings[message.name];
        $(binding ? binding.el : null).trigger({
          type: 'shiny:' + message.status
        });
      }
    });

    addMessageHandler('reload', function (message) {
      window.location.reload();
    });

    addMessageHandler('shiny-insert-ui', function (message) {
      var targets = $(message.selector);
      if (targets.length === 0) {
        // render the HTML and deps to a null target, so
        // the side-effect of rendering the deps, singletons,
        // and <head> still occur
        console.warn('The selector you chose ("' + message.selector + '") could not be found in the DOM.');
        exports.renderHtml(message.content.html, $([]), message.content.deps);
      } else {
        targets.each(function (i, target) {
          exports.renderContent(target, message.content, message.where);
          return message.multiple;
        });
      }
    });

    addMessageHandler('shiny-remove-ui', function (message) {
      var els = $(message.selector);
      els.each(function (i, el) {
        exports.unbindAll(el, true);
        $(el).remove();
        // If `multiple` is false, returning false terminates the function
        // and no other elements are removed; if `multiple` is true,
        // returning true continues removing all remaining elements.
        return message.multiple;
      });
    });

    function getTabset(id) {
      var $tabset = $("#" + $escape(id));
      if ($tabset.length === 0) throw "There is no tabsetPanel (or navbarPage or navlistPanel) " + "with id equal to '" + id + "'";
      return $tabset;
    }

    function getTabContent($tabset) {
      var tabsetId = $tabset.attr("data-tabsetid");
      var $tabContent = $("div.tab-content[data-tabsetid='" + $escape(tabsetId) + "']");
      return $tabContent;
    }

    function getTargetTabs($tabset, $tabContent, target) {
      var dataValue = "[data-value='" + $escape(target) + "']";
      var $aTag = $tabset.find("a" + dataValue);
      var $liTag = $aTag.parent();
      if ($liTag.length === 0) {
        throw "There is no tabPanel (or navbarMenu) with value" + " (or menuName) equal to '" + target + "'";
      }
      var $liTags = [];
      var $divTags = [];

      if ($aTag.attr("data-toggle") === "dropdown") {
        // dropdown
        var $dropdownTabset = $aTag.find("+ ul.dropdown-menu");
        var dropdownId = $dropdownTabset.attr("data-tabsetid");

        var $dropdownLiTags = $dropdownTabset.find("a[data-toggle='tab']").parent("li");
        $dropdownLiTags.each(function (i, el) {
          $liTags.push($(el));
        });
        var selector = "div.tab-pane[id^='tab-" + $escape(dropdownId) + "']";
        var $dropdownDivs = $tabContent.find(selector);
        $dropdownDivs.each(function (i, el) {
          $divTags.push($(el));
        });
      } else {
        // regular tab
        $divTags.push($tabContent.find("div" + dataValue));
      }
      return { $liTag: $liTag, $liTags: $liTags, $divTags: $divTags };
    }

    addMessageHandler("shiny-insert-tab", function (message) {
      var $parentTabset = getTabset(message.inputId);
      var $tabset = $parentTabset;
      var $tabContent = getTabContent($tabset);
      var tabsetId = $parentTabset.attr("data-tabsetid");

      var $divTag = $(message.divTag.html);
      var $liTag = $(message.liTag.html);
      var $aTag = $liTag.find("> a");

      // Unless the item is being prepended/appended, the target tab
      // must be provided
      var target = null;
      var $targetLiTag = null;
      if (message.target !== null) {
        target = getTargetTabs($tabset, $tabContent, message.target);
        $targetLiTag = target.$liTag;
      }

      // If the item is to be placed inside a navbarMenu (dropdown),
      // change the value of $tabset from the parent's ul tag to the
      // dropdown's ul tag
      var dropdown = getDropdown();
      if (dropdown !== null) {
        if ($aTag.attr("data-toggle") === "dropdown") throw "Cannot insert a navbarMenu inside another one";
        $tabset = dropdown.$tabset;
        tabsetId = dropdown.id;
      }

      // For regular tab items, fix the href (of the li > a tag)
      // and the id (of the div tag). This does not apply to plain
      // text items (which function as dividers and headers inside
      // navbarMenus) and whole navbarMenus (since those get
      // constructed from scratch on the R side and therefore
      // there are no ids that need matching)
      if ($aTag.attr("data-toggle") === "tab") {
        var index = getTabIndex($tabset, tabsetId);
        var tabId = "tab-" + tabsetId + "-" + index;
        $liTag.find("> a").attr("href", "#" + tabId);
        $divTag.attr("id", tabId);
      }

      // actually insert the item into the right place
      if (message.position === "before") {
        if ($targetLiTag) {
          $targetLiTag.before($liTag);
        } else {
          $tabset.append($liTag);
        }
      } else if (message.position === "after") {
        if ($targetLiTag) {
          $targetLiTag.after($liTag);
        } else {
          $tabset.prepend($liTag);
        }
      }

      exports.renderContent($liTag[0], { html: $liTag.html(), deps: message.liTag.deps });
      // jcheng 2017-07-28: This next part might look a little insane versus the
      // more obvious `$tabContent.append($divTag);`, but there's a method to the
      // madness.
      //
      // 1) We need to load the dependencies, and this needs to happen before
      //    any scripts in $divTag get a chance to run.
      // 2) The scripts in $divTag need to run only once.
      // 3) The contents of $divTag need to be sent through renderContent so that
      //    singletons may be registered and/or obeyed, and so that inputs/outputs
      //    may be bound.
      //
      // Add to these constraints these facts:
      //
      // A) The (non-jQuery) DOM manipulation functions don't cause scripts to
      //    run, but the jQuery functions all do.
      // B) renderContent must be called on an element that's attached to the
      //    document.
      // C) $divTag may be of length > 1 (e.g. navbarMenu). I also noticed text
      //    elements consisting of just "\n" being included in the nodeset of
      //    $divTag.
      // D) renderContent has a bug where only position "replace" (the default)
      //    uses the jQuery functions, so other positions like "beforeend" will
      //    prevent child script tags from running.
      //
      // In theory the same problem exists for $liTag but since that content is
      // much less likely to include arbitrary scripts, we're skipping it.
      //
      // This code could be nicer if we didn't use renderContent, but rather the
      // lower-level functions that renderContent uses. Like if we pre-process
      // the value of message.divTag.html for singletons, we could do that, then
      // render dependencies, then do $tabContent.append($divTag).
      exports.renderContent($tabContent[0], { html: "", deps: message.divTag.deps }, "beforeend");
      $divTag.get().forEach(function (el) {
        // Must not use jQuery for appending el to the doc, we don't want any
        // scripts to run (since they will run when renderContent takes a crack).
        $tabContent[0].appendChild(el);
        // If `el` itself is a script tag, this approach won't work (the script
        // won't be run), since we're only sending innerHTML through renderContent
        // and not the whole tag. That's fine in this case because we control the
        // R code that generates this HTML, and we know that the element is not
        // a script tag.
        exports.renderContent(el, el.innerHTML || el.textContent);
      });

      if (message.select) {
        $liTag.find("a").tab("show");
      }

      /* Barbara -- August 2017
      Note: until now, the number of tabs in a tabsetPanel (or navbarPage
      or navlistPanel) was always fixed. So, an easy way to give an id to
      a tab was simply incrementing a counter. (Just like it was easy to
      give a random 4-digit number to identify the tabsetPanel). Now that
      we're introducing dynamic tabs, we must retrieve these numbers and
      fix the dummy id given to the tab in the R side -- there, we always
      set the tab id (counter dummy) to "id" and the tabset id to "tsid")
      */
      function getTabIndex($tabset, tabsetId) {
        // The 0 is to ensure this works for empty tabsetPanels as well
        var existingTabIds = [0];
        // loop through all existing tabs, find the one with highest id
        // (since this is based on a numeric counter), and increment
        $tabset.find("> li").each(function () {
          var $tab = $(this).find("> a[data-toggle='tab']");
          if ($tab.length > 0) {
            // remove leading url if it exists. (copy of bootstrap url stripper)
            var href = $tab.attr("href").replace(/.*(?=#[^\s]+$)/, '');
            // remove tab id to get the index
            var index = href.replace("#tab-" + tabsetId + "-", "");
            existingTabIds.push(Number(index));
          }
        });
        return Math.max.apply(null, existingTabIds) + 1;
      }

      // Finds out if the item will be placed inside a navbarMenu
      // (dropdown). If so, returns the dropdown tabset (ul tag)
      // and the dropdown tabsetid (to be used to fix the tab ID)
      function getDropdown() {
        if (message.menuName !== null) {
          // menuName is only provided if the user wants to prepend
          // or append an item inside a navbarMenu (dropdown)
          var $dropdownATag = $("a.dropdown-toggle[data-value='" + $escape(message.menuName) + "']");
          if ($dropdownATag.length === 0) {
            throw "There is no navbarMenu with menuName equal to '" + message.menuName + "'";
          }
          var $dropdownTabset = $dropdownATag.find("+ ul.dropdown-menu");
          var dropdownId = $dropdownTabset.attr("data-tabsetid");
          return { $tabset: $dropdownTabset, id: dropdownId };
        } else if (message.target !== null) {
          // if our item is to be placed next to a tab that is inside
          // a navbarMenu, our item will also be inside
          var $uncleTabset = $targetLiTag.parent("ul");
          if ($uncleTabset.hasClass("dropdown-menu")) {
            var uncleId = $uncleTabset.attr("data-tabsetid");
            return { $tabset: $uncleTabset, id: uncleId };
          }
        }
        return null;
      }
    });

    // If the given tabset has no active tabs, select the first one
    function ensureTabsetHasVisibleTab($tabset) {
      if ($tabset.find("li.active").not(".dropdown").length === 0) {
        // Note: destTabValue may be null. We still want to proceed
        // through the below logic and setValue so that the input
        // value for the tabset gets updated (i.e. input$tabsetId
        // should be null if there are no tabs).
        var destTabValue = getFirstTab($tabset);
        var inputBinding = $tabset.data('shiny-input-binding');
        var evt = jQuery.Event('shiny:updateinput');
        evt.binding = inputBinding;
        $tabset.trigger(evt);
        inputBinding.setValue($tabset[0], destTabValue);
      }
    }

    // Given a tabset ul jquery object, return the value of the first tab
    // (in document order) that's visible and able to be selected.
    function getFirstTab($ul) {
      return $ul.find("li:visible a[data-toggle='tab']").first().attr("data-value") || null;
    }

    function tabApplyFunction(target, func) {
      var liTags = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;

      $.each(target, function (key, el) {
        if (key === "$liTag") {
          // $liTag is always just one jQuery element
          func(el);
        } else if (key === "$divTags") {
          // $divTags is always an array (even if length = 1)
          $.each(el, function (i, div) {
            func(div);
          });
        } else if (liTags && key === "$liTags") {
          // $liTags is always an array (even if length = 0)
          $.each(el, function (i, div) {
            func(div);
          });
        }
      });
    }

    addMessageHandler("shiny-remove-tab", function (message) {
      var $tabset = getTabset(message.inputId);
      var $tabContent = getTabContent($tabset);
      var target = getTargetTabs($tabset, $tabContent, message.target);

      tabApplyFunction(target, removeEl);

      ensureTabsetHasVisibleTab($tabset);

      function removeEl($el) {
        exports.unbindAll($el, true);
        $el.remove();
      }
    });

    addMessageHandler("shiny-change-tab-visibility", function (message) {
      var $tabset = getTabset(message.inputId);
      var $tabContent = getTabContent($tabset);
      var target = getTargetTabs($tabset, $tabContent, message.target);

      tabApplyFunction(target, changeVisibility, true);

      ensureTabsetHasVisibleTab($tabset);

      function changeVisibility($el) {
        if (message.type === "show") $el.css("display", "");else if (message.type === "hide") {
          $el.hide();
          $el.removeClass("active");
        }
      }
    });

    addMessageHandler('updateQueryString', function (message) {

      // leave the bookmarking code intact
      if (message.mode === "replace") {
        window.history.replaceState(null, null, message.queryString);
        return;
      }

      var what = null;
      if (message.queryString.charAt(0) === "#") what = "hash";else if (message.queryString.charAt(0) === "?") what = "query";else throw "The 'query' string must start with either '?' " + "(to update the query string) or with '#' (to " + "update the hash).";

      var path = window.location.pathname;
      var oldQS = window.location.search;
      var oldHash = window.location.hash;

      /* Barbara -- December 2016
      Note: we could check if the new QS and/or hash are different
      from the old one(s) and, if not, we could choose not to push
      a new state (whether or not we would replace it is moot/
      inconsequential). However, I think that it is better to
      interpret each call to `updateQueryString` as representing
      new state (even if the message.queryString is the same), so
      that check isn't even performed as of right now.
      */

      var relURL = path;
      if (what === "query") relURL += message.queryString;else relURL += oldQS + message.queryString; // leave old QS if it exists
      window.history.pushState(null, null, relURL);

      // for the case when message.queryString has both a query string
      // and a hash (`what = "hash"` allows us to trigger the
      // hashchange event)
      if (message.queryString.indexOf("#") !== -1) what = "hash";

      // for the case when there was a hash before, but there isn't
      // any hash now (e.g. for when only the query string is updated)
      if (window.location.hash !== oldHash) what = "hash";

      // This event needs to be triggered manually because pushState() never
      // causes a hashchange event to be fired,
      if (what === "hash") $(document).trigger("hashchange");
    });

    addMessageHandler("resetBrush", function (message) {
      exports.resetBrush(message.brushId);
    });

    // Progress reporting ====================================================

    var progressHandlers = {
      // Progress for a particular object
      binding: function binding(message) {
        var key = message.id;
        var binding = this.$bindings[key];
        if (binding) {
          $(binding.el).trigger({
            type: 'shiny:outputinvalidated',
            binding: binding,
            name: key
          });
          if (binding.showProgress) binding.showProgress(true);
        }
      },

      // Open a page-level progress bar
      open: function open(message) {
        if (message.style === "notification") {
          // For new-style (starting in Shiny 0.14) progress indicators that use
          // the notification API.

          // Progress bar starts hidden; will be made visible if a value is provided
          // during updates.
          exports.notifications.show({
            html: "<div id=\"shiny-progress-" + message.id + "\" class=\"shiny-progress-notification\">" + '<div class="progress progress-striped active" style="display: none;"><div class="progress-bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span> ' + '<span class="progress-detail"></span>' + '</div>' + '</div>',
            id: message.id,
            duration: null
          });
        } else if (message.style === "old") {
          // For old-style (Shiny <=0.13.2) progress indicators.

          // Add progress container (for all progress items) if not already present
          var $container = $('.shiny-progress-container');
          if ($container.length === 0) {
            $container = $('<div class="shiny-progress-container"></div>');
            $(document.body).append($container);
          }

          // Add div for just this progress ID
          var depth = $('.shiny-progress.open').length;
          // The 'bar' class is needed for backward compatibility with Bootstrap 2.
          var $progress = $('<div class="shiny-progress open">' + '<div class="progress progress-striped active"><div class="progress-bar bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span>' + '<span class="progress-detail"></span>' + '</div>' + '</div>');

          $progress.attr('id', message.id);
          $container.append($progress);

          // Stack bars
          var $progressBar = $progress.find('.progress');
          $progressBar.css('top', depth * $progressBar.height() + 'px');

          // Stack text objects
          var $progressText = $progress.find('.progress-text');
          $progressText.css('top', 3 * $progressBar.height() + depth * $progressText.outerHeight() + 'px');

          $progress.hide();
        }
      },

      // Update page-level progress bar
      update: function update(message) {
        if (message.style === "notification") {
          // For new-style (starting in Shiny 0.14) progress indicators that use
          // the notification API.
          var $progress = $('#shiny-progress-' + message.id);

          if ($progress.length === 0) return;

          if (typeof message.message !== 'undefined') {
            $progress.find('.progress-message').text(message.message);
          }
          if (typeof message.detail !== 'undefined') {
            $progress.find('.progress-detail').text(message.detail);
          }
          if (typeof message.value !== 'undefined' && message.value !== null) {
            $progress.find('.progress').show();
            $progress.find('.progress-bar').width(message.value * 100 + '%');
          }
        } else if (message.style === "old") {
          // For old-style (Shiny <=0.13.2) progress indicators.

          var $progress = $('#' + message.id + '.shiny-progress');
          if (typeof message.message !== 'undefined') {
            $progress.find('.progress-message').text(message.message);
          }
          if (typeof message.detail !== 'undefined') {
            $progress.find('.progress-detail').text(message.detail);
          }
          if (typeof message.value !== 'undefined' && message.value !== null) {
            $progress.find('.progress').show();
            $progress.find('.bar').width(message.value * 100 + '%');
          }

          $progress.fadeIn();
        }
      },

      // Close page-level progress bar
      close: function close(message) {
        if (message.style === "notification") {
          exports.notifications.remove(message.id);
        } else if (message.style === "old") {
          var $progress = $('#' + message.id + '.shiny-progress');
          $progress.removeClass('open');

          $progress.fadeOut({
            complete: function complete() {
              $progress.remove();

              // If this was the last shiny-progress, remove container
              if ($('.shiny-progress').length === 0) $('.shiny-progress-container').remove();
            }
          });
        }
      }
    };

    exports.progressHandlers = progressHandlers;

    // Returns a URL which can be queried to get values from inside the server
    // function. This is enabled with `options(shiny.testmode=TRUE)`.
    this.getTestSnapshotBaseUrl = function () {
      var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
          _ref2$fullUrl = _ref2.fullUrl,
          fullUrl = _ref2$fullUrl === undefined ? true : _ref2$fullUrl;

      var loc = window.location;
      var url = "";

      if (fullUrl) {
        // Strip off everything after last slash in path, like dirname() in R
        url = loc.origin + loc.pathname.replace(/\/[^/]*$/, "");
      }
      url += "/session/" + encodeURIComponent(this.config.sessionId) + "/dataobj/shinytest?w=" + encodeURIComponent(this.config.workerId) + "&nonce=" + randomId();

      return url;
    };
  }).call(ShinyApp.prototype);

  exports.showReconnectDialog = function () {
    var reconnectTime = null;

    function updateTime() {
      var $time = $("#shiny-reconnect-time");
      // If the time has been removed, exit and don't reschedule this function.
      if ($time.length === 0) return;

      var seconds = Math.floor((reconnectTime - new Date().getTime()) / 1000);
      if (seconds > 0) {
        $time.text(" in " + seconds + "s");
      } else {
        $time.text("...");
      }

      // Reschedule this function after 1 second
      setTimeout(updateTime, 1000);
    }

    return function (delay) {
      reconnectTime = new Date().getTime() + delay;

      // If there's already a reconnect dialog, don't add another
      if ($('#shiny-reconnect-text').length > 0) return;

      var html = '<span id="shiny-reconnect-text">Attempting to reconnect</span>' + '<span id="shiny-reconnect-time"></span>';
      var action = '<a id="shiny-reconnect-now" href="#" onclick="Shiny.shinyapp.reconnect();">Try now</a>';

      exports.notifications.show({
        id: "reconnect",
        html: html,
        action: action,
        duration: null,
        closeButton: false,
        type: 'warning'
      });

      updateTime();
    };
  }();

  exports.hideReconnectDialog = function () {
    exports.notifications.remove("reconnect");
  };

  //---------------------------------------------------------------------
  // Source file: ../srcjs/notifications.js

  exports.notifications = function () {

    // Milliseconds to fade in or out
    var fadeDuration = 250;

    function show() {
      var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
          _ref3$html = _ref3.html,
          html = _ref3$html === undefined ? '' : _ref3$html,
          _ref3$action = _ref3.action,
          action = _ref3$action === undefined ? '' : _ref3$action,
          _ref3$deps = _ref3.deps,
          deps = _ref3$deps === undefined ? [] : _ref3$deps,
          _ref3$duration = _ref3.duration,
          duration = _ref3$duration === undefined ? 5000 : _ref3$duration,
          _ref3$id = _ref3.id,
          id = _ref3$id === undefined ? null : _ref3$id,
          _ref3$closeButton = _ref3.closeButton,
          closeButton = _ref3$closeButton === undefined ? true : _ref3$closeButton,
          _ref3$type = _ref3.type,
          type = _ref3$type === undefined ? null : _ref3$type;

      if (!id) id = randomId();

      // Create panel if necessary
      _createPanel();

      // Get existing DOM element for this ID, or create if needed.
      var $notification = _get(id);
      if ($notification.length === 0) $notification = _create(id);

      // Render html and dependencies
      var newHtml = "<div class=\"shiny-notification-content-text\">" + html + "</div>" + ("<div class=\"shiny-notification-content-action\">" + action + "</div>");
      var $content = $notification.find('.shiny-notification-content');
      exports.renderContent($content, { html: newHtml, deps: deps });

      // Remove any existing classes of the form 'shiny-notification-xxxx'.
      // The xxxx would be strings like 'warning'.
      var classes = $notification.attr('class').split(/\s+/).filter(function (cls) {
        return cls.match(/^shiny-notification-/);
      }).join(' ');
      $notification.removeClass(classes);

      // Add class. 'default' means no additional CSS class.
      if (type && type !== 'default') $notification.addClass('shiny-notification-' + type);

      // Make sure that the presence/absence of close button matches with value
      // of `closeButton`.
      var $close = $notification.find('.shiny-notification-close');
      if (closeButton && $close.length === 0) {
        $notification.append('<div class="shiny-notification-close">&times;</div>');
      } else if (!closeButton && $close.length !== 0) {
        $close.remove();
      }

      // If duration was provided, schedule removal. If not, clear existing
      // removal callback (this happens if a message was first added with
      // a duration, and then updated with no duration).
      if (duration) _addRemovalCallback(id, duration);else _clearRemovalCallback(id);

      return id;
    }

    function remove(id) {
      _get(id).fadeOut(fadeDuration, function () {

        exports.unbindAll(this);
        $(this).remove();

        // If no more notifications, remove the panel from the DOM.
        if (_ids().length === 0) {
          _getPanel().remove();
        }
      });
    }

    // Returns an individual notification DOM object (wrapped in jQuery).
    function _get(id) {
      if (!id) return null;
      return _getPanel().find('#shiny-notification-' + $escape(id));
    }

    // Return array of all notification IDs
    function _ids() {
      return _getPanel().find('.shiny-notification').map(function () {
        return this.id.replace(/shiny-notification-/, '');
      }).get();
    }

    // Returns the notification panel DOM object (wrapped in jQuery).
    function _getPanel() {
      return $('#shiny-notification-panel');
    }

    // Create notifications panel and return the jQuery object. If the DOM
    // element already exists, just return it.
    function _createPanel() {
      var $panel = _getPanel();

      if ($panel.length > 0) return $panel;

      $(document.body).append('<div id="shiny-notification-panel">');

      return $panel;
    }

    // Create a notification DOM element and return the jQuery object. If the
    // DOM element already exists for the ID, just return it without creating.
    function _create(id) {
      var $notification = _get(id);

      if ($notification.length === 0) {
        $notification = $("<div id=\"shiny-notification-" + id + "\" class=\"shiny-notification\">" + '<div class="shiny-notification-close">&times;</div>' + '<div class="shiny-notification-content"></div>' + '</div>');

        $notification.find('.shiny-notification-close').on('click', function (e) {
          e.preventDefault();
          e.stopPropagation();
          remove(id);
        });

        _getPanel().append($notification);
      }

      return $notification;
    }

    // Add a callback to remove a notification after a delay in ms.
    function _addRemovalCallback(id, delay) {
      // If there's an existing removalCallback, clear it before adding the new
      // one.
      _clearRemovalCallback(id);

      // Attach new removal callback
      var removalCallback = setTimeout(function () {
        remove(id);
      }, delay);
      _get(id).data('removalCallback', removalCallback);
    }

    // Clear a removal callback from a notification, if present.
    function _clearRemovalCallback(id) {
      var $notification = _get(id);
      var oldRemovalCallback = $notification.data('removalCallback');
      if (oldRemovalCallback) {
        clearTimeout(oldRemovalCallback);
      }
    }

    return {
      show: show,
      remove: remove
    };
  }();

  //---------------------------------------------------------------------
  // Source file: ../srcjs/modal.js

  exports.modal = {

    // Show a modal dialog. This is meant to handle two types of cases: one is
    // that the content is a Bootstrap modal dialog, and the other is that the
    // content is non-Bootstrap. Bootstrap modals require some special handling,
    // which is coded in here.
    show: function show() {
      var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
          _ref4$html = _ref4.html,
          html = _ref4$html === undefined ? '' : _ref4$html,
          _ref4$deps = _ref4.deps,
          deps = _ref4$deps === undefined ? [] : _ref4$deps;

      // If there was an existing Bootstrap modal, then there will be a modal-
      // backdrop div that was added outside of the modal wrapper, and it must be
      // removed; otherwise there can be multiple of these divs.
      $('.modal-backdrop').remove();

      // Get existing wrapper DOM element, or create if needed.
      var $modal = $('#shiny-modal-wrapper');
      if ($modal.length === 0) {
        $modal = $('<div id="shiny-modal-wrapper"></div>');
        $(document.body).append($modal);

        // If the wrapper's content is a Bootstrap modal, then when the inner
        // modal is hidden, remove the entire thing, including wrapper.
        $modal.on('hidden.bs.modal', function (e) {
          if (e.target === $("#shiny-modal")[0]) {
            exports.unbindAll($modal);
            $modal.remove();
          }
        });
      }

      $modal.on('keydown.shinymodal', function (e) {
        // If we're listening for Esc, don't let the event propagate. See
        // https://github.com/rstudio/shiny/issues/1453. The value of
        // data("keyboard") needs to be checked inside the handler, because at
        // the time that $modal.on() is called, the $("#shiny-modal") div doesn't
        // yet exist.
        if ($("#shiny-modal").data("keyboard") === false) return;

        if (e.keyCode === 27) {
          e.stopPropagation();
          e.preventDefault();
        }
      });

      // Set/replace contents of wrapper with html.
      exports.renderContent($modal, { html: html, deps: deps });
    },

    remove: function remove() {
      var $modal = $('#shiny-modal-wrapper');

      $modal.off('keydown.shinymodal');

      // Look for a Bootstrap modal and if present, trigger hide event. This will
      // trigger the hidden.bs.modal callback that we set in show(), which unbinds
      // and removes the element.
      if ($modal.find('.modal').length > 0) {
        $modal.find('.modal').modal('hide');
      } else {
        // If not a Bootstrap modal dialog, simply unbind and remove it.
        exports.unbindAll($modal);
        $modal.remove();
      }
    }
  };

  //---------------------------------------------------------------------
  // Source file: ../srcjs/file_processor.js

  // Generic driver class for doing chunk-wise asynchronous processing of a
  // FileList object. Subclass/clone it and override the `on*` functions to
  // make it do something useful.
  var FileProcessor = function FileProcessor(files) {
    this.files = files;
    this.fileIndex = -1;
    // Currently need to use small chunk size because R-Websockets can't
    // handle continuation frames
    this.aborted = false;
    this.completed = false;

    // TODO: Register error/abort callbacks

    this.$run();
  };
  (function () {
    // Begin callbacks. Subclassers/cloners may override any or all of these.
    this.onBegin = function (files, cont) {
      setTimeout(cont, 0);
    };
    this.onFile = function (file, cont) {
      setTimeout(cont, 0);
    };
    this.onComplete = function () {};
    this.onAbort = function () {};
    // End callbacks

    // Aborts processing, unless it's already completed
    this.abort = function () {
      if (this.completed || this.aborted) return;

      this.aborted = true;
      this.onAbort();
    };

    // Returns a bound function that will call this.$run one time.
    this.$getRun = function () {
      var self = this;
      var called = false;
      return function () {
        if (called) return;
        called = true;
        self.$run();
      };
    };

    // This function will be called multiple times to advance the process.
    // It relies on the state of the object's fields to know what to do next.
    this.$run = function () {

      if (this.aborted || this.completed) return;

      if (this.fileIndex < 0) {
        // Haven't started yet--begin
        this.fileIndex = 0;
        this.onBegin(this.files, this.$getRun());
        return;
      }

      if (this.fileIndex === this.files.length) {
        // Just ended
        this.completed = true;
        this.onComplete();
        return;
      }

      // If we got here, then we have a file to process, or we are
      // in the middle of processing a file, or have just finished
      // processing a file.

      var file = this.files[this.fileIndex++];
      this.onFile(file, this.$getRun());
    };
  }).call(FileProcessor.prototype);

  //---------------------------------------------------------------------
  // Source file: ../srcjs/binding_registry.js

  var BindingRegistry = function BindingRegistry() {
    this.bindings = [];
    this.bindingNames = {};
  };
  (function () {
    this.register = function (binding, bindingName, priority) {
      var bindingObj = { binding: binding, priority: priority || 0 };
      this.bindings.unshift(bindingObj);
      if (bindingName) {
        this.bindingNames[bindingName] = bindingObj;
        binding.name = bindingName;
      }
    };
    this.setPriority = function (bindingName, priority) {
      var bindingObj = this.bindingNames[bindingName];
      if (!bindingObj) throw "Tried to set priority on unknown binding " + bindingName;
      bindingObj.priority = priority || 0;
    };
    this.getPriority = function (bindingName) {
      var bindingObj = this.bindingNames[bindingName];
      if (!bindingObj) return false;
      return bindingObj.priority;
    };
    this.getBindings = function () {
      // Sort the bindings. The ones with higher priority are consulted
      // first; ties are broken by most-recently-registered.
      return mergeSort(this.bindings, function (a, b) {
        return b.priority - a.priority;
      });
    };
  }).call(BindingRegistry.prototype);

  var inputBindings = exports.inputBindings = new BindingRegistry();
  var outputBindings = exports.outputBindings = new BindingRegistry();

  //---------------------------------------------------------------------
  // Source file: ../srcjs/output_binding.js

  var OutputBinding = exports.OutputBinding = function () {};
  (function () {
    // Returns a jQuery object or element array that contains the
    // descendants of scope that match this binding
    this.find = function (scope) {
      throw "Not implemented";
    };

    this.getId = function (el) {
      return el['data-input-id'] || el.id;
    };

    this.onValueChange = function (el, data) {
      this.clearError(el);
      this.renderValue(el, data);
    };
    this.onValueError = function (el, err) {
      this.renderError(el, err);
    };
    this.renderError = function (el, err) {
      this.clearError(el);
      if (err.message === '') {
        // not really error, but we just need to wait (e.g. action buttons)
        $(el).empty();
        return;
      }
      var errClass = 'shiny-output-error';
      if (err.type !== null) {
        // use the classes of the error condition as CSS class names
        errClass = errClass + ' ' + $.map(asArray(err.type), function (type) {
          return errClass + '-' + type;
        }).join(' ');
      }
      $(el).addClass(errClass).text(err.message);
    };
    this.clearError = function (el) {
      $(el).attr('class', function (i, c) {
        return c.replace(/(^|\s)shiny-output-error\S*/g, '');
      });
    };
    this.showProgress = function (el, show) {
      var RECALC_CLASS = 'recalculating';
      if (show) $(el).addClass(RECALC_CLASS);else $(el).removeClass(RECALC_CLASS);
    };
  }).call(OutputBinding.prototype);

  //---------------------------------------------------------------------
  // Source file: ../srcjs/output_binding_text.js

  var textOutputBinding = new OutputBinding();
  $.extend(textOutputBinding, {
    find: function find(scope) {
      return $(scope).find('.shiny-text-output');
    },
    renderValue: function renderValue(el, data) {
      $(el).text(data);
    }
  });
  outputBindings.register(textOutputBinding, 'shiny.textOutput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/output_binding_image.js

  var imageOutputBinding = new OutputBinding();
  $.extend(imageOutputBinding, {
    find: function find(scope) {
      return $(scope).find('.shiny-image-output, .shiny-plot-output');
    },
    renderValue: function renderValue(el, data) {
      // The overall strategy:
      // * Clear out existing image and event handlers.
      // * Create new image.
      // * Create various event handlers.
      // * Bind those event handlers to events.
      // * Insert the new image.

      var outputId = this.getId(el);

      var $el = $(el);
      var img;

      // Get existing img element if present.
      var $img = $el.find('img');

      if ($img.length === 0) {
        // If a img element is not already present, that means this is either
        // the first time renderValue() has been called, or this is after an
        // error.
        img = document.createElement('img');
        $el.append(img);
        $img = $(img);
      } else {
        // Trigger custom 'reset' event for any existing images in the div
        img = $img[0];
        $img.trigger('reset');
      }

      if (!data) {
        $el.empty();
        return;
      }

      // If value is undefined, return alternate. Sort of like ||, except it won't
      // return alternate for other falsy values (0, false, null).
      function OR(value, alternate) {
        if (value === undefined) return alternate;
        return value;
      }

      var opts = {
        clickId: $el.data('click-id'),
        clickClip: OR(strToBool($el.data('click-clip')), true),

        dblclickId: $el.data('dblclick-id'),
        dblclickClip: OR(strToBool($el.data('dblclick-clip')), true),
        dblclickDelay: OR($el.data('dblclick-delay'), 400),

        hoverId: $el.data('hover-id'),
        hoverClip: OR(strToBool($el.data('hover-clip')), true),
        hoverDelayType: OR($el.data('hover-delay-type'), 'debounce'),
        hoverDelay: OR($el.data('hover-delay'), 300),
        hoverNullOutside: OR(strToBool($el.data('hover-null-outside')), false),

        brushId: $el.data('brush-id'),
        brushClip: OR(strToBool($el.data('brush-clip')), true),
        brushDelayType: OR($el.data('brush-delay-type'), 'debounce'),
        brushDelay: OR($el.data('brush-delay'), 300),
        brushFill: OR($el.data('brush-fill'), '#666'),
        brushStroke: OR($el.data('brush-stroke'), '#000'),
        brushOpacity: OR($el.data('brush-opacity'), 0.3),
        brushDirection: OR($el.data('brush-direction'), 'xy'),
        brushResetOnNew: OR(strToBool($el.data('brush-reset-on-new')), false),

        coordmap: data.coordmap
      };

      // Copy items from data to img. Don't set the coordmap as an attribute.
      $.each(data, function (key, value) {
        if (value === null || key === 'coordmap') {
          return;
        }
        // this checks only against base64 encoded src values
        // images put here are only from renderImage and renderPlot
        if (key === "src" && value === img.getAttribute("src")) {
          // Ensure the browser actually fires an onLoad event, which doesn't
          // happen on WebKit if the value we set on src is the same as the
          // value it already has
          // https://github.com/rstudio/shiny/issues/2197
          // https://stackoverflow.com/questions/5024111/javascript-image-onload-doesnt-fire-in-webkit-if-loading-same-image
          img.removeAttribute("src");
        }
        img.setAttribute(key, value);
      });

      // Unset any attributes in the current img that were not provided in the
      // new data.
      for (var i = 0; i < img.attributes.length; i++) {
        var attrib = img.attributes[i];
        // Need to check attrib.specified on IE because img.attributes contains
        // all possible attributes on IE.
        if (attrib.specified && !data.hasOwnProperty(attrib.name)) {
          img.removeAttribute(attrib.name);
        }
      }

      if (!opts.coordmap) {
        opts.coordmap = {
          panels: [],
          dims: {
            // These values be set to the naturalWidth and naturalHeight once the image has loaded
            height: null,
            width: null
          }
        };
      }

      // Remove event handlers that were added in previous runs of this function.
      $el.off('.image_output');
      $img.off('.image_output');

      // When the image loads, initialize all the interaction handlers. When the
      // value of src is set, the browser may not load the image immediately,
      // even if it's a data URL. If we try to initialize this stuff
      // immediately, it can cause problems because we use we need the raw image
      // height and width
      $img.off("load.shiny_image_interaction");
      $img.one("load.shiny_image_interaction", function () {

        imageutils.initCoordmap($el, opts.coordmap);

        // This object listens for mousedowns, and triggers mousedown2 and dblclick2
        // events as appropriate.
        var clickInfo = imageutils.createClickInfo($el, opts.dblclickId, opts.dblclickDelay);

        $el.on('mousedown.image_output', clickInfo.mousedown);

        if (browser.isIE && browser.IEVersion === 8) {
          $el.on('dblclick.image_output', clickInfo.dblclickIE8);
        }

        // ----------------------------------------------------------
        // Register the various event handlers
        // ----------------------------------------------------------
        if (opts.clickId) {
          imageutils.disableDrag($el, $img);

          var clickHandler = imageutils.createClickHandler(opts.clickId, opts.clickClip, opts.coordmap);
          $el.on('mousedown2.image_output', clickHandler.mousedown);

          $el.on('resize.image_output', clickHandler.onResize);

          // When img is reset, do housekeeping: clear $el's mouse listener and
          // call the handler's onResetImg callback.
          $img.on('reset.image_output', clickHandler.onResetImg);
        }

        if (opts.dblclickId) {
          imageutils.disableDrag($el, $img);

          // We'll use the clickHandler's mousedown function, but register it to
          // our custom 'dblclick2' event.
          var dblclickHandler = imageutils.createClickHandler(opts.dblclickId, opts.clickClip, opts.coordmap);
          $el.on('dblclick2.image_output', dblclickHandler.mousedown);

          $el.on('resize.image_output', dblclickHandler.onResize);
          $img.on('reset.image_output', dblclickHandler.onResetImg);
        }

        if (opts.hoverId) {
          imageutils.disableDrag($el, $img);

          var hoverHandler = imageutils.createHoverHandler(opts.hoverId, opts.hoverDelay, opts.hoverDelayType, opts.hoverClip, opts.hoverNullOutside, opts.coordmap);
          $el.on('mousemove.image_output', hoverHandler.mousemove);
          $el.on('mouseout.image_output', hoverHandler.mouseout);

          $el.on('resize.image_output', hoverHandler.onResize);
          $img.on('reset.image_output', hoverHandler.onResetImg);
        }

        if (opts.brushId) {
          imageutils.disableDrag($el, $img);

          var brushHandler = imageutils.createBrushHandler(opts.brushId, $el, opts, opts.coordmap, outputId);
          $el.on('mousedown.image_output', brushHandler.mousedown);
          $el.on('mousemove.image_output', brushHandler.mousemove);

          $el.on('resize.image_output', brushHandler.onResize);
          $img.on('reset.image_output', brushHandler.onResetImg);
        }

        if (opts.clickId || opts.dblclickId || opts.hoverId || opts.brushId) {
          $el.addClass('crosshair');
        }

        if (data.error) console.log('Error on server extracting coordmap: ' + data.error);
      });
    },

    renderError: function renderError(el, err) {
      $(el).find('img').trigger('reset');
      OutputBinding.prototype.renderError.call(this, el, err);
    },

    clearError: function clearError(el) {
      // Remove all elements except img and the brush; this is usually just
      // error messages.
      $(el).contents().filter(function () {
        return this.tagName !== "IMG" && this.id !== el.id + '_brush';
      }).remove();

      OutputBinding.prototype.clearError.call(this, el);
    },

    resize: function resize(el, width, height) {
      $(el).find("img").trigger("resize");
    }
  });
  outputBindings.register(imageOutputBinding, 'shiny.imageOutput');

  var imageutils = {};

  imageutils.disableDrag = function ($el, $img) {
    // Make image non-draggable (Chrome, Safari)
    $img.css('-webkit-user-drag', 'none');

    // Firefox, IE<=10
    // First remove existing handler so we don't keep adding handlers.
    $img.off('dragstart.image_output');
    $img.on('dragstart.image_output', function () {
      return false;
    });

    // Disable selection of image and text when dragging in IE<=10
    $el.off('selectstart.image_output');
    $el.on('selectstart.image_output', function () {
      return false;
    });
  };

  // Modifies the panel objects in a coordmap, adding scaleImgToData(),
  // scaleDataToImg(), and clipImg() functions to each one. The panel objects
  // use img and data coordinates only; they do not use css coordinates. The
  // domain is in data coordinates; the range is in img coordinates.
  imageutils.initPanelScales = function (panels) {
    // Map a value x from a domain to a range. If clip is true, clip it to the
    // range.
    function mapLinear(x, domainMin, domainMax, rangeMin, rangeMax, clip) {
      // By default, clip to range
      clip = clip || true;

      var factor = (rangeMax - rangeMin) / (domainMax - domainMin);
      var val = x - domainMin;
      var newval = val * factor + rangeMin;

      if (clip) {
        var max = Math.max(rangeMax, rangeMin);
        var min = Math.min(rangeMax, rangeMin);
        if (newval > max) newval = max;else if (newval < min) newval = min;
      }
      return newval;
    }

    // Create scale and inverse-scale functions for a single direction (x or y).
    function scaler1D(domainMin, domainMax, rangeMin, rangeMax, logbase) {
      return {
        scale: function scale(val, clip) {
          if (logbase) val = Math.log(val) / Math.log(logbase);
          return mapLinear(val, domainMin, domainMax, rangeMin, rangeMax, clip);
        },

        scaleInv: function scaleInv(val, clip) {
          var res = mapLinear(val, rangeMin, rangeMax, domainMin, domainMax, clip);
          if (logbase) res = Math.pow(logbase, res);
          return res;
        }
      };
    }

    // Modify panel, adding scale and inverse-scale functions that take objects
    // like {x:1, y:3}, and also add clip function.
    function addScaleFuns(panel) {
      var d = panel.domain;
      var r = panel.range;
      var xlog = panel.log && panel.log.x ? panel.log.x : null;
      var ylog = panel.log && panel.log.y ? panel.log.y : null;
      var xscaler = scaler1D(d.left, d.right, r.left, r.right, xlog);
      var yscaler = scaler1D(d.bottom, d.top, r.bottom, r.top, ylog);

      // Given an object of form {x:1, y:2}, or {x:1, xmin:2:, ymax: 3}, convert
      // from data coordinates to img. Whether a value is converted as x or y
      // depends on the first character of the key.
      panel.scaleDataToImg = function (val, clip) {
        return mapValues(val, function (value, key) {
          var prefix = key.substring(0, 1);
          if (prefix === "x") {
            return xscaler.scale(value, clip);
          } else if (prefix === "y") {
            return yscaler.scale(value, clip);
          }
          return null;
        });
      };

      panel.scaleImgToData = function (val, clip) {
        return mapValues(val, function (value, key) {
          var prefix = key.substring(0, 1);
          if (prefix === "x") {
            return xscaler.scaleInv(value, clip);
          } else if (prefix === "y") {
            return yscaler.scaleInv(value, clip);
          }
          return null;
        });
      };

      // Given a scaled offset (in img pixels), clip it to the nearest panel region.
      panel.clipImg = function (offset_img) {
        var newOffset = {
          x: offset_img.x,
          y: offset_img.y
        };

        var bounds = panel.range;

        if (offset_img.x > bounds.right) newOffset.x = bounds.right;else if (offset_img.x < bounds.left) newOffset.x = bounds.left;

        if (offset_img.y > bounds.bottom) newOffset.y = bounds.bottom;else if (offset_img.y < bounds.top) newOffset.y = bounds.top;

        return newOffset;
      };
    }

    // Add the functions to each panel object.
    for (var i = 0; i < panels.length; i++) {
      var panel = panels[i];
      addScaleFuns(panel);
    }
  };

  // This adds functions to the coordmap object to handle various
  // coordinate-mapping tasks, and send information to the server. The input
  // coordmap is an array of objects, each of which represents a panel. coordmap
  // must be an array, even if empty, so that it can be modified in place; when
  // empty, we add a dummy panel to the array. It also calls initPanelScales,
  // which modifies each panel object to have scaleImgToData, scaleDataToImg,
  // and clip functions.
  //
  // There are three coordinate spaces which we need to translate between:
  //
  // 1. css: The pixel coordinates in the web browser, also known as CSS pixels.
  //    The origin is the upper-left corner of the <img> (not including padding
  //    and border).
  // 2. img: The pixel coordinates of the image data. A common case is on a
  //    HiDPI device, where the source PNG image could be 1000 pixels wide but
  //    be displayed in 500 CSS pixels. Another case is when the image has
  //    additional scaling due to CSS transforms or width.
  // 3. data: The coordinates in the data space. This is a bit more complicated
  //    than the other two, because there can be multiple panels (as in facets).
  imageutils.initCoordmap = function ($el, coordmap) {
    var $img = $el.find("img");
    var img = $img[0];

    // If we didn't get any panels, create a dummy one where the domain and range
    // are simply the pixel dimensions.
    // that we modify.
    if (coordmap.panels.length === 0) {
      var bounds = {
        top: 0,
        left: 0,
        right: img.clientWidth - 1,
        bottom: img.clientHeight - 1
      };

      coordmap.panels[0] = {
        domain: bounds,
        range: bounds,
        mapping: {}
      };
    }

    // If no dim height and width values are found, set them to the raw image height and width
    // These values should be the same...
    // This is only done to initialize an image output, whose height and width are unknown until the image is retrieved
    coordmap.dims.height = coordmap.dims.height || img.naturalHeight;
    coordmap.dims.width = coordmap.dims.width || img.naturalWidth;

    // Add scaling functions to each panel
    imageutils.initPanelScales(coordmap.panels);

    // This returns the offset of the mouse in CSS pixels relative to the img,
    // but not including the  padding or border, if present.
    coordmap.mouseOffsetCss = function (mouseEvent) {
      var img_origin = findOrigin($img);

      // The offset of the mouse from the upper-left corner of the img, in
      // pixels.
      return {
        x: mouseEvent.pageX - img_origin.x,
        y: mouseEvent.pageY - img_origin.y
      };
    };

    // Given an offset in an img in CSS pixels, return the corresponding offset
    // in source image pixels. The offset_css can have properties like "x",
    // "xmin", "y", and "ymax" -- anything that starts with "x" and "y". If the
    // img content is 1000 pixels wide, but is scaled to 400 pixels on screen,
    // and the input is x:400, then this will return x:1000.
    coordmap.scaleCssToImg = function (offset_css) {
      var pixel_scaling = coordmap.imgToCssScalingRatio();

      var result = mapValues(offset_css, function (value, key) {
        var prefix = key.substring(0, 1);

        if (prefix === "x") {
          return offset_css[key] / pixel_scaling.x;
        } else if (prefix === "y") {
          return offset_css[key] / pixel_scaling.y;
        }
        return null;
      });

      return result;
    };

    // Given an offset in an img, in source image pixels, return the
    // corresponding offset in CSS pixels. If the img content is 1000 pixels
    // wide, but is scaled to 400 pixels on screen, and the input is x:1000,
    // then this will return x:400.
    coordmap.scaleImgToCss = function (offset_img) {
      var pixel_scaling = coordmap.imgToCssScalingRatio();

      var result = mapValues(offset_img, function (value, key) {
        var prefix = key.substring(0, 1);

        if (prefix === "x") {
          return offset_img[key] * pixel_scaling.x;
        } else if (prefix === "y") {
          return offset_img[key] * pixel_scaling.y;
        }
        return null;
      });

      return result;
    };

    // Returns the x and y ratio the image content is scaled to on screen. If
    // the image data is 1000 pixels wide and is scaled to 300 pixels on screen,
    // then this returns 0.3. (Note the 300 pixels refers to CSS pixels.)
    coordmap.imgToCssScalingRatio = function () {
      var img_dims = findDims($img);
      return {
        x: img_dims.x / coordmap.dims.width,
        y: img_dims.y / coordmap.dims.height
      };
    };

    coordmap.cssToImgScalingRatio = function () {
      var res = coordmap.imgToCssScalingRatio();
      return {
        x: 1 / res.x,
        y: 1 / res.y
      };
    };

    // Given an offset in css pixels, return an object representing which panel
    // it's in. The `expand` argument tells it to expand the panel area by that
    // many pixels. It's possible for an offset to be within more than one
    // panel, because of the `expand` value. If that's the case, find the
    // nearest panel.
    coordmap.getPanelCss = function (offset_css) {
      var expand = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

      var offset_img = coordmap.scaleCssToImg(offset_css);
      var x = offset_img.x;
      var y = offset_img.y;

      // Convert expand from css pixels to img pixels
      var cssToImgRatio = coordmap.cssToImgScalingRatio();
      var expand_img = {
        x: expand * cssToImgRatio.x,
        y: expand * cssToImgRatio.y
      };

      var matches = []; // Panels that match
      var dists = []; // Distance of offset to each matching panel
      var b = void 0;
      for (var i = 0; i < coordmap.panels.length; i++) {
        b = coordmap.panels[i].range;

        if (x <= b.right + expand_img.x && x >= b.left - expand_img.x && y <= b.bottom + expand_img.y && y >= b.top - expand_img.y) {
          matches.push(coordmap.panels[i]);

          // Find distance from edges for x and y
          var xdist = 0;
          var ydist = 0;
          if (x > b.right && x <= b.right + expand_img.x) {
            xdist = x - b.right;
          } else if (x < b.left && x >= b.left - expand_img.x) {
            xdist = x - b.left;
          }
          if (y > b.bottom && y <= b.bottom + expand_img.y) {
            ydist = y - b.bottom;
          } else if (y < b.top && y >= b.top - expand_img.y) {
            ydist = y - b.top;
          }

          // Cartesian distance
          dists.push(Math.sqrt(Math.pow(xdist, 2) + Math.pow(ydist, 2)));
        }
      }

      if (matches.length) {
        // Find shortest distance
        var min_dist = Math.min.apply(null, dists);
        for (i = 0; i < matches.length; i++) {
          if (dists[i] === min_dist) {
            return matches[i];
          }
        }
      }

      return null;
    };

    // Is an offset (in css pixels) in a panel? If supplied, `expand` tells us
    // to expand the panels by that many pixels in all directions.
    coordmap.isInPanelCss = function (offset_css) {
      var expand = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

      if (coordmap.getPanelCss(offset_css, expand)) return true;

      return false;
    };

    // Returns a function that sends mouse coordinates, scaled to data space.
    // If that function is passed a null event, it will send null.
    coordmap.mouseCoordinateSender = function (inputId, clip, nullOutside) {
      if (clip === undefined) clip = true;
      if (nullOutside === undefined) nullOutside = false;

      return function (e) {
        if (e === null) {
          exports.setInputValue(inputId, null);
          return;
        }
        var coords = {};
        var coords_css = coordmap.mouseOffsetCss(e);
        // If outside of plotting region
        if (!coordmap.isInPanelCss(coords_css)) {
          if (nullOutside) {
            exports.setInputValue(inputId, null);
            return;
          }
          if (clip) return;

          coords.coords_css = coords_css;
          coords.coords_img = coordmap.scaleCssToImg(coords_css);

          exports.setInputValue(inputId, coords, { priority: "event" });
          return;
        }
        var panel = coordmap.getPanelCss(coords_css);

        var coords_img = coordmap.scaleCssToImg(coords_css);
        var coords_data = panel.scaleImgToData(coords_img);
        coords.x = coords_data.x;
        coords.y = coords_data.y;
        coords.coords_css = coords_css;
        coords.coords_img = coords_img;

        coords.img_css_ratio = coordmap.cssToImgScalingRatio();

        // Add the panel (facet) variables, if present
        $.extend(coords, panel.panel_vars);

        // Add variable name mappings
        coords.mapping = panel.mapping;

        // Add scaling information
        coords.domain = panel.domain;
        coords.range = panel.range;
        coords.log = panel.log;

        exports.setInputValue(inputId, coords, { priority: "event" });
      };
    };
  };

  // Given two sets of x/y coordinates, return an object representing the min
  // and max x and y values. (This could be generalized to any number of
  // points).
  imageutils.findBox = function (offset1, offset2) {
    return {
      xmin: Math.min(offset1.x, offset2.x),
      xmax: Math.max(offset1.x, offset2.x),
      ymin: Math.min(offset1.y, offset2.y),
      ymax: Math.max(offset1.y, offset2.y)
    };
  };

  // Shift an array of values so that they are within a min and max. The vals
  // will be shifted so that they maintain the same spacing internally. If the
  // range in vals is larger than the range of min and max, the result might not
  // make sense.
  imageutils.shiftToRange = function (vals, min, max) {
    if (!(vals instanceof Array)) vals = [vals];

    var maxval = Math.max.apply(null, vals);
    var minval = Math.min.apply(null, vals);
    var shiftAmount = 0;
    if (maxval > max) {
      shiftAmount = max - maxval;
    } else if (minval < min) {
      shiftAmount = min - minval;
    }

    var newvals = [];
    for (var i = 0; i < vals.length; i++) {
      newvals[i] = vals[i] + shiftAmount;
    }
    return newvals;
  };

  // This object provides two public event listeners: mousedown, and
  // dblclickIE8.
  // We need to make sure that, when the image is listening for double-
  // clicks, that a double-click doesn't trigger two click events. We'll
  // trigger custom mousedown2 and dblclick2 events with this mousedown
  // listener.
  imageutils.createClickInfo = function ($el, dblclickId, dblclickDelay) {
    var clickTimer = null;
    var pending_e = null; // A pending mousedown2 event

    // Create a new event of type eventType (like 'mousedown2'), and trigger
    // it with the information stored in this.e.
    function triggerEvent(newEventType, e) {
      // Extract important info from e and construct a new event with type
      // eventType.
      var e2 = $.Event(newEventType, {
        which: e.which,
        pageX: e.pageX,
        pageY: e.pageY
      });

      $el.trigger(e2);
    }

    function triggerPendingMousedown2() {
      // It's possible that between the scheduling of a mousedown2 and the
      // time this callback is executed, someone else triggers a
      // mousedown2, so check for that.
      if (pending_e) {
        triggerEvent('mousedown2', pending_e);
        pending_e = null;
      }
    }

    // Set a timer to trigger a mousedown2 event, using information from the
    // last recorded mousdown event.
    function scheduleMousedown2(e) {
      pending_e = e;

      clickTimer = setTimeout(function () {
        triggerPendingMousedown2();
      }, dblclickDelay);
    }

    function mousedown(e) {
      // Listen for left mouse button only
      if (e.which !== 1) return;

      // If no dblclick listener, immediately trigger a mousedown2 event.
      if (!dblclickId) {
        triggerEvent('mousedown2', e);
        return;
      }

      // If there's a dblclick listener, make sure not to count this as a
      // click on the first mousedown; we need to wait for the dblclick
      // delay before we can be sure this click was a single-click.
      if (pending_e === null) {
        scheduleMousedown2(e);
      } else {
        clearTimeout(clickTimer);

        // If second click is too far away, it doesn't count as a double
        // click. Instead, immediately trigger a mousedown2 for the previous
        // click, and set this click as a new first click.
        if (pending_e && Math.abs(pending_e.pageX - e.pageX) > 2 || Math.abs(pending_e.pageY - e.pageY) > 2) {

          triggerPendingMousedown2();
          scheduleMousedown2(e);
        } else {
          // The second click was close to the first one. If it happened
          // within specified delay, trigger our custom 'dblclick2' event.
          pending_e = null;
          triggerEvent('dblclick2', e);
        }
      }
    }

    // IE8 needs a special hack because when you do a double-click it doesn't
    // trigger the click event twice - it directly triggers dblclick.
    function dblclickIE8(e) {
      e.which = 1; // In IE8, e.which is 0 instead of 1. ???
      triggerEvent('dblclick2', e);
    }

    return {
      mousedown: mousedown,
      dblclickIE8: dblclickIE8
    };
  };

  // ----------------------------------------------------------
  // Handler creators for click, hover, brush.
  // Each of these returns an object with a few public members. These public
  // members are callbacks that are meant to be bound to events on $el with
  // the same name (like 'mousedown').
  // ----------------------------------------------------------

  imageutils.createClickHandler = function (inputId, clip, coordmap) {
    var clickInfoSender = coordmap.mouseCoordinateSender(inputId, clip);

    return {
      mousedown: function mousedown(e) {
        // Listen for left mouse button only
        if (e.which !== 1) return;
        clickInfoSender(e);
      },
      onResetImg: function onResetImg() {
        clickInfoSender(null);
      },
      onResize: null
    };
  };

  imageutils.createHoverHandler = function (inputId, delay, delayType, clip, nullOutside, coordmap) {
    var sendHoverInfo = coordmap.mouseCoordinateSender(inputId, clip, nullOutside);

    var hoverInfoSender;
    if (delayType === 'throttle') hoverInfoSender = new Throttler(null, sendHoverInfo, delay);else hoverInfoSender = new Debouncer(null, sendHoverInfo, delay);

    // What to do when mouse exits the image
    var mouseout;
    if (nullOutside) mouseout = function mouseout() {
      hoverInfoSender.normalCall(null);
    };else mouseout = function mouseout() {};

    return {
      mousemove: function mousemove(e) {
        hoverInfoSender.normalCall(e);
      },
      mouseout: mouseout,
      onResetImg: function onResetImg() {
        hoverInfoSender.immediateCall(null);
      },
      onResize: null
    };
  };

  // Returns a brush handler object. This has three public functions:
  // mousedown, mousemove, and onResetImg.
  imageutils.createBrushHandler = function (inputId, $el, opts, coordmap, outputId) {
    // Parameter: expand the area in which a brush can be started, by this
    // many pixels in all directions. (This should probably be a brush option)
    var expandPixels = 20;

    // Represents the state of the brush
    var brush = imageutils.createBrush($el, opts, coordmap, expandPixels);

    // Brush IDs can span multiple image/plot outputs. When an output is brushed,
    // if a brush with the same ID is active on a different image/plot, it must
    // be dismissed (but without sending any data to the server). We implement
    // this by sending the shiny-internal:brushed event to all plots, and letting
    // each plot decide for itself what to do.
    //
    // The decision to have the event sent to each plot (as opposed to a single
    // event triggered on, say, the document) was made to make cleanup easier;
    // listening on an event on the document would prevent garbage collection
    // of plot outputs that are removed from the document.
    $el.on("shiny-internal:brushed.image_output", function (e, coords) {
      // If the new brush shares our ID but not our output element ID, we
      // need to clear our brush (if any).
      if (coords.brushId === inputId && coords.outputId !== outputId) {
        $el.data("mostRecentBrush", false);
        brush.reset();
      }
    });

    // Set cursor to one of 7 styles. We need to set the cursor on the whole
    // el instead of the brush div, because the brush div has
    // 'pointer-events:none' so that it won't intercept pointer events.
    // If `style` is null, don't add a cursor style.
    function setCursorStyle(style) {
      $el.removeClass('crosshair grabbable grabbing ns-resize ew-resize nesw-resize nwse-resize');

      if (style) $el.addClass(style);
    }

    function sendBrushInfo() {
      var coords = brush.boundsData();

      // We're in a new or reset state
      if (isNaN(coords.xmin)) {
        exports.setInputValue(inputId, null);
        // Must tell other brushes to clear.
        imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
          brushId: inputId, outputId: null
        });
        return;
      }

      var panel = brush.getPanel();

      // Add the panel (facet) variables, if present
      $.extend(coords, panel.panel_vars);

      coords.coords_css = brush.boundsCss();
      coords.coords_img = coordmap.scaleCssToImg(coords.coords_css);

      coords.img_css_ratio = coordmap.cssToImgScalingRatio();

      // Add variable name mappings
      coords.mapping = panel.mapping;

      // Add scaling information
      coords.domain = panel.domain;
      coords.range = panel.range;
      coords.log = panel.log;

      coords.direction = opts.brushDirection;

      coords.brushId = inputId;
      coords.outputId = outputId;

      // Send data to server
      exports.setInputValue(inputId, coords);

      $el.data("mostRecentBrush", true);
      imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
    }

    var brushInfoSender;
    if (opts.brushDelayType === 'throttle') {
      brushInfoSender = new Throttler(null, sendBrushInfo, opts.brushDelay);
    } else {
      brushInfoSender = new Debouncer(null, sendBrushInfo, opts.brushDelay);
    }

    function mousedown(e) {
      // This can happen when mousedown inside the graphic, then mouseup
      // outside, then mousedown inside. Just ignore the second
      // mousedown.
      if (brush.isBrushing() || brush.isDragging() || brush.isResizing()) return;

      // Listen for left mouse button only
      if (e.which !== 1) return;

      // In general, brush uses css pixels, and coordmap uses img pixels.
      var offset_css = coordmap.mouseOffsetCss(e);

      // Ignore mousedown events outside of plotting region, expanded by
      // a number of pixels specified in expandPixels.
      if (opts.brushClip && !coordmap.isInPanelCss(offset_css, expandPixels)) return;

      brush.up({ x: NaN, y: NaN });
      brush.down(offset_css);

      if (brush.isInResizeArea(offset_css)) {
        brush.startResizing(offset_css);

        // Attach the move and up handlers to the window so that they respond
        // even when the mouse is moved outside of the image.
        $(document).on('mousemove.image_brush', mousemoveResizing).on('mouseup.image_brush', mouseupResizing);
      } else if (brush.isInsideBrush(offset_css)) {
        brush.startDragging(offset_css);
        setCursorStyle('grabbing');

        // Attach the move and up handlers to the window so that they respond
        // even when the mouse is moved outside of the image.
        $(document).on('mousemove.image_brush', mousemoveDragging).on('mouseup.image_brush', mouseupDragging);
      } else {
        var panel = coordmap.getPanelCss(offset_css, expandPixels);
        brush.startBrushing(panel.clipImg(coordmap.scaleCssToImg(offset_css)));

        // Attach the move and up handlers to the window so that they respond
        // even when the mouse is moved outside of the image.
        $(document).on('mousemove.image_brush', mousemoveBrushing).on('mouseup.image_brush', mouseupBrushing);
      }
    }

    // This sets the cursor style when it's in the el
    function mousemove(e) {
      // In general, brush uses css pixels, and coordmap uses img pixels.
      var offset_css = coordmap.mouseOffsetCss(e);

      if (!(brush.isBrushing() || brush.isDragging() || brush.isResizing())) {
        // Set the cursor depending on where it is
        if (brush.isInResizeArea(offset_css)) {
          var r = brush.whichResizeSides(offset_css);

          if (r.left && r.top || r.right && r.bottom) {
            setCursorStyle('nwse-resize');
          } else if (r.left && r.bottom || r.right && r.top) {
            setCursorStyle('nesw-resize');
          } else if (r.left || r.right) {
            setCursorStyle('ew-resize');
          } else if (r.top || r.bottom) {
            setCursorStyle('ns-resize');
          }
        } else if (brush.isInsideBrush(offset_css)) {
          setCursorStyle('grabbable');
        } else if (coordmap.isInPanelCss(offset_css, expandPixels)) {
          setCursorStyle('crosshair');
        } else {
          setCursorStyle(null);
        }
      }
    }

    // mousemove handlers while brushing or dragging
    function mousemoveBrushing(e) {
      brush.brushTo(coordmap.mouseOffsetCss(e));
      brushInfoSender.normalCall();
    }

    function mousemoveDragging(e) {
      brush.dragTo(coordmap.mouseOffsetCss(e));
      brushInfoSender.normalCall();
    }

    function mousemoveResizing(e) {
      brush.resizeTo(coordmap.mouseOffsetCss(e));
      brushInfoSender.normalCall();
    }

    // mouseup handlers while brushing or dragging
    function mouseupBrushing(e) {
      // Listen for left mouse button only
      if (e.which !== 1) return;

      $(document).off('mousemove.image_brush').off('mouseup.image_brush');

      brush.up(coordmap.mouseOffsetCss(e));

      brush.stopBrushing();
      setCursorStyle('crosshair');

      // If the brush didn't go anywhere, hide the brush, clear value,
      // and return.
      if (brush.down().x === brush.up().x && brush.down().y === brush.up().y) {
        brush.reset();
        brushInfoSender.immediateCall();
        return;
      }

      // Send info immediately on mouseup, but only if needed. If we don't
      // do the pending check, we might send the same data twice (with
      // with difference nonce).
      if (brushInfoSender.isPending()) brushInfoSender.immediateCall();
    }

    function mouseupDragging(e) {
      // Listen for left mouse button only
      if (e.which !== 1) return;

      $(document).off('mousemove.image_brush').off('mouseup.image_brush');

      brush.up(coordmap.mouseOffsetCss(e));

      brush.stopDragging();
      setCursorStyle('grabbable');

      if (brushInfoSender.isPending()) brushInfoSender.immediateCall();
    }

    function mouseupResizing(e) {
      // Listen for left mouse button only
      if (e.which !== 1) return;

      $(document).off('mousemove.image_brush').off('mouseup.image_brush');

      brush.up(coordmap.mouseOffsetCss(e));
      brush.stopResizing();

      if (brushInfoSender.isPending()) brushInfoSender.immediateCall();
    }

    // Brush maintenance: When an image is re-rendered, the brush must either
    // be removed (if brushResetOnNew) or imported (if !brushResetOnNew). The
    // "mostRecentBrush" bit is to ensure that when multiple outputs share the
    // same brush ID, inactive brushes don't send null values up to the server.

    // This should be called when the img (not the el) is reset
    function onResetImg() {
      if (opts.brushResetOnNew) {
        if ($el.data("mostRecentBrush")) {
          brush.reset();
          brushInfoSender.immediateCall();
        }
      }
    }

    if (!opts.brushResetOnNew) {
      if ($el.data("mostRecentBrush")) {
        // Importing an old brush must happen after the image data has loaded
        // and the <img> DOM element has the updated size. If importOldBrush()
        // is called before this happens, then the css-img coordinate mappings
        // will give the wrong result, and the brush will have the wrong
        // position.
        //
        // jcheng 09/26/2018: This used to happen in img.onLoad, but recently
        // we moved to all brush initialization moving to img.onLoad so this
        // logic can be executed inline.
        brush.importOldBrush();
        brushInfoSender.immediateCall();
      }
    }

    function onResize() {
      brush.onResize();
      brushInfoSender.immediateCall();
    }

    return {
      mousedown: mousedown,
      mousemove: mousemove,
      onResetImg: onResetImg,
      onResize: onResize
    };
  };

  // Returns an object that represents the state of the brush. This gets wrapped
  // in a brushHandler, which provides various event listeners.
  imageutils.createBrush = function ($el, opts, coordmap, expandPixels) {
    // Number of pixels outside of brush to allow start resizing
    var resizeExpand = 10;

    var el = $el[0];
    var $div = null; // The div representing the brush

    var state = {};

    // Aliases for conciseness
    var cssToImg = coordmap.scaleCssToImg;
    var imgToCss = coordmap.scaleImgToCss;

    reset();

    function reset() {
      // Current brushing/dragging/resizing state
      state.brushing = false;
      state.dragging = false;
      state.resizing = false;

      // Offset of last mouse down and up events (in CSS pixels)
      state.down = { x: NaN, y: NaN };
      state.up = { x: NaN, y: NaN };

      // Which side(s) we're currently resizing
      state.resizeSides = {
        left: false,
        right: false,
        top: false,
        bottom: false
      };

      // Bounding rectangle of the brush, in CSS pixel and data dimensions. We
      // need to record data dimensions along with pixel dimensions so that when
      // a new plot is sent, we can re-draw the brush div with the appropriate
      // coords.
      state.boundsCss = {
        xmin: NaN,
        xmax: NaN,
        ymin: NaN,
        ymax: NaN
      };
      state.boundsData = {
        xmin: NaN,
        xmax: NaN,
        ymin: NaN,
        ymax: NaN
      };

      // Panel object that the brush is in
      state.panel = null;

      // The bounds at the start of a drag/resize (in CSS pixels)
      state.changeStartBounds = {
        xmin: NaN,
        xmax: NaN,
        ymin: NaN,
        ymax: NaN
      };

      if ($div) $div.remove();
    }

    // If there's an existing brush div, use that div to set the new brush's
    // settings, provided that the x, y, and panel variables have the same names,
    // and there's a panel with matching panel variable values.
    function importOldBrush() {
      var oldDiv = $el.find('#' + el.id + '_brush');
      if (oldDiv.length === 0) return;

      var oldBoundsData = oldDiv.data('bounds-data');
      var oldPanel = oldDiv.data('panel');

      if (!oldBoundsData || !oldPanel) return;

      // Find a panel that has matching vars; if none found, we can't restore.
      // The oldPanel and new panel must match on their mapping vars, and the
      // values.
      for (var i = 0; i < coordmap.panels.length; i++) {
        var curPanel = coordmap.panels[i];

        if (equal(oldPanel.mapping, curPanel.mapping) && equal(oldPanel.panel_vars, curPanel.panel_vars)) {
          // We've found a matching panel
          state.panel = coordmap.panels[i];
          break;
        }
      }

      // If we didn't find a matching panel, remove the old div and return
      if (state.panel === null) {
        oldDiv.remove();
        return;
      }

      $div = oldDiv;

      boundsData(oldBoundsData);
      updateDiv();
    }

    // This will reposition the brush div when the image is resized, maintaining
    // the same data coordinates. Note that the "resize" here refers to the
    // wrapper div/img being resized; elsewhere, "resize" refers to the brush
    // div being resized.
    function onResize() {
      var bounds_data = boundsData();
      // Check to see if we have valid boundsData
      for (var val in bounds_data) {
        if (isnan(bounds_data[val])) return;
      }

      boundsData(bounds_data);
      updateDiv();
    }

    // Return true if the offset is inside min/max coords
    function isInsideBrush(offset_css) {
      var bounds = state.boundsCss;
      return offset_css.x <= bounds.xmax && offset_css.x >= bounds.xmin && offset_css.y <= bounds.ymax && offset_css.y >= bounds.ymin;
    }

    // Return true if offset is inside a region to start a resize
    function isInResizeArea(offset_css) {
      var sides = whichResizeSides(offset_css);
      return sides.left || sides.right || sides.top || sides.bottom;
    }

    // Return an object representing which resize region(s) the cursor is in.
    function whichResizeSides(offset_css) {
      var b = state.boundsCss;
      // Bounds with expansion
      var e = {
        xmin: b.xmin - resizeExpand,
        xmax: b.xmax + resizeExpand,
        ymin: b.ymin - resizeExpand,
        ymax: b.ymax + resizeExpand
      };
      var res = {
        left: false,
        right: false,
        top: false,
        bottom: false
      };

      if ((opts.brushDirection === 'xy' || opts.brushDirection === 'x') && offset_css.y <= e.ymax && offset_css.y >= e.ymin) {
        if (offset_css.x < b.xmin && offset_css.x >= e.xmin) res.left = true;else if (offset_css.x > b.xmax && offset_css.x <= e.xmax) res.right = true;
      }

      if ((opts.brushDirection === 'xy' || opts.brushDirection === 'y') && offset_css.x <= e.xmax && offset_css.x >= e.xmin) {
        if (offset_css.y < b.ymin && offset_css.y >= e.ymin) res.top = true;else if (offset_css.y > b.ymax && offset_css.y <= e.ymax) res.bottom = true;
      }

      return res;
    }

    // Sets the bounds of the brush (in CSS pixels), given a box and optional
    // panel. This will fit the box bounds into the panel, so we don't brush
    // outside of it. This knows whether we're brushing in the x, y, or xy
    // directions, and sets bounds accordingly. If no box is passed in, just
    // return current bounds.
    function boundsCss(box_css) {
      if (box_css === undefined) {
        return $.extend({}, state.boundsCss);
      }

      var min_css = { x: box_css.xmin, y: box_css.ymin };
      var max_css = { x: box_css.xmax, y: box_css.ymax };

      var panel = state.panel;
      var panelBounds_img = panel.range;

      if (opts.brushClip) {
        min_css = imgToCss(panel.clipImg(cssToImg(min_css)));
        max_css = imgToCss(panel.clipImg(cssToImg(max_css)));
      }

      if (opts.brushDirection === 'xy') {
        // No change

      } else if (opts.brushDirection === 'x') {
        // Extend top and bottom of plotting area
        min_css.y = imgToCss({ y: panelBounds_img.top }).y;
        max_css.y = imgToCss({ y: panelBounds_img.bottom }).y;
      } else if (opts.brushDirection === 'y') {
        min_css.x = imgToCss({ x: panelBounds_img.left }).x;
        max_css.x = imgToCss({ x: panelBounds_img.right }).x;
      }

      state.boundsCss = {
        xmin: min_css.x,
        xmax: max_css.x,
        ymin: min_css.y,
        ymax: max_css.y
      };

      // Positions in data space
      var min_data = state.panel.scaleImgToData(cssToImg(min_css));
      var max_data = state.panel.scaleImgToData(cssToImg(max_css));
      // For reversed scales, the min and max can be reversed, so use findBox
      // to ensure correct order.
      state.boundsData = imageutils.findBox(min_data, max_data);
      // Round to 14 significant digits to avoid spurious changes in FP values
      // (#1634).
      state.boundsData = mapValues(state.boundsData, function (val) {
        return roundSignif(val, 14);
      });

      // We also need to attach the data bounds and panel as data attributes, so
      // that if the image is re-sent, we can grab the data bounds to create a new
      // brush. This should be fast because it doesn't actually modify the DOM.
      $div.data('bounds-data', state.boundsData);
      $div.data('panel', state.panel);
      return undefined;
    }

    // Get or set the bounds of the brush using coordinates in the data space.
    function boundsData(box_data) {
      if (box_data === undefined) {
        return $.extend({}, state.boundsData);
      }

      var box_css = imgToCss(state.panel.scaleDataToImg(box_data));
      // Round to 13 significant digits to avoid spurious changes in FP values
      // (#2197).
      box_css = mapValues(box_css, function (val) {
        return roundSignif(val, 13);
      });

      // The scaling function can reverse the direction of the axes, so we need to
      // find the min and max again.
      boundsCss({
        xmin: Math.min(box_css.xmin, box_css.xmax),
        xmax: Math.max(box_css.xmin, box_css.xmax),
        ymin: Math.min(box_css.ymin, box_css.ymax),
        ymax: Math.max(box_css.ymin, box_css.ymax)
      });
      return undefined;
    }

    function getPanel() {
      return state.panel;
    }

    // Add a new div representing the brush.
    function addDiv() {
      if ($div) $div.remove();

      // Start hidden; we'll show it when movement occurs
      $div = $(document.createElement('div')).attr('id', el.id + '_brush').css({
        'background-color': opts.brushFill,
        'opacity': opts.brushOpacity,
        'pointer-events': 'none',
        'position': 'absolute'
      }).hide();

      var borderStyle = '1px solid ' + opts.brushStroke;
      if (opts.brushDirection === 'xy') {
        $div.css({
          'border': borderStyle
        });
      } else if (opts.brushDirection === 'x') {
        $div.css({
          'border-left': borderStyle,
          'border-right': borderStyle
        });
      } else if (opts.brushDirection === 'y') {
        $div.css({
          'border-top': borderStyle,
          'border-bottom': borderStyle
        });
      }

      $el.append($div);
      $div.offset({ x: 0, y: 0 }).width(0).outerHeight(0);
    }

    // Update the brush div to reflect the current brush bounds.
    function updateDiv() {
      // Need parent offset relative to page to calculate mouse offset
      // relative to page.
      var img_offset_css = findOrigin($el.find("img"));
      var b = state.boundsCss;

      $div.offset({
        top: img_offset_css.y + b.ymin,
        left: img_offset_css.x + b.xmin
      }).outerWidth(b.xmax - b.xmin + 1).outerHeight(b.ymax - b.ymin + 1);
    }

    function down(offset_css) {
      if (offset_css === undefined) return state.down;

      state.down = offset_css;
      return undefined;
    }

    function up(offset_css) {
      if (offset_css === undefined) return state.up;

      state.up = offset_css;
      return undefined;
    }

    function isBrushing() {
      return state.brushing;
    }

    function startBrushing() {
      state.brushing = true;
      addDiv();
      state.panel = coordmap.getPanelCss(state.down, expandPixels);

      boundsCss(imageutils.findBox(state.down, state.down));
      updateDiv();
    }

    function brushTo(offset_css) {
      boundsCss(imageutils.findBox(state.down, offset_css));
      $div.show();
      updateDiv();
    }

    function stopBrushing() {
      state.brushing = false;
      // Save the final bounding box of the brush
      boundsCss(imageutils.findBox(state.down, state.up));
    }

    function isDragging() {
      return state.dragging;
    }

    function startDragging() {
      state.dragging = true;
      state.changeStartBounds = $.extend({}, state.boundsCss);
    }

    function dragTo(offset_css) {
      // How far the brush was dragged
      var dx = offset_css.x - state.down.x;
      var dy = offset_css.y - state.down.y;

      // Calculate what new positions would be, before clipping.
      var start = state.changeStartBounds;
      var newBounds_css = {
        xmin: start.xmin + dx,
        xmax: start.xmax + dx,
        ymin: start.ymin + dy,
        ymax: start.ymax + dy
      };

      // Clip to the plotting area
      if (opts.brushClip) {
        var panelBounds_img = state.panel.range;
        var newBounds_img = cssToImg(newBounds_css);

        // Convert to format for shiftToRange
        var xvals_img = [newBounds_img.xmin, newBounds_img.xmax];
        var yvals_img = [newBounds_img.ymin, newBounds_img.ymax];

        xvals_img = imageutils.shiftToRange(xvals_img, panelBounds_img.left, panelBounds_img.right);
        yvals_img = imageutils.shiftToRange(yvals_img, panelBounds_img.top, panelBounds_img.bottom);

        // Convert back to bounds format
        newBounds_css = imgToCss({
          xmin: xvals_img[0],
          xmax: xvals_img[1],
          ymin: yvals_img[0],
          ymax: yvals_img[1]
        });
      }

      boundsCss(newBounds_css);
      updateDiv();
    }

    function stopDragging() {
      state.dragging = false;
    }

    function isResizing() {
      return state.resizing;
    }

    function startResizing() {
      state.resizing = true;
      state.changeStartBounds = $.extend({}, state.boundsCss);
      state.resizeSides = whichResizeSides(state.down);
    }

    function resizeTo(offset_css) {
      // How far the brush was dragged
      var d_css = {
        x: offset_css.x - state.down.x,
        y: offset_css.y - state.down.y
      };

      var d_img = cssToImg(d_css);

      // Calculate what new positions would be, before clipping.
      var b_img = cssToImg(state.changeStartBounds);
      var panelBounds_img = state.panel.range;

      if (state.resizeSides.left) {
        var xmin_img = imageutils.shiftToRange(b_img.xmin + d_img.x, panelBounds_img.left, b_img.xmax)[0];
        b_img.xmin = xmin_img;
      } else if (state.resizeSides.right) {
        var xmax_img = imageutils.shiftToRange(b_img.xmax + d_img.x, b_img.xmin, panelBounds_img.right)[0];
        b_img.xmax = xmax_img;
      }

      if (state.resizeSides.top) {
        var ymin_img = imageutils.shiftToRange(b_img.ymin + d_img.y, panelBounds_img.top, b_img.ymax)[0];
        b_img.ymin = ymin_img;
      } else if (state.resizeSides.bottom) {
        var ymax_img = imageutils.shiftToRange(b_img.ymax + d_img.y, b_img.ymin, panelBounds_img.bottom)[0];
        b_img.ymax = ymax_img;
      }

      boundsCss(imgToCss(b_img));
      updateDiv();
    }

    function stopResizing() {
      state.resizing = false;
    }

    return {
      reset: reset,

      importOldBrush: importOldBrush,
      isInsideBrush: isInsideBrush,
      isInResizeArea: isInResizeArea,
      whichResizeSides: whichResizeSides,

      onResize: onResize, // A callback when the wrapper div or img is resized.

      boundsCss: boundsCss,
      boundsData: boundsData,
      getPanel: getPanel,

      down: down,
      up: up,

      isBrushing: isBrushing,
      startBrushing: startBrushing,
      brushTo: brushTo,
      stopBrushing: stopBrushing,

      isDragging: isDragging,
      startDragging: startDragging,
      dragTo: dragTo,
      stopDragging: stopDragging,

      isResizing: isResizing,
      startResizing: startResizing,
      resizeTo: resizeTo,
      stopResizing: stopResizing
    };
  };

  exports.resetBrush = function (brushId) {
    exports.setInputValue(brushId, null);
    imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
      brushId: brushId, outputId: null
    });
  };

  // -----------------------------------------------------------------------
  // Utility functions for finding dimensions and locations of DOM elements
  // -----------------------------------------------------------------------

  // Returns the ratio that an element has been scaled (for example, by CSS
  // transforms) in the x and y directions.
  function findScalingRatio($el) {
    var boundingRect = $el[0].getBoundingClientRect();
    return {
      x: boundingRect.width / $el.outerWidth(),
      y: boundingRect.height / $el.outerHeight()
    };
  }

  function findOrigin($el) {
    var offset = $el.offset();
    var scaling_ratio = findScalingRatio($el);

    // Find the size of the padding and border, for the top and left. This is
    // before any transforms.
    var paddingBorder = {
      left: parseInt($el.css("border-left-width")) + parseInt($el.css("padding-left")),
      top: parseInt($el.css("border-top-width")) + parseInt($el.css("padding-top"))
    };

    // offset() returns the upper left corner of the element relative to the
    // page, but it includes padding and border. Here we find the upper left
    // of the element, not including padding and border.
    return {
      x: offset.left + scaling_ratio.x * paddingBorder.left,
      y: offset.top + scaling_ratio.y * paddingBorder.top
    };
  }

  // Find the dimensions of a tag, after transforms, and without padding and
  // border.
  function findDims($el) {
    // If there's any padding/border, we need to find the ratio of the actual
    // element content compared to the element plus padding and border.
    var content_ratio = {
      x: $el.width() / $el.outerWidth(),
      y: $el.height() / $el.outerHeight()
    };

    // Get the dimensions of the element _after_ any CSS transforms. This
    // includes the padding and border.
    var bounding_rect = $el[0].getBoundingClientRect();

    // Dimensions of the element after any CSS transforms, and without
    // padding/border.
    return {
      x: content_ratio.x * bounding_rect.width,
      y: content_ratio.y * bounding_rect.height
    };
  }

  //---------------------------------------------------------------------
  // Source file: ../srcjs/output_binding_html.js

  var htmlOutputBinding = new OutputBinding();
  $.extend(htmlOutputBinding, {
    find: function find(scope) {
      return $(scope).find('.shiny-html-output');
    },
    onValueError: function onValueError(el, err) {
      exports.unbindAll(el);
      this.renderError(el, err);
    },
    renderValue: function renderValue(el, data) {
      exports.renderContent(el, data);
    }
  });
  outputBindings.register(htmlOutputBinding, 'shiny.htmlOutput');

  var renderDependencies = exports.renderDependencies = function (dependencies) {
    if (dependencies) {
      $.each(dependencies, function (i, dep) {
        renderDependency(dep);
      });
    }
  };

  // Render HTML in a DOM element, add dependencies, and bind Shiny
  // inputs/outputs. `content` can be null, a string, or an object with
  // properties 'html' and 'deps'.
  exports.renderContent = function (el, content) {
    var where = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "replace";

    if (where === "replace") {
      exports.unbindAll(el);
    }

    var html;
    var dependencies = [];
    if (content === null) {
      html = '';
    } else if (typeof content === 'string') {
      html = content;
    } else if ((typeof content === "undefined" ? "undefined" : _typeof(content)) === 'object') {
      html = content.html;
      dependencies = content.deps || [];
    }

    exports.renderHtml(html, el, dependencies, where);

    var scope = el;
    if (where === "replace") {
      exports.initializeInputs(el);
      exports.bindAll(el);
    } else {
      var $parent = $(el).parent();
      if ($parent.length > 0) {
        scope = $parent;
        if (where === "beforeBegin" || where === "afterEnd") {
          var $grandparent = $parent.parent();
          if ($grandparent.length > 0) scope = $grandparent;
        }
      }
      exports.initializeInputs(scope);
      exports.bindAll(scope);
    }
  };

  // Render HTML in a DOM element, inserting singletons into head as needed
  exports.renderHtml = function (html, el, dependencies) {
    var where = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'replace';

    renderDependencies(dependencies);
    return singletons.renderHtml(html, el, where);
  };

  var htmlDependencies = {};
  function registerDependency(name, version) {
    htmlDependencies[name] = version;
  }

  // Client-side dependency resolution and rendering
  function renderDependency(dep) {
    if (htmlDependencies.hasOwnProperty(dep.name)) return false;

    registerDependency(dep.name, dep.version);

    var href = dep.src.href;

    var $head = $("head").first();

    if (dep.meta) {
      var metas = $.map(asArray(dep.meta), function (obj, idx) {
        // only one named pair is expected in obj as it's already been decomposed
        var name = Object.keys(obj)[0];
        return $("<meta>").attr("name", name).attr("content", obj[name]);
      });
      $head.append(metas);
    }

    if (dep.stylesheet) {
      var stylesheets = $.map(asArray(dep.stylesheet), function (stylesheet) {
        return $("<link rel='stylesheet' type='text/css'>").attr("href", href + "/" + encodeURI(stylesheet));
      });
      $head.append(stylesheets);
    }

    if (dep.script) {
      var scripts = $.map(asArray(dep.script), function (scriptName) {
        return $("<script>").attr("src", href + "/" + encodeURI(scriptName));
      });
      $head.append(scripts);
    }

    if (dep.attachment) {
      // dep.attachment might be a single string, an array, or an object.
      var attachments = dep.attachment;
      if (typeof attachments === "string") attachments = [attachments];
      if ($.isArray(attachments)) {
        // The contract for attachments is that arrays of attachments are
        // addressed using 1-based indexes. Convert this array to an object.
        var tmp = {};
        $.each(attachments, function (index, attachment) {
          tmp[index + 1 + ""] = attachment;
        });
        attachments = tmp;
      }

      var attach = $.map(attachments, function (attachment, key) {
        return $("<link rel='attachment'>").attr("id", dep.name + "-" + key + "-attachment").attr("href", href + "/" + encodeURI(attachment));
      });
      $head.append(attach);
    }

    if (dep.head) {
      var $newHead = $("<head></head>");
      $newHead.html(dep.head);
      $head.append($newHead.children());
    }
    return true;
  }

  var singletons = {
    knownSingletons: {},
    renderHtml: function renderHtml(html, el, where) {
      var processed = this._processHtml(html);
      this._addToHead(processed.head);
      this.register(processed.singletons);
      if (where === "replace") {
        $(el).html(processed.html);
      } else {
        el.insertAdjacentHTML(where, processed.html);
      }
      return processed;
    },
    // Take an object where keys are names of singletons, and merges it into
    // knownSingletons
    register: function register(s) {
      $.extend(this.knownSingletons, s);
    },
    // Takes a string or array of strings and adds them to knownSingletons
    registerNames: function registerNames(s) {
      if (typeof s === 'string') {
        this.knownSingletons[s] = true;
      } else if (s instanceof Array) {
        for (var i = 0; i < s.length; i++) {
          this.knownSingletons[s[i]] = true;
        }
      }
    },
    // Inserts new content into document head
    _addToHead: function _addToHead(head) {
      if (head.length > 0) {
        var tempDiv = $("<div>" + head + "</div>")[0];
        var $head = $('head');
        while (tempDiv.hasChildNodes()) {
          $head.append(tempDiv.firstChild);
        }
      }
    },
    // Reads HTML and returns an object with info about singletons
    _processHtml: function _processHtml(val) {
      var self = this;
      var newSingletons = {};
      var newVal;

      var findNewPayload = function findNewPayload(match, p1, sig, payload) {
        if (self.knownSingletons[sig] || newSingletons[sig]) return "";
        newSingletons[sig] = true;
        return payload;
      };
      while (true) {
        newVal = val.replace(self._reSingleton, findNewPayload);
        if (val.length === newVal.length) break;
        val = newVal;
      }

      var heads = [];
      var headAddPayload = function headAddPayload(match, payload) {
        heads.push(payload);
        return "";
      };
      while (true) {
        newVal = val.replace(self._reHead, headAddPayload);
        if (val.length === newVal.length) break;
        val = newVal;
      }

      return {
        html: val,
        head: heads.join("\n"),
        singletons: newSingletons
      };
    },
    _reSingleton: /<!--(SHINY.SINGLETON\[([\w]+)\])-->([\s\S]*?)<!--\/\1-->/,
    _reHead: /<head(?:\s[^>]*)?>([\s\S]*?)<\/head>/
  };

  //---------------------------------------------------------------------
  // Source file: ../srcjs/output_binding_downloadlink.js

  var downloadLinkOutputBinding = new OutputBinding();
  $.extend(downloadLinkOutputBinding, {
    find: function find(scope) {
      return $(scope).find('a.shiny-download-link');
    },
    renderValue: function renderValue(el, data) {
      $(el).attr('href', data);
    }
  });
  outputBindings.register(downloadLinkOutputBinding, 'shiny.downloadLink');

  // Trigger shiny:filedownload event whenever a downloadButton/Link is clicked
  $(document).on('click.shinyDownloadLink', 'a.shiny-download-link', function (e) {
    var evt = jQuery.Event('shiny:filedownload');
    evt.name = this.id;
    evt.href = this.href;
    $(document).trigger(evt);
  });

  //---------------------------------------------------------------------
  // Source file: ../srcjs/output_binding_datatable.js

  var datatableOutputBinding = new OutputBinding();
  $.extend(datatableOutputBinding, {
    find: function find(scope) {
      return $(scope).find('.shiny-datatable-output');
    },
    onValueError: function onValueError(el, err) {
      exports.unbindAll(el);
      this.renderError(el, err);
    },
    renderValue: function renderValue(el, _data) {
      var $el = $(el).empty();
      if (!_data || !_data.colnames) return;

      var colnames = $.makeArray(_data.colnames);
      var header = $.map(colnames, function (x) {
        return '<th>' + x + '</th>';
      }).join('');
      header = '<thead><tr>' + header + '</tr></thead>';
      var footer = '';
      if (_data.options === null || _data.options.searching !== false) {
        footer = $.map(colnames, function (x) {
          // placeholder needs to be escaped (and HTML tags are stripped off)
          return '<th><input type="text" placeholder="' + escapeHTML(x.replace(/(<([^>]+)>)/ig, '')) + '" /></th>';
        }).join('');
        footer = '<tfoot>' + footer + '</tfoot>';
      }
      var content = '<table class="table table-striped table-hover">' + header + footer + '</table>';
      $el.append(content);

      // options that should be eval()ed
      if (_data.evalOptions) $.each(_data.evalOptions, function (i, x) {
        /*jshint evil: true */
        _data.options[x] = eval('(' + _data.options[x] + ')');
      });

      // caseInsensitive searching? default true
      var searchCI = _data.options === null || typeof _data.options.search === 'undefined' || _data.options.search.caseInsensitive !== false;
      var oTable = $(el).children("table").DataTable($.extend({
        "processing": true,
        "serverSide": true,
        "order": [],
        "orderClasses": false,
        "pageLength": 25,
        "ajax": {
          "url": _data.action,
          "type": "POST",
          "data": function data(d) {
            d.search.caseInsensitive = searchCI;
            d.escape = _data.escape;
          }
        }
      }, _data.options));
      // the table object may need post-processing
      if (typeof _data.callback === 'string') {
        /*jshint evil: true */
        var callback = eval('(' + _data.callback + ')');
        if (typeof callback === 'function') callback(oTable);
      }

      // use debouncing for searching boxes
      $el.find('label input').first().unbind('keyup').keyup(debounce(_data.searchDelay, function () {
        oTable.search(this.value).draw();
      }));
      var searchInputs = $el.find("tfoot input");
      if (searchInputs.length > 0) {
        // this is a little weird: aoColumns/bSearchable are still in DT 1.10
        // https://github.com/DataTables/DataTables/issues/388
        $.each(oTable.settings()[0].aoColumns, function (i, x) {
          // hide the text box if not searchable
          if (!x.bSearchable) searchInputs.eq(i).hide();
        });
        searchInputs.keyup(debounce(_data.searchDelay, function () {
          oTable.column(searchInputs.index(this)).search(this.value).draw();
        }));
      }
      // FIXME: ugly scrollbars in tab panels b/c Bootstrap uses 'visible: auto'
      $el.parents('.tab-content').css('overflow', 'visible');
    }
  });
  outputBindings.register(datatableOutputBinding, 'shiny.datatableOutput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/output_binding_adapter.js

  var OutputBindingAdapter = function OutputBindingAdapter(el, binding) {
    this.el = el;
    this.binding = binding;

    // If the binding actually has a resize method, override the prototype of
    // onResize with a version that does a makeResizeFilter on the element.
    if (binding.resize) {
      this.onResize = makeResizeFilter(el, function (width, height) {
        binding.resize(el, width, height);
      });
    }
  };
  (function () {
    this.getId = function () {
      return this.binding.getId(this.el);
    };
    this.onValueChange = function (data) {
      this.binding.onValueChange(this.el, data);
    };
    this.onValueError = function (err) {
      this.binding.onValueError(this.el, err);
    };
    this.showProgress = function (show) {
      this.binding.showProgress(this.el, show);
    };
    this.onResize = function () {
      // Intentionally left blank; see constructor
    };
  }).call(OutputBindingAdapter.prototype);

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding.js

  var InputBinding = exports.InputBinding = function () {};

  (function () {

    // Returns a jQuery object or element array that contains the
    // descendants of scope that match this binding
    this.find = function (scope) {
      throw "Not implemented";
    };

    this.getId = function (el) {
      return el['data-input-id'] || el.id;
    };

    // Gives the input a type in case the server needs to know it
    // to deserialize the JSON correctly
    this.getType = function () {
      return false;
    };
    this.getValue = function (el) {
      throw "Not implemented";
    };

    // The callback method takes one argument, whose value is boolean. If true,
    // allow deferred (debounce or throttle) sending depending on the value of
    // getRatePolicy. If false, send value immediately.
    this.subscribe = function (el, callback) {};
    this.unsubscribe = function (el) {};

    // This is used for receiving messages that tell the input object to do
    // things, such as setting values (including min, max, and others).
    // 'data' should be an object with elements corresponding to value, min,
    // max, etc., as appropriate for the type of input object. It also should
    // trigger a change event.
    this.receiveMessage = function (el, data) {
      throw "Not implemented";
    };
    this.getState = function (el, data) {
      throw "Not implemented";
    };

    this.getRatePolicy = function () {
      return null;
    };

    // Some input objects need initialization before being bound. This is
    // called when the document is ready (for statically-added input objects),
    // and when new input objects are added to the document with
    // htmlOutputBinding.renderValue() (for dynamically-added input objects).
    // This is called before the input is bound.
    this.initialize = function (el) {};

    // This is called after unbinding the output.
    this.dispose = function (el) {};
  }).call(InputBinding.prototype);

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_text.js

  var textInputBinding = new InputBinding();
  $.extend(textInputBinding, {
    find: function find(scope) {
      var $inputs = $(scope).find('input[type="text"], input[type="search"], input[type="url"], input[type="email"]');
      // selectize.js 0.12.4 inserts a hidden text input with an
      // id that ends in '-selectized'. The .not() selector below
      // is to prevent textInputBinding from accidentally picking up
      // this hidden element as a shiny input (#2396)
      return $inputs.not('input[type="text"][id$="-selectized"]');
    },
    getId: function getId(el) {
      return InputBinding.prototype.getId.call(this, el) || el.name;
    },
    getValue: function getValue(el) {
      return el.value;
    },
    setValue: function setValue(el, value) {
      el.value = value;
    },
    subscribe: function subscribe(el, callback) {
      $(el).on('keyup.textInputBinding input.textInputBinding', function (event) {
        callback(true);
      });
      $(el).on('change.textInputBinding', function (event) {
        callback(false);
      });
    },
    unsubscribe: function unsubscribe(el) {
      $(el).off('.textInputBinding');
    },
    receiveMessage: function receiveMessage(el, data) {
      if (data.hasOwnProperty('value')) this.setValue(el, data.value);

      updateLabel(data.label, this._getLabelNode(el));

      if (data.hasOwnProperty('placeholder')) el.placeholder = data.placeholder;

      $(el).trigger('change');
    },
    getState: function getState(el) {
      return {
        label: this._getLabelNode(el).text(),
        value: el.value,
        placeholder: el.placeholder
      };
    },
    getRatePolicy: function getRatePolicy() {
      return {
        policy: 'debounce',
        delay: 250
      };
    },
    _getLabelNode: function _getLabelNode(el) {
      return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
    }
  });
  inputBindings.register(textInputBinding, 'shiny.textInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_textarea.js

  var textareaInputBinding = {};
  $.extend(textareaInputBinding, textInputBinding, {
    find: function find(scope) {
      return $(scope).find('textarea');
    }
  });
  inputBindings.register(textareaInputBinding, 'shiny.textareaInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_password.js

  var passwordInputBinding = {};
  $.extend(passwordInputBinding, textInputBinding, {
    find: function find(scope) {
      return $(scope).find('input[type="password"]');
    },
    getType: function getType(el) {
      return "shiny.password";
    }
  });
  inputBindings.register(passwordInputBinding, 'shiny.passwordInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_number.js

  var numberInputBinding = {};
  $.extend(numberInputBinding, textInputBinding, {
    find: function find(scope) {
      return $(scope).find('input[type="number"]');
    },
    getValue: function getValue(el) {
      var numberVal = $(el).val();
      if (/^\s*$/.test(numberVal)) // Return null if all whitespace
        return null;else if (!isNaN(numberVal)) // If valid Javascript number string, coerce to number
        return +numberVal;else return numberVal; // If other string like "1e6", send it unchanged
    },
    setValue: function setValue(el, value) {
      el.value = value;
    },
    getType: function getType(el) {
      return "shiny.number";
    },
    receiveMessage: function receiveMessage(el, data) {
      if (data.hasOwnProperty('value')) el.value = data.value;
      if (data.hasOwnProperty('min')) el.min = data.min;
      if (data.hasOwnProperty('max')) el.max = data.max;
      if (data.hasOwnProperty('step')) el.step = data.step;

      updateLabel(data.label, this._getLabelNode(el));

      $(el).trigger('change');
    },
    getState: function getState(el) {
      return { label: this._getLabelNode(el).text(),
        value: this.getValue(el),
        min: Number(el.min),
        max: Number(el.max),
        step: Number(el.step) };
    },
    _getLabelNode: function _getLabelNode(el) {
      return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
    }
  });
  inputBindings.register(numberInputBinding, 'shiny.numberInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_checkbox.js

  var checkboxInputBinding = new InputBinding();
  $.extend(checkboxInputBinding, {
    find: function find(scope) {
      return $(scope).find('input[type="checkbox"]');
    },
    getValue: function getValue(el) {
      return el.checked;
    },
    setValue: function setValue(el, value) {
      el.checked = value;
    },
    subscribe: function subscribe(el, callback) {
      $(el).on('change.checkboxInputBinding', function (event) {
        callback(true);
      });
    },
    unsubscribe: function unsubscribe(el) {
      $(el).off('.checkboxInputBinding');
    },
    getState: function getState(el) {
      return {
        label: $(el).parent().find('span').text(),
        value: el.checked
      };
    },
    receiveMessage: function receiveMessage(el, data) {
      if (data.hasOwnProperty('value')) el.checked = data.value;

      // checkboxInput()'s label works different from other
      // input labels...the label container should always exist
      if (data.hasOwnProperty('label')) $(el).parent().find('span').text(data.label);

      $(el).trigger('change');
    }
  });
  inputBindings.register(checkboxInputBinding, 'shiny.checkboxInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_slider.js

  // Necessary to get hidden sliders to send their updated values
  function forceIonSliderUpdate(slider) {
    if (slider.$cache && slider.$cache.input) slider.$cache.input.trigger('change');else console.log("Couldn't force ion slider to update");
  }

  function getTypePrettifyer(dataType, timeFormat, timezone) {
    var timeFormatter;
    var prettify;
    if (dataType === 'date') {
      timeFormatter = strftime.utc();
      prettify = function prettify(num) {
        return timeFormatter(timeFormat, new Date(num));
      };
    } else if (dataType === 'datetime') {
      if (timezone) timeFormatter = strftime.timezone(timezone);else timeFormatter = strftime;

      prettify = function prettify(num) {
        return timeFormatter(timeFormat, new Date(num));
      };
    } else {
      // The default prettify function for ion.rangeSlider adds thousands
      // separators after the decimal mark, so we have our own version here.
      // (#1958)
      prettify = function prettify(num) {
        // When executed, `this` will refer to the `IonRangeSlider.options`
        // object.
        return formatNumber(num, this.prettify_separator);
      };
    }
    return prettify;
  }

  var sliderInputBinding = {};
  $.extend(sliderInputBinding, textInputBinding, {
    find: function find(scope) {
      // Check if ionRangeSlider plugin is loaded
      if (!$.fn.ionRangeSlider) return [];

      return $(scope).find('input.js-range-slider');
    },
    getType: function getType(el) {
      var dataType = $(el).data('data-type');
      if (dataType === 'date') return 'shiny.date';else if (dataType === 'datetime') return 'shiny.datetime';else return false;
    },
    getValue: function getValue(el) {
      var $el = $(el);
      var result = $(el).data('ionRangeSlider').result;

      // Function for converting numeric value from slider to appropriate type.
      var convert;
      var dataType = $el.data('data-type');
      if (dataType === 'date') {
        convert = function convert(val) {
          return formatDateUTC(new Date(+val));
        };
      } else if (dataType === 'datetime') {
        convert = function convert(val) {
          // Convert ms to s
          return +val / 1000;
        };
      } else {
        convert = function convert(val) {
          return +val;
        };
      }

      if (this._numValues(el) === 2) {
        return [convert(result.from), convert(result.to)];
      } else {
        return convert(result.from);
      }
    },
    setValue: function setValue(el, value) {
      var $el = $(el);
      var slider = $el.data('ionRangeSlider');

      $el.data('immediate', true);
      try {
        if (this._numValues(el) === 2 && value instanceof Array) {
          slider.update({ from: value[0], to: value[1] });
        } else {
          slider.update({ from: value });
        }

        forceIonSliderUpdate(slider);
      } finally {
        $el.data('immediate', false);
      }
    },
    subscribe: function subscribe(el, callback) {
      $(el).on('change.sliderInputBinding', function (event) {
        callback(!$(el).data('immediate') && !$(el).data('animating'));
      });
    },
    unsubscribe: function unsubscribe(el) {
      $(el).off('.sliderInputBinding');
    },
    receiveMessage: function receiveMessage(el, data) {
      var $el = $(el);
      var slider = $el.data('ionRangeSlider');
      var msg = {};

      if (data.hasOwnProperty('value')) {
        if (this._numValues(el) === 2 && data.value instanceof Array) {
          msg.from = data.value[0];
          msg.to = data.value[1];
        } else {
          msg.from = data.value;
        }
      }
      var sliderFeatures = ['min', 'max', 'step'];
      for (var i = 0; i < sliderFeatures.length; i++) {
        var feats = sliderFeatures[i];
        if (data.hasOwnProperty(feats)) {
          msg[feats] = data[feats];
        }
      }

      updateLabel(data.label, this._getLabelNode(el));

      var domElements = ['data-type', 'time-format', 'timezone'];
      for (var i = 0; i < domElements.length; i++) {
        var elem = domElements[i];
        if (data.hasOwnProperty(elem)) {
          $el.data(elem, data[elem]);
        }
      }

      var dataType = $el.data('data-type');
      var timeFormat = $el.data('time-format');
      var timezone = $el.data('timezone');

      msg.prettify = getTypePrettifyer(dataType, timeFormat, timezone);

      $el.data('immediate', true);
      try {
        slider.update(msg);
        forceIonSliderUpdate(slider);
      } finally {
        $el.data('immediate', false);
      }
    },
    getRatePolicy: function getRatePolicy() {
      return {
        policy: 'debounce',
        delay: 250
      };
    },
    getState: function getState(el) {},
    initialize: function initialize(el) {
      var opts = {};
      var $el = $(el);
      var dataType = $el.data('data-type');
      var timeFormat = $el.data('time-format');
      var timezone = $el.data('timezone');

      opts.prettify = getTypePrettifyer(dataType, timeFormat, timezone);

      $el.ionRangeSlider(opts);
    },
    _getLabelNode: function _getLabelNode(el) {
      return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
    },
    // Number of values; 1 for single slider, 2 for range slider
    _numValues: function _numValues(el) {
      if ($(el).data('ionRangeSlider').options.type === 'double') return 2;else return 1;
    }
  });
  inputBindings.register(sliderInputBinding, 'shiny.sliderInput');

  // Format numbers for nicer output.
  // formatNumber(1234567.12345)           === "1,234,567.12345"
  // formatNumber(1234567.12345, ".", ",") === "1.234.567,12345"
  // formatNumber(1000, " ")               === "1 000"
  // formatNumber(20)                      === "20"
  // formatNumber(1.2345e24)               === "1.2345e+24"
  function formatNumber(num) {
    var thousand_sep = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ",";
    var decimal_sep = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ".";

    var parts = num.toString().split(".");

    // Add separators to portion before decimal mark.
    parts[0] = parts[0].replace(/(\d{1,3}(?=(?:\d\d\d)+(?!\d)))/g, "$1" + thousand_sep);

    if (parts.length === 1) return parts[0];else if (parts.length === 2) return parts[0] + decimal_sep + parts[1];else return "";
  };

  $(document).on('click', '.slider-animate-button', function (evt) {
    evt.preventDefault();
    var self = $(this);
    var target = $('#' + $escape(self.attr('data-target-id')));
    var startLabel = 'Play';
    var stopLabel = 'Pause';
    var loop = self.attr('data-loop') !== undefined && !/^\s*false\s*$/i.test(self.attr('data-loop'));
    var animInterval = self.attr('data-interval');
    if (isNaN(animInterval)) animInterval = 1500;else animInterval = +animInterval;

    if (!target.data('animTimer')) {
      var slider;
      var timer;

      // Separate code paths:
      // Backward compatible code for old-style jsliders (Shiny <= 0.10.2.2),
      // and new-style ionsliders.
      if (target.hasClass('jslider')) {
        slider = target.slider();

        // If we're currently at the end, restart
        if (!slider.canStepNext()) slider.resetToStart();

        timer = setInterval(function () {
          if (loop && !slider.canStepNext()) {
            slider.resetToStart();
          } else {
            slider.stepNext();
            if (!loop && !slider.canStepNext()) {
              self.click(); // stop the animation
            }
          }
        }, animInterval);
      } else {
        slider = target.data('ionRangeSlider');
        // Single sliders have slider.options.type == "single", and only the
        // `from` value is used. Double sliders have type == "double", and also
        // use the `to` value for the right handle.
        var sliderCanStep = function sliderCanStep() {
          if (slider.options.type === "double") return slider.result.to < slider.result.max;else return slider.result.from < slider.result.max;
        };
        var sliderReset = function sliderReset() {
          var val = { from: slider.result.min };
          // Preserve the current spacing for double sliders
          if (slider.options.type === "double") val.to = val.from + (slider.result.to - slider.result.from);

          slider.update(val);
          forceIonSliderUpdate(slider);
        };
        var sliderStep = function sliderStep() {
          // Don't overshoot the end
          var val = {
            from: Math.min(slider.result.max, slider.result.from + slider.options.step)
          };
          if (slider.options.type === "double") val.to = Math.min(slider.result.max, slider.result.to + slider.options.step);

          slider.update(val);
          forceIonSliderUpdate(slider);
        };

        // If we're currently at the end, restart
        if (!sliderCanStep()) sliderReset();

        timer = setInterval(function () {
          if (loop && !sliderCanStep()) {
            sliderReset();
          } else {
            sliderStep();
            if (!loop && !sliderCanStep()) {
              self.click(); // stop the animation
            }
          }
        }, animInterval);
      }

      target.data('animTimer', timer);
      self.attr('title', stopLabel);
      self.addClass('playing');
      target.data('animating', true);
    } else {
      clearTimeout(target.data('animTimer'));
      target.removeData('animTimer');
      self.attr('title', startLabel);
      self.removeClass('playing');
      target.removeData('animating');
    }
  });

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_date.js

  var dateInputBinding = new InputBinding();
  $.extend(dateInputBinding, {
    find: function find(scope) {
      return $(scope).find('.shiny-date-input');
    },
    getType: function getType(el) {
      return "shiny.date";
    },
    // Return the date in an unambiguous format, yyyy-mm-dd (as opposed to a
    // format like mm/dd/yyyy)
    getValue: function getValue(el) {
      var date = $(el).find('input').bsDatepicker('getUTCDate');
      return formatDateUTC(date);
    },
    // value must be an unambiguous string like '2001-01-01', or a Date object.
    setValue: function setValue(el, value) {
      // R's NA, which is null here will remove current value
      if (value === null) {
        $(el).find('input').val('').bsDatepicker('update');
        return;
      }

      var date = this._newDate(value);
      // If date is invalid, do nothing
      if (isNaN(date)) return;

      $(el).find('input').bsDatepicker('setUTCDate', date);
    },
    getState: function getState(el) {
      var $el = $(el);
      var $input = $el.find('input');

      var min = $input.data('datepicker').startDate;
      var max = $input.data('datepicker').endDate;

      // Stringify min and max. If min and max aren't set, they will be
      // -Infinity and Infinity; replace these with null.
      min = min === -Infinity ? null : formatDateUTC(min);
      max = max === Infinity ? null : formatDateUTC(max);

      // startViewMode is stored as a number; convert to string
      var startview = $input.data('datepicker').startViewMode;
      if (startview === 2) startview = 'decade';else if (startview === 1) startview = 'year';else if (startview === 0) startview = 'month';

      return {
        label: this._getLabelNode(el).text(),
        value: this.getValue(el),
        valueString: $input.val(),
        min: min,
        max: max,
        language: $input.data('datepicker').language,
        weekstart: $input.data('datepicker').weekStart,
        format: this._formatToString($input.data('datepicker').format),
        startview: startview
      };
    },
    receiveMessage: function receiveMessage(el, data) {
      var $input = $(el).find('input');

      updateLabel(data.label, this._getLabelNode(el));

      if (data.hasOwnProperty('min')) this._setMin($input[0], data.min);

      if (data.hasOwnProperty('max')) this._setMax($input[0], data.max);

      // Must set value only after min and max have been set. If new value is
      // outside the bounds of the previous min/max, then the result will be a
      // blank input.
      if (data.hasOwnProperty('value')) this.setValue(el, data.value);

      $(el).trigger('change');
    },
    subscribe: function subscribe(el, callback) {
      $(el).on('keyup.dateInputBinding input.dateInputBinding', function (event) {
        // Use normal debouncing policy when typing
        callback(true);
      });
      $(el).on('changeDate.dateInputBinding change.dateInputBinding', function (event) {
        // Send immediately when clicked
        callback(false);
      });
    },
    unsubscribe: function unsubscribe(el) {
      $(el).off('.dateInputBinding');
    },
    getRatePolicy: function getRatePolicy() {
      return {
        policy: 'debounce',
        delay: 250
      };
    },
    initialize: function initialize(el) {
      var $input = $(el).find('input');

      var date = $input.data('initial-date');
      // If initial_date is null, set to current date
      if (date === undefined || date === null) {
        // Get local date, but as UTC
        date = this._dateAsUTC(new Date());
      }

      this.setValue(el, date);

      // Set the start and end dates, from min-date and max-date. These always
      // use yyyy-mm-dd format, instead of bootstrap-datepicker's built-in
      // support for date-startdate and data-enddate, which use the current
      // date format.
      if ($input.data('min-date') !== undefined) {
        this._setMin($input[0], $input.data('min-date'));
      }
      if ($input.data('max-date') !== undefined) {
        this._setMax($input[0], $input.data('max-date'));
      }
    },
    _getLabelNode: function _getLabelNode(el) {
      return $(el).find('label[for="' + $escape(el.id) + '"]');
    },
    // Given a format object from a date picker, return a string
    _formatToString: function _formatToString(format) {
      // Format object has structure like:
      // { parts: ['mm', 'dd', 'yy'], separators: ['', '/', '/' ,''] }
      var str = '';
      for (var i = 0; i < format.parts.length; i++) {
        str += format.separators[i] + format.parts[i];
      }
      str += format.separators[i];
      return str;
    },
    // Given an unambiguous date string or a Date object, set the min (start) date.
    // null will unset. undefined will result in no change,
    _setMin: function _setMin(el, date) {
      if (date === undefined) return;
      if (date === null) {
        $(el).bsDatepicker('setStartDate', null);
        return;
      }

      date = this._newDate(date);
      // If date parsing fails, do nothing
      if (date === null) return;

      date = this._UTCDateAsLocal(date);
      if (isNaN(date)) return;
      // Workaround for https://github.com/eternicode/bootstrap-datepicker/issues/2010
      // If the start date when there's a two-digit year format, it will set
      // the date value to null. So we'll save the value, set the start
      // date, and the restore the value.
      var curValue = $(el).bsDatepicker('getUTCDate');
      $(el).bsDatepicker('setStartDate', date);
      $(el).bsDatepicker('setUTCDate', curValue);

      // Workaround for https://github.com/rstudio/shiny/issues/2335
      // We only set the start date *after* the value in this special
      // case so we don't effect the intended behavior of having a blank
      // value when it falls outside the start date
      if (typeof date.toDateString !== 'function') return;
      if (typeof curValue.toDateString !== 'function') return;
      if (date.toDateString() === curValue.toDateString()) {
        $(el).bsDatepicker('setStartDate', null);
        $(el).bsDatepicker('setUTCDate', curValue);
        $(el).bsDatepicker('setStartDate', date);
      }
    },
    // Given an unambiguous date string or a Date object, set the max (end) date
    // null will unset.
    _setMax: function _setMax(el, date) {
      if (date === undefined) return;
      if (date === null) {
        $(el).bsDatepicker('setEndDate', null);
        return;
      }

      date = this._newDate(date);
      // If date parsing fails, do nothing
      if (date === null) return;

      date = this._UTCDateAsLocal(date);
      if (isNaN(date)) return;

      // Workaround for same issue as in _setMin.
      var curValue = $(el).bsDatepicker('getUTCDate');
      $(el).bsDatepicker('setEndDate', date);
      $(el).bsDatepicker('setUTCDate', curValue);

      // Workaround for same issue as in _setMin.
      if (typeof date.toDateString !== 'function') return;
      if (typeof curValue.toDateString !== 'function') return;
      if (date.toDateString() === curValue.toDateString()) {
        $(el).bsDatepicker('setEndDate', null);
        $(el).bsDatepicker('setUTCDate', curValue);
        $(el).bsDatepicker('setEndDate', date);
      }
    },
    // Given a date string of format yyyy-mm-dd, return a Date object with
    // that date at 12AM UTC.
    // If date is a Date object, return it unchanged.
    _newDate: function _newDate(date) {
      if (date instanceof Date) return date;
      if (!date) return null;

      // Get Date object - this will be at 12AM in UTC, but may print
      // differently at the Javascript console.
      var d = parseDate(date);

      // If invalid date, return null
      if (isNaN(d)) return null;

      return new Date(d.getTime());
    },
    // Given a Date object, return a Date object which has the same "clock time"
    // in UTC. For example, if input date is 2013-02-01 23:00:00 GMT-0600 (CST),
    // output will be 2013-02-01 23:00:00 UTC. Note that the JS console may
    // print this in local time, as "Sat Feb 02 2013 05:00:00 GMT-0600 (CST)".
    _dateAsUTC: function _dateAsUTC(date) {
      return new Date(date.getTime() - date.getTimezoneOffset() * 60000);
    },
    // The inverse of _dateAsUTC. This is needed to adjust time zones because
    // some bootstrap-datepicker methods only take local dates as input, and not
    // UTC.
    _UTCDateAsLocal: function _UTCDateAsLocal(date) {
      return new Date(date.getTime() + date.getTimezoneOffset() * 60000);
    }
  });
  inputBindings.register(dateInputBinding, 'shiny.dateInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_daterange.js

  var dateRangeInputBinding = {};
  $.extend(dateRangeInputBinding, dateInputBinding, {
    find: function find(scope) {
      return $(scope).find('.shiny-date-range-input');
    },
    // Return the date in an unambiguous format, yyyy-mm-dd (as opposed to a
    // format like mm/dd/yyyy)
    getValue: function getValue(el) {
      var $inputs = $(el).find('input');
      var start = $inputs.eq(0).bsDatepicker('getUTCDate');
      var end = $inputs.eq(1).bsDatepicker('getUTCDate');

      return [formatDateUTC(start), formatDateUTC(end)];
    },
    // value must be an object, with optional fields `start` and `end`. These
    // should be unambiguous strings like '2001-01-01', or Date objects.
    setValue: function setValue(el, value) {
      if (!(value instanceof Object)) {
        return;
      }

      // Get the start and end input objects
      var $inputs = $(el).find('input');

      // If value is undefined, don't try to set
      // null will remove the current value
      if (value.start !== undefined) {
        if (value.start === null) {
          $inputs.eq(0).val('').bsDatepicker('update');
        } else {
          var start = this._newDate(value.start);
          $inputs.eq(0).bsDatepicker('setUTCDate', start);
        }
      }
      if (value.end !== undefined) {
        if (value.end === null) {
          $inputs.eq(1).val('').bsDatepicker('update');
        } else {
          var end = this._newDate(value.end);
          $inputs.eq(1).bsDatepicker('setUTCDate', end);
        }
      }
    },
    getState: function getState(el) {
      var $el = $(el);
      var $inputs = $el.find('input');
      var $startinput = $inputs.eq(0);
      var $endinput = $inputs.eq(1);

      // For many of the properties, assume start and end have the same values
      var min = $startinput.bsDatepicker('getStartDate');
      var max = $startinput.bsDatepicker('getEndDate');

      // Stringify min and max. If min and max aren't set, they will be
      // -Infinity and Infinity; replace these with null.
      min = min === -Infinity ? null : formatDateUTC(min);
      max = max === Infinity ? null : formatDateUTC(max);

      // startViewMode is stored as a number; convert to string
      var startview = $startinput.data('datepicker').startView;
      if (startview === 2) startview = 'decade';else if (startview === 1) startview = 'year';else if (startview === 0) startview = 'month';

      return {
        label: this._getLabelNode(el).text(),
        value: this.getValue(el),
        valueString: [$startinput.val(), $endinput.val()],
        min: min,
        max: max,
        weekstart: $startinput.data('datepicker').weekStart,
        format: this._formatToString($startinput.data('datepicker').format),
        language: $startinput.data('datepicker').language,
        startview: startview
      };
    },
    receiveMessage: function receiveMessage(el, data) {
      var $el = $(el);
      var $inputs = $el.find('input');
      var $startinput = $inputs.eq(0);
      var $endinput = $inputs.eq(1);

      updateLabel(data.label, this._getLabelNode(el));

      if (data.hasOwnProperty('min')) {
        this._setMin($startinput[0], data.min);
        this._setMin($endinput[0], data.min);
      }

      if (data.hasOwnProperty('max')) {
        this._setMax($startinput[0], data.max);
        this._setMax($endinput[0], data.max);
      }

      // Must set value only after min and max have been set. If new value is
      // outside the bounds of the previous min/max, then the result will be a
      // blank input.
      if (data.hasOwnProperty('value')) this.setValue(el, data.value);

      $el.trigger('change');
    },
    initialize: function initialize(el) {
      var $el = $(el);
      var $inputs = $el.find('input');
      var $startinput = $inputs.eq(0);
      var $endinput = $inputs.eq(1);

      var start = $startinput.data('initial-date');
      var end = $endinput.data('initial-date');

      // If empty/null, use local date, but as UTC
      if (start === undefined || start === null) start = this._dateAsUTC(new Date());

      if (end === undefined || end === null) end = this._dateAsUTC(new Date());

      this.setValue(el, { "start": start, "end": end });

      // // Set the start and end dates, from min-date and max-date. These always
      // // use yyyy-mm-dd format, instead of bootstrap-datepicker's built-in
      // // support for date-startdate and data-enddate, which use the current
      // // date format.
      this._setMin($startinput[0], $startinput.data('min-date'));
      this._setMin($endinput[0], $startinput.data('min-date'));
      this._setMax($startinput[0], $endinput.data('max-date'));
      this._setMax($endinput[0], $endinput.data('max-date'));
    },
    subscribe: function subscribe(el, callback) {
      $(el).on('keyup.dateRangeInputBinding input.dateRangeInputBinding', function (event) {
        // Use normal debouncing policy when typing
        callback(true);
      });
      $(el).on('changeDate.dateRangeInputBinding change.dateRangeInputBinding', function (event) {
        // Send immediately when clicked
        callback(false);
      });
    },
    unsubscribe: function unsubscribe(el) {
      $(el).off('.dateRangeInputBinding');
    },
    _getLabelNode: function _getLabelNode(el) {
      return $(el).find('label[for="' + $escape(el.id) + '"]');
    }
  });
  inputBindings.register(dateRangeInputBinding, 'shiny.dateRangeInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_select.js

  var selectInputBinding = new InputBinding();
  $.extend(selectInputBinding, {
    find: function find(scope) {
      return $(scope).find('select');
    },
    getType: function getType(el) {
      var $el = $(el);
      if (!$el.hasClass("symbol")) {
        // default character type
        return null;
      }
      if ($el.attr("multiple") === "multiple") {
        return 'shiny.symbolList';
      } else {
        return 'shiny.symbol';
      }
    },
    getId: function getId(el) {
      return InputBinding.prototype.getId.call(this, el) || el.name;
    },
    getValue: function getValue(el) {
      return $(el).val();
    },
    setValue: function setValue(el, value) {
      if (!this._is_selectize(el)) {
        $(el).val(value);
      } else {
        var selectize = this._selectize(el);
        if (selectize) {
          selectize.setValue(value);
        }
      }
    },
    getState: function getState(el) {
      // Store options in an array of objects, each with with value and label
      var options = new Array(el.length);
      for (var i = 0; i < el.length; i++) {
        options[i] = { value: el[i].value,
          label: el[i].label };
      }

      return {
        label: this._getLabelNode(el),
        value: this.getValue(el),
        options: options
      };
    },
    receiveMessage: function receiveMessage(el, data) {
      var $el = $(el),
          selectize;

      // This will replace all the options
      if (data.hasOwnProperty('options')) {
        selectize = this._selectize(el);
        // Must destroy selectize before appending new options, otherwise
        // selectize will restore the original select
        if (selectize) selectize.destroy();
        // Clear existing options and add each new one
        $el.empty().append(data.options);
        this._selectize(el);
      }

      // re-initialize selectize
      if (data.hasOwnProperty('config')) {
        $el.parent().find('script[data-for="' + $escape(el.id) + '"]').replaceWith(data.config);
        this._selectize(el, true);
      }

      // use server-side processing for selectize
      if (data.hasOwnProperty('url')) {
        selectize = this._selectize(el);
        selectize.clearOptions();
        var loaded = false;
        selectize.settings.load = function (query, callback) {
          var settings = selectize.settings;
          $.ajax({
            url: data.url,
            data: {
              query: query,
              field: JSON.stringify([settings.searchField]),
              value: settings.valueField,
              conju: settings.searchConjunction,
              maxop: settings.maxOptions
            },
            type: 'GET',
            error: function error() {
              callback();
            },
            success: function success(res) {
              // res = [{label: '1', value: '1', group: '1'}, ...]
              // success is called after options are added, but
              // groups need to be added manually below
              $.each(res, function (index, elem) {
                // Call selectize.addOptionGroup once for each optgroup; the
                // first argument is the group ID, the second is an object with
                // the group's label and value. We use the current settings of
                // the selectize object to decide the fieldnames of that obj.
                var optgroupId = elem[settings.optgroupField || "optgroup"];
                var optgroup = {};
                optgroup[settings.optgroupLabelField || "label"] = optgroupId;
                optgroup[settings.optgroupValueField || "value"] = optgroupId;
                selectize.addOptionGroup(optgroupId, optgroup);
              });
              callback(res);
              if (!loaded) {
                if (data.hasOwnProperty('value')) {
                  selectize.setValue(data.value);
                } else if (settings.maxItems === 1) {
                  // first item selected by default only for single-select
                  selectize.setValue(res[0].value);
                }
              }
              loaded = true;
            }
          });
        };
        // perform an empty search after changing the `load` function
        selectize.load(function (callback) {
          selectize.settings.load.apply(selectize, ['', callback]);
        });
      } else if (data.hasOwnProperty('value')) {
        this.setValue(el, data.value);
      }

      updateLabel(data.label, this._getLabelNode(el));

      $(el).trigger('change');
    },
    subscribe: function subscribe(el, callback) {
      var _this = this;

      $(el).on('change.selectInputBinding', function (event) {
        // https://github.com/rstudio/shiny/issues/2162
        // Prevent spurious events that are gonna be squelched in
        // a second anyway by the onItemRemove down below
        if (el.nonempty && _this.getValue(el) === "") {
          return;
        }
        callback();
      });
    },
    unsubscribe: function unsubscribe(el) {
      $(el).off('.selectInputBinding');
    },
    initialize: function initialize(el) {
      this._selectize(el);
    },
    _getLabelNode: function _getLabelNode(el) {
      var escaped_id = $escape(el.id);
      if (this._is_selectize(el)) {
        escaped_id += "-selectized";
      }
      return $(el).parent().parent().find('label[for="' + escaped_id + '"]');
    },
    // Return true if it's a selectize input, false if it's a regular select input.
    _is_selectize: function _is_selectize(el) {
      var config = $(el).parent().find('script[data-for="' + $escape(el.id) + '"]');
      return config.length > 0;
    },
    _selectize: function _selectize(el, update) {
      if (!$.fn.selectize) return undefined;
      var $el = $(el);
      var config = $el.parent().find('script[data-for="' + $escape(el.id) + '"]');
      if (config.length === 0) return undefined;

      var options = $.extend({
        labelField: 'label',
        valueField: 'value',
        searchField: ['label']
      }, JSON.parse(config.html()));

      // selectize created from selectInput()
      if (typeof config.data('nonempty') !== 'undefined') {
        el.nonempty = true;
        options = $.extend(options, {
          onItemRemove: function onItemRemove(value) {
            if (this.getValue() === "") $("select#" + $escape(el.id)).empty().append($("<option/>", {
              "value": value,
              "selected": true
            })).trigger("change");
          },
          onDropdownClose: function onDropdownClose($dropdown) {
            if (this.getValue() === "") this.setValue($("select#" + $escape(el.id)).val());
          }
        });
      } else {
        el.nonempty = false;
      }
      // options that should be eval()ed
      if (config.data('eval') instanceof Array) $.each(config.data('eval'), function (i, x) {
        /*jshint evil: true*/
        options[x] = eval('(' + options[x] + ')');
      });
      var control = $el.selectize(options)[0].selectize;
      // .selectize() does not really update settings; must destroy and rebuild
      if (update) {
        var settings = $.extend(control.settings, options);
        control.destroy();
        control = $el.selectize(settings)[0].selectize;
      }
      return control;
    }
  });
  inputBindings.register(selectInputBinding, 'shiny.selectInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_radio.js

  var radioInputBinding = new InputBinding();
  $.extend(radioInputBinding, {
    find: function find(scope) {
      return $(scope).find('.shiny-input-radiogroup');
    },
    getValue: function getValue(el) {
      // Select the radio objects that have name equal to the grouping div's id
      return $('input:radio[name="' + $escape(el.id) + '"]:checked').val();
    },
    setValue: function setValue(el, value) {
      $('input:radio[name="' + $escape(el.id) + '"][value="' + $escape(value) + '"]').prop('checked', true);
    },
    getState: function getState(el) {
      var $objs = $('input:radio[name="' + $escape(el.id) + '"]');

      // Store options in an array of objects, each with with value and label
      var options = new Array($objs.length);
      for (var i = 0; i < options.length; i++) {
        options[i] = { value: $objs[i].value,
          label: this._getLabel($objs[i]) };
      }

      return {
        label: this._getLabelNode(el).text(),
        value: this.getValue(el),
        options: options
      };
    },
    receiveMessage: function receiveMessage(el, data) {
      var $el = $(el);

      // This will replace all the options
      if (data.hasOwnProperty('options')) {
        // Clear existing options and add each new one
        $el.find('div.shiny-options-group').remove();
        // Backward compatibility: for HTML generated by shinybootstrap2 package
        $el.find('label.radio').remove();
        $el.append(data.options);
      }

      if (data.hasOwnProperty('value')) this.setValue(el, data.value);

      updateLabel(data.label, this._getLabelNode(el));

      $(el).trigger('change');
    },
    subscribe: function subscribe(el, callback) {
      $(el).on('change.radioInputBinding', function (event) {
        callback();
      });
    },
    unsubscribe: function unsubscribe(el) {
      $(el).off('.radioInputBinding');
    },
    // Get the DOM element that contains the top-level label
    _getLabelNode: function _getLabelNode(el) {
      return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
    },
    // Given an input DOM object, get the associated label. Handles labels
    // that wrap the input as well as labels associated with 'for' attribute.
    _getLabel: function _getLabel(obj) {
      // If <label><input /><span>label text</span></label>
      if (obj.parentNode.tagName === "LABEL") {
        return $.trim($(obj.parentNode).find('span').text());
      }

      return null;
    },
    // Given an input DOM object, set the associated label. Handles labels
    // that wrap the input as well as labels associated with 'for' attribute.
    _setLabel: function _setLabel(obj, value) {
      // If <label><input /><span>label text</span></label>
      if (obj.parentNode.tagName === "LABEL") {
        $(obj.parentNode).find('span').text(value);
      }

      return null;
    }

  });
  inputBindings.register(radioInputBinding, 'shiny.radioInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_checkboxgroup.js

  var checkboxGroupInputBinding = new InputBinding();
  $.extend(checkboxGroupInputBinding, {
    find: function find(scope) {
      return $(scope).find('.shiny-input-checkboxgroup');
    },
    getValue: function getValue(el) {
      // Select the checkbox objects that have name equal to the grouping div's id
      var $objs = $('input:checkbox[name="' + $escape(el.id) + '"]:checked');
      var values = new Array($objs.length);
      for (var i = 0; i < $objs.length; i++) {
        values[i] = $objs[i].value;
      }
      return values;
    },
    setValue: function setValue(el, value) {
      // Clear all checkboxes
      $('input:checkbox[name="' + $escape(el.id) + '"]').prop('checked', false);

      // Accept array
      if (value instanceof Array) {
        for (var i = 0; i < value.length; i++) {
          $('input:checkbox[name="' + $escape(el.id) + '"][value="' + $escape(value[i]) + '"]').prop('checked', true);
        }
        // Else assume it's a single value
      } else {
        $('input:checkbox[name="' + $escape(el.id) + '"][value="' + $escape(value) + '"]').prop('checked', true);
      }
    },
    getState: function getState(el) {
      var $objs = $('input:checkbox[name="' + $escape(el.id) + '"]');

      // Store options in an array of objects, each with with value and label
      var options = new Array($objs.length);
      for (var i = 0; i < options.length; i++) {
        options[i] = { value: $objs[i].value,
          label: this._getLabel($objs[i]) };
      }

      return { label: this._getLabelNode(el).text(),
        value: this.getValue(el),
        options: options
      };
    },
    receiveMessage: function receiveMessage(el, data) {
      var $el = $(el);

      // This will replace all the options
      if (data.hasOwnProperty('options')) {
        // Clear existing options and add each new one
        $el.find('div.shiny-options-group').remove();
        // Backward compatibility: for HTML generated by shinybootstrap2 package
        $el.find('label.checkbox').remove();
        $el.append(data.options);
      }

      if (data.hasOwnProperty('value')) this.setValue(el, data.value);

      updateLabel(data.label, this._getLabelNode(el));

      $(el).trigger('change');
    },
    subscribe: function subscribe(el, callback) {
      $(el).on('change.checkboxGroupInputBinding', function (event) {
        callback();
      });
    },
    unsubscribe: function unsubscribe(el) {
      $(el).off('.checkboxGroupInputBinding');
    },
    // Get the DOM element that contains the top-level label
    _getLabelNode: function _getLabelNode(el) {
      return $(el).find('label[for="' + $escape(el.id) + '"]');
    },
    // Given an input DOM object, get the associated label. Handles labels
    // that wrap the input as well as labels associated with 'for' attribute.
    _getLabel: function _getLabel(obj) {
      // If <label><input /><span>label text</span></label>
      if (obj.parentNode.tagName === "LABEL") {
        return $.trim($(obj.parentNode).find('span').text());
      }

      return null;
    },
    // Given an input DOM object, set the associated label. Handles labels
    // that wrap the input as well as labels associated with 'for' attribute.
    _setLabel: function _setLabel(obj, value) {
      // If <label><input /><span>label text</span></label>
      if (obj.parentNode.tagName === "LABEL") {
        $(obj.parentNode).find('span').text(value);
      }

      return null;
    }

  });
  inputBindings.register(checkboxGroupInputBinding, 'shiny.checkboxGroupInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_actionbutton.js

  var actionButtonInputBinding = new InputBinding();
  $.extend(actionButtonInputBinding, {
    find: function find(scope) {
      return $(scope).find(".action-button");
    },
    getValue: function getValue(el) {
      return $(el).data('val') || 0;
    },
    setValue: function setValue(el, value) {
      $(el).data('val', value);
    },
    getType: function getType(el) {
      return 'shiny.action';
    },
    subscribe: function subscribe(el, callback) {
      $(el).on("click.actionButtonInputBinding", function (e) {
        var $el = $(this);
        var val = $el.data('val') || 0;
        $el.data('val', val + 1);

        callback();
      });
    },
    getState: function getState(el) {
      return { value: this.getValue(el) };
    },
    receiveMessage: function receiveMessage(el, data) {
      var $el = $(el);

      // retrieve current label and icon
      var label = $el.text();
      var icon = '';

      // to check (and store) the previous icon, we look for a $el child
      // object that has an i tag, and some (any) class (this prevents
      // italicized text - which has an i tag but, usually, no class -
      // from being mistakenly selected)
      if ($el.find('i[class]').length > 0) {
        var icon_html = $el.find('i[class]')[0];
        if (icon_html === $el.children()[0]) {
          // another check for robustness
          icon = $(icon_html).prop('outerHTML');
        }
      }

      // update the requested properties
      if (data.hasOwnProperty('label')) label = data.label;
      if (data.hasOwnProperty('icon')) {
        icon = data.icon;
        // if the user entered icon=character(0), remove the icon
        if (icon.length === 0) icon = '';
      }

      // produce new html
      $el.html(icon + ' ' + label);
    },
    unsubscribe: function unsubscribe(el) {
      $(el).off(".actionButtonInputBinding");
    }
  });
  inputBindings.register(actionButtonInputBinding, 'shiny.actionButtonInput');

  $(document).on('click', 'a.action-button', function (e) {
    e.preventDefault();
  });

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_tabinput.js

  var bootstrapTabInputBinding = new InputBinding();
  $.extend(bootstrapTabInputBinding, {
    find: function find(scope) {
      return $(scope).find('ul.nav.shiny-tab-input');
    },
    getValue: function getValue(el) {
      var anchor = $(el).find('li:not(.dropdown).active').children('a');
      if (anchor.length === 1) return this._getTabName(anchor);

      return null;
    },
    setValue: function setValue(el, value) {
      var self = this;
      var success = false;
      if (value) {
        var anchors = $(el).find('li:not(.dropdown)').children('a');
        anchors.each(function () {
          if (self._getTabName($(this)) === value) {
            $(this).tab('show');
            success = true;
            return false; // Break out of each()
          }
          return true;
        });
      }
      if (!success) {
        // This is to handle the case where nothing is selected, e.g. the last tab
        // was removed using removeTab.
        $(el).trigger("change");
      }
    },
    getState: function getState(el) {
      return { value: this.getValue(el) };
    },
    receiveMessage: function receiveMessage(el, data) {
      if (data.hasOwnProperty('value')) this.setValue(el, data.value);
    },
    subscribe: function subscribe(el, callback) {
      $(el).on('change shown.bootstrapTabInputBinding shown.bs.tab.bootstrapTabInputBinding', function (event) {
        callback();
      });
    },
    unsubscribe: function unsubscribe(el) {
      $(el).off('.bootstrapTabInputBinding');
    },
    _getTabName: function _getTabName(anchor) {
      return anchor.attr('data-value') || anchor.text();
    }
  });
  inputBindings.register(bootstrapTabInputBinding, 'shiny.bootstrapTabInput');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/input_binding_fileinput.js

  var IE8FileUploader = function IE8FileUploader(shinyapp, id, fileEl) {
    this.shinyapp = shinyapp;
    this.id = id;
    this.fileEl = fileEl;
    this.beginUpload();
  };
  (function () {
    this.beginUpload = function () {
      var self = this;
      // Create invisible frame
      var iframeId = 'shinyupload_iframe_' + this.id;
      this.iframe = document.createElement('iframe');
      this.iframe.id = iframeId;
      this.iframe.name = iframeId;
      this.iframe.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 0; height: 0; border: none');
      $(document.body).append(this.iframe);
      var iframeDestroy = function iframeDestroy() {
        // Forces Shiny to flushReact, flush outputs, etc. Without this we get
        // invalidated reactives, but observers don't actually execute.
        self.shinyapp.makeRequest('uploadieFinish', [], function () {}, function () {});
        $(self.iframe).remove();
        // Reset the file input's value to "". This allows the same file to be
        // uploaded again. https://stackoverflow.com/a/22521275
        $(self.fileEl).val("");
      };
      if (this.iframe.attachEvent) {
        this.iframe.attachEvent('onload', iframeDestroy);
      } else {
        this.iframe.onload = iframeDestroy;
      }

      this.form = document.createElement('form');
      this.form.method = 'POST';
      this.form.setAttribute('enctype', 'multipart/form-data');
      this.form.action = "session/" + encodeURI(this.shinyapp.config.sessionId) + "/uploadie/" + encodeURI(this.id);
      this.form.id = 'shinyupload_form_' + this.id;
      this.form.target = iframeId;
      $(this.form).insertAfter(this.fileEl).append(this.fileEl);
      this.form.submit();
    };
  }).call(IE8FileUploader.prototype);

  var FileUploader = function FileUploader(shinyapp, id, files, el) {
    this.shinyapp = shinyapp;
    this.id = id;
    this.el = el;
    FileProcessor.call(this, files);
  };
  $.extend(FileUploader.prototype, FileProcessor.prototype);
  (function () {
    this.makeRequest = function (method, args, onSuccess, onFailure, blobs) {
      this.shinyapp.makeRequest(method, args, onSuccess, onFailure, blobs);
    };
    this.onBegin = function (files, cont) {
      var self = this;

      // Reset progress bar
      this.$setError(null);
      this.$setActive(true);
      this.$setVisible(true);
      this.onProgress(null, 0);

      this.totalBytes = 0;
      this.progressBytes = 0;
      $.each(files, function (i, file) {
        self.totalBytes += file.size;
      });

      var fileInfo = $.map(files, function (file, i) {
        return {
          name: file.name,
          size: file.size,
          type: file.type
        };
      });

      this.makeRequest('uploadInit', [fileInfo], function (response) {
        self.jobId = response.jobId;
        self.uploadUrl = response.uploadUrl;
        cont();
      }, function (error) {
        self.onError(error);
      });
    };
    this.onFile = function (file, cont) {
      var self = this;
      this.onProgress(file, 0);

      $.ajax(this.uploadUrl, {
        type: 'POST',
        cache: false,
        xhr: function xhr() {
          var xhrVal = $.ajaxSettings.xhr();
          if (xhrVal.upload) {
            xhrVal.upload.onprogress = function (e) {
              if (e.lengthComputable) {
                self.onProgress(file, (self.progressBytes + e.loaded) / self.totalBytes);
              }
            };
          }
          return xhrVal;
        },
        data: file,
        contentType: 'application/octet-stream',
        processData: false,
        success: function success() {
          self.progressBytes += file.size;
          cont();
        },
        error: function error(jqXHR, textStatus, errorThrown) {
          self.onError(jqXHR.responseText || textStatus);
        }
      });
    };
    this.onComplete = function () {
      var self = this;

      var fileInfo = $.map(this.files, function (file, i) {
        return {
          name: file.name,
          size: file.size,
          type: file.type
        };
      });

      // Trigger shiny:inputchanged. Unlike a normal shiny:inputchanged event,
      // it's not possible to modify the information before the values get
      // sent to the server.
      var evt = jQuery.Event("shiny:inputchanged");
      evt.name = this.id;
      evt.value = fileInfo;
      evt.binding = fileInputBinding;
      evt.el = this.el;
      evt.inputType = 'shiny.fileupload';
      $(document).trigger(evt);

      this.makeRequest('uploadEnd', [this.jobId, this.id], function (response) {
        self.$setActive(false);
        self.onProgress(null, 1);
        self.$bar().text('Upload complete');
        // Reset the file input's value to "". This allows the same file to be
        // uploaded again. https://stackoverflow.com/a/22521275
        $(evt.el).val("");
      }, function (error) {
        self.onError(error);
      });
      this.$bar().text('Finishing upload');
    };
    this.onError = function (message) {
      this.$setError(message || '');
      this.$setActive(false);
    };
    this.onAbort = function () {
      this.$setVisible(false);
    };
    this.onProgress = function (file, completed) {
      this.$bar().width(Math.round(completed * 100) + '%');
      this.$bar().text(file ? file.name : '');
    };
    this.$container = function () {
      return $('#' + $escape(this.id) + '_progress.shiny-file-input-progress');
    };
    this.$bar = function () {
      return $('#' + $escape(this.id) + '_progress.shiny-file-input-progress .progress-bar');
    };
    this.$setVisible = function (visible) {
      this.$container().css('visibility', visible ? 'visible' : 'hidden');
    };
    this.$setError = function (error) {
      this.$bar().toggleClass('progress-bar-danger', error !== null);
      if (error !== null) {
        this.onProgress(null, 1);
        this.$bar().text(error);
      }
    };
    this.$setActive = function (active) {
      this.$container().toggleClass('active', !!active);
    };
  }).call(FileUploader.prototype);

  // NOTE On Safari, at least version 10.1.2, *if the developer console is open*,
  // setting the input's value will behave strangely because of a Safari bug. The
  // uploaded file's name will appear over the placeholder value, instead of
  // replacing it. The workaround is to restart Safari. When I (Alan Dipert) ran
  // into this bug Winston Chang helped me diagnose the exact problem, and Winston
  // then submitted a bug report to Apple.
  function setFileText($el, files) {
    var $fileText = $el.closest('div.input-group').find('input[type=text]');
    if (files.length === 1) {
      $fileText.val(files[0].name);
    } else {
      $fileText.val(files.length + " files");
    }
  }

  // If previously selected files are uploading, abort that.
  function abortCurrentUpload($el) {
    var uploader = $el.data('currentUploader');
    if (uploader) uploader.abort();
    // Clear data-restore attribute if present.
    $el.removeAttr('data-restore');
  }

  function uploadDroppedFilesIE10Plus(el, files) {
    var $el = $(el);
    abortCurrentUpload($el);

    // Set the label in the text box
    setFileText($el, files);

    // Start the new upload and put the uploader in 'currentUploader'.
    $el.data('currentUploader', new FileUploader(exports.shinyapp, fileInputBinding.getId(el), files, el));
  }

  function uploadFiles(evt) {
    var $el = $(evt.target);
    abortCurrentUpload($el);

    var files = evt.target.files;
    // IE8 here does not necessarily mean literally IE8; it indicates if the web
    // browser supports the FileList object (IE8/9 do not support it)
    var IE8 = typeof files === 'undefined';
    var id = fileInputBinding.getId(evt.target);

    if (!IE8 && files.length === 0) return;

    // Set the label in the text box
    var $fileText = $el.closest('div.input-group').find('input[type=text]');
    if (IE8) {
      // If we're using IE8/9, just use this placeholder
      $fileText.val("[Uploaded file]");
    } else {
      setFileText($el, files);
    }

    // Start the new upload and put the uploader in 'currentUploader'.
    if (IE8) {
      /*jshint nonew:false */
      new IE8FileUploader(exports.shinyapp, id, evt.target);
    } else {
      $el.data('currentUploader', new FileUploader(exports.shinyapp, id, files, evt.target));
    }
  }

  // Here we maintain a list of all the current file inputs. This is necessary
  // because we need to trigger events on them in order to respond to file drag
  // events. For example, they should all light up when a file is dragged on to
  // the page.
  var $fileInputs = $();

  var fileInputBinding = new InputBinding();
  $.extend(fileInputBinding, {
    find: function find(scope) {
      return $(scope).find('input[type="file"]');
    },
    getId: function getId(el) {
      return InputBinding.prototype.getId.call(this, el) || el.name;
    },
    getValue: function getValue(el) {
      // This returns a non-undefined value only when there's a 'data-restore'
      // attribute, which is set only when restoring Shiny state. If a file is
      // uploaded through the browser, 'data-restore' gets cleared.
      var data = $(el).attr('data-restore');
      if (data) {
        data = JSON.parse(data);

        // Set the label in the text box
        var $fileText = $(el).closest('div.input-group').find('input[type=text]');
        if (data.name.length === 1) {
          $fileText.val(data.name[0]);
        } else {
          $fileText.val(data.name.length + " files");
        }

        // Manually set up progress bar. A bit inelegant because it duplicates
        // code from FileUploader, but duplication is less bad than alternatives.
        var $progress = $(el).closest('div.form-group').find('.progress');
        var $bar = $progress.find('.progress-bar');
        $progress.removeClass('active');
        $bar.width('100%');
        $bar.css('visibility', 'visible');

        return data;
      } else {
        return null;
      }
    },
    setValue: function setValue(el, value) {
      // Not implemented
    },
    getType: function getType(el) {
      // This will be used only when restoring a file from a saved state.
      return 'shiny.file';
    },
    _zoneOf: function _zoneOf(el) {
      return $(el).closest("div.input-group");
    },
    // This function makes it possible to attach listeners to the dragenter,
    // dragleave, and drop events of a single element with children. It's not
    // intuitive to do directly because outer elements fire "dragleave" events
    // both when the drag leaves the element and when the drag enters a child. To
    // make it easier, we maintain a count of the elements being dragged across
    // and trigger 3 new types of event:
    //
    // 1. draghover:enter - When a drag enters el and any of its children.
    // 2. draghover:leave - When the drag leaves el and all of its children.
    // 3. draghover:drop - When an item is dropped on el or any of its children.
    _enableDraghover: function _enableDraghover(el) {
      var $el = $(el),
          childCounter = 0;
      $el.on({
        "dragenter.draghover": function dragenterDraghover(e) {
          if (childCounter++ === 0) {
            $el.trigger("draghover:enter", e);
          }
        },
        "dragleave.draghover": function dragleaveDraghover(e) {
          if (--childCounter === 0) {
            $el.trigger("draghover:leave", e);
          }
          if (childCounter < 0) {
            console.error("draghover childCounter is negative somehow");
          }
        },
        "dragover.draghover": function dragoverDraghover(e) {
          e.preventDefault();
        },
        "drop.draghover": function dropDraghover(e) {
          childCounter = 0;
          $el.trigger("draghover:drop", e);
          e.preventDefault();
        }
      });
      return $el;
    },
    _disableDraghover: function _disableDraghover(el) {
      return $(el).off(".draghover");
    },
    _ZoneClass: {
      ACTIVE: "shiny-file-input-active",
      OVER: "shiny-file-input-over"
    },
    _enableDocumentEvents: function _enableDocumentEvents() {
      var _this2 = this;

      var $doc = $("html"),
          _ZoneClass = this._ZoneClass,
          ACTIVE = _ZoneClass.ACTIVE,
          OVER = _ZoneClass.OVER;

      this._enableDraghover($doc).on({
        "draghover:enter.draghover": function draghoverEnterDraghover(e) {
          _this2._zoneOf($fileInputs).addClass(ACTIVE);
        },
        "draghover:leave.draghover": function draghoverLeaveDraghover(e) {
          _this2._zoneOf($fileInputs).removeClass(ACTIVE);
        },
        "draghover:drop.draghover": function draghoverDropDraghover(e) {
          _this2._zoneOf($fileInputs).removeClass(OVER).removeClass(ACTIVE);
        }
      });
    },
    _disableDocumentEvents: function _disableDocumentEvents() {
      var $doc = $("html");
      $doc.off(".draghover");
      this._disableDraghover($doc);
    },
    _canSetFiles: function _canSetFiles(fileList) {
      var testEl = document.createElement("input");
      testEl.type = "file";
      try {
        testEl.files = fileList;
      } catch (e) {
        return false;
      }
      return true;
    },
    _handleDrop: function _handleDrop(e, el) {
      var files = e.originalEvent.dataTransfer.files,
          $el = $(el);
      if (files === undefined || files === null) {
        // 1. The FileList object isn't supported by this browser, and
        // there's nothing else we can try. (< IE 10)
        console.log("Dropping files is not supported on this browser. (no FileList)");
      } else if (!this._canSetFiles(files)) {
        // 2. The browser doesn't support assigning a type=file input's .files
        // property, but we do have a FileList to work with. (IE10+/Edge)
        $el.val("");
        uploadDroppedFilesIE10Plus(el, files);
      } else {
        // 3. The browser supports FileList and input.files assignment.
        // (Chrome, Safari)
        $el.val("");
        el.files = e.originalEvent.dataTransfer.files;
        // Recent versions of Firefox (57+, or "Quantum" and beyond) don't seem to
        // automatically trigger a change event, so we trigger one manually here.
        // On browsers that do trigger change, this operation appears to be
        // idempotent, as el.files doesn't change between events.
        $el.trigger("change");
      }
    },
    _isIE9: function _isIE9() {
      try {
        return window.navigator.userAgent.match(/MSIE 9\./) && true || false;
      } catch (e) {
        return false;
      }
    },
    subscribe: function subscribe(el, callback) {
      var _this3 = this;

      $(el).on("change.fileInputBinding", uploadFiles);
      // Here we try to set up the necessary events for Drag and Drop ("DnD") on
      // every browser except IE9. We specifically exclude IE9 because it's one
      // browser that supports just enough of the functionality we need to be
      // confusing. In particular, it supports drag events, so drop zones will
      // highlight when a file is dragged into the browser window. It doesn't
      // support the FileList object though, so the user's expectation that DnD is
      // supported based on this highlighting would be incorrect.
      if (!this._isIE9()) {
        if ($fileInputs.length === 0) this._enableDocumentEvents();
        $fileInputs = $fileInputs.add(el);
        var $zone = this._zoneOf(el),
            OVER = this._ZoneClass.OVER;

        this._enableDraghover($zone).on({
          "draghover:enter.draghover": function draghoverEnterDraghover(e) {
            $zone.addClass(OVER);
          },
          "draghover:leave.draghover": function draghoverLeaveDraghover(e) {
            $zone.removeClass(OVER);
            // Prevent this event from bubbling to the document handler,
            // which would deactivate all zones.
            e.stopPropagation();
          },
          "draghover:drop.draghover": function draghoverDropDraghover(e, dropEvent) {
            _this3._handleDrop(dropEvent, el);
          }
        });
      }
    },

    unsubscribe: function unsubscribe(el) {
      var $el = $(el),
          $zone = this._zoneOf(el);

      $zone.removeClass(this._ZoneClass.OVER).removeClass(this._ZoneClass.ACTIVE);

      this._disableDraghover($zone);
      $el.off(".fileInputBinding");
      $zone.off(".draghover");

      // Remove el from list of inputs and (maybe) clean up global event handlers.
      $fileInputs = $fileInputs.not(el);
      if ($fileInputs.length === 0) this._disableDocumentEvents();
    }
  });
  inputBindings.register(fileInputBinding, 'shiny.fileInputBinding');

  //---------------------------------------------------------------------
  // Source file: ../srcjs/init_shiny.js

  function initShiny() {

    var shinyapp = exports.shinyapp = new ShinyApp();

    function bindOutputs() {
      var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;

      scope = $(scope);

      var bindings = outputBindings.getBindings();

      for (var i = 0; i < bindings.length; i++) {
        var binding = bindings[i].binding;
        var matches = binding.find(scope) || [];
        for (var j = 0; j < matches.length; j++) {
          var el = matches[j];
          var id = binding.getId(el);

          // Check if ID is falsy
          if (!id) continue;

          // In some uncommon cases, elements that are later in the
          // matches array can be removed from the document by earlier
          // iterations. See https://github.com/rstudio/shiny/issues/1399
          if (!$.contains(document, el)) continue;

          var $el = $(el);
          if ($el.hasClass('shiny-bound-output')) {
            // Already bound; can happen with nested uiOutput (bindAll
            // gets called on two ancestors)
            continue;
          }

          var bindingAdapter = new OutputBindingAdapter(el, binding);
          shinyapp.bindOutput(id, bindingAdapter);
          $el.data('shiny-output-binding', bindingAdapter);
          $el.addClass('shiny-bound-output');
          $el.trigger({
            type: 'shiny:bound',
            binding: binding,
            bindingType: 'output'
          });
        }
      }

      // Send later in case DOM layout isn't final yet.
      setTimeout(sendImageSize, 0);
      setTimeout(sendOutputHiddenState, 0);
    }

    function unbindOutputs() {
      var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
      var includeSelf = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;

      var outputs = $(scope).find('.shiny-bound-output');

      if (includeSelf && $(scope).hasClass('shiny-bound-output')) {
        outputs.push(scope);
      }

      for (var i = 0; i < outputs.length; i++) {
        var $el = $(outputs[i]);
        var bindingAdapter = $el.data('shiny-output-binding');
        if (!bindingAdapter) continue;
        var id = bindingAdapter.binding.getId(outputs[i]);
        shinyapp.unbindOutput(id, bindingAdapter);
        $el.removeClass('shiny-bound-output');
        $el.removeData('shiny-output-binding');
        $el.trigger({
          type: 'shiny:unbound',
          binding: bindingAdapter.binding,
          bindingType: 'output'
        });
      }

      // Send later in case DOM layout isn't final yet.
      setTimeout(sendImageSize, 0);
      setTimeout(sendOutputHiddenState, 0);
    }

    var inputBatchSender = new InputBatchSender(shinyapp);
    var inputsNoResend = new InputNoResendDecorator(inputBatchSender);
    var inputsEvent = new InputEventDecorator(inputsNoResend);
    var inputsRate = new InputRateDecorator(inputsEvent);
    var inputsDefer = new InputDeferDecorator(inputsEvent);

    var inputs;
    if ($('input[type="submit"], button[type="submit"]').length > 0) {
      // If there is a submit button on the page, use defer decorator
      inputs = inputsDefer;

      $('input[type="submit"], button[type="submit"]').each(function () {
        $(this).click(function (event) {
          event.preventDefault();
          inputsDefer.submit();
        });
      });
    } else {
      // By default, use rate decorator
      inputs = inputsRate;
    }

    inputs = new InputValidateDecorator(inputs);

    exports.setInputValue = exports.onInputChange = function (name, value, opts) {
      opts = addDefaultInputOpts(opts);
      inputs.setInput(name, value, opts);
    };

    var boundInputs = {};

    function valueChangeCallback(binding, el, allowDeferred) {
      var id = binding.getId(el);
      if (id) {
        var value = binding.getValue(el);
        var type = binding.getType(el);
        if (type) id = id + ":" + type;

        var opts = {
          priority: allowDeferred ? "deferred" : "immediate",
          binding: binding,
          el: el
        };
        inputs.setInput(id, value, opts);
      }
    }

    function bindInputs() {
      var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;

      var bindings = inputBindings.getBindings();

      var inputItems = {};

      for (var i = 0; i < bindings.length; i++) {
        var binding = bindings[i].binding;
        var matches = binding.find(scope) || [];
        for (var j = 0; j < matches.length; j++) {
          var el = matches[j];
          var id = binding.getId(el);

          // Check if ID is falsy, or if already bound
          if (!id || boundInputs[id]) continue;

          var type = binding.getType(el);
          var effectiveId = type ? id + ":" + type : id;
          inputItems[effectiveId] = {
            value: binding.getValue(el),
            opts: {
              immediate: true,
              binding: binding,
              el: el
            }
          };

          /*jshint loopfunc:true*/
          var thisCallback = function () {
            var thisBinding = binding;
            var thisEl = el;
            return function (allowDeferred) {
              valueChangeCallback(thisBinding, thisEl, allowDeferred);
            };
          }();

          binding.subscribe(el, thisCallback);
          $(el).data('shiny-input-binding', binding);
          $(el).addClass('shiny-bound-input');
          var ratePolicy = binding.getRatePolicy(el);
          if (ratePolicy !== null) {
            inputsRate.setRatePolicy(effectiveId, ratePolicy.policy, ratePolicy.delay);
          }

          boundInputs[id] = {
            binding: binding,
            node: el
          };

          $(el).trigger({
            type: 'shiny:bound',
            binding: binding,
            bindingType: 'input'
          });
        }
      }

      return inputItems;
    }

    function unbindInputs() {
      var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
      var includeSelf = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;

      var inputs = $(scope).find('.shiny-bound-input');

      if (includeSelf && $(scope).hasClass('shiny-bound-input')) {
        inputs.push(scope);
      }

      for (var i = 0; i < inputs.length; i++) {
        var el = inputs[i];
        var binding = $(el).data('shiny-input-binding');
        if (!binding) continue;
        var id = binding.getId(el);
        $(el).removeClass('shiny-bound-input');
        delete boundInputs[id];
        binding.unsubscribe(el);
        $(el).trigger({
          type: 'shiny:unbound',
          binding: binding,
          bindingType: 'input'
        });
      }
    }

    function _bindAll(scope) {
      bindOutputs(scope);
      return bindInputs(scope);
    }
    function unbindAll(scope) {
      var includeSelf = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;

      unbindInputs(scope, includeSelf);
      unbindOutputs(scope, includeSelf);
    }
    exports.bindAll = function (scope) {
      // _bindAll returns input values; it doesn't send them to the server.
      // export.bindAll needs to send the values to the server.
      var currentInputItems = _bindAll(scope);
      $.each(currentInputItems, function (name, item) {
        inputs.setInput(name, item.value, item.opts);
      });

      // Not sure if the iframe stuff is an intrinsic part of bindAll, but bindAll
      // is a convenient place to hang it. bindAll will be called anytime new HTML
      // appears that might contain inputs/outputs; it's reasonable to assume that
      // any such HTML may contain iframes as well.
      initDeferredIframes();
    };
    exports.unbindAll = unbindAll;

    // Calls .initialize() for all of the input objects in all input bindings,
    // in the given scope.
    function initializeInputs() {
      var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;

      var bindings = inputBindings.getBindings();

      // Iterate over all bindings
      for (var i = 0; i < bindings.length; i++) {
        var binding = bindings[i].binding;
        var inputObjects = binding.find(scope) || [];

        // Iterate over all input objects for this binding
        for (var j = 0; j < inputObjects.length; j++) {
          if (!inputObjects[j]._shiny_initialized) {
            inputObjects[j]._shiny_initialized = true;
            binding.initialize(inputObjects[j]);
          }
        }
      }
    }
    exports.initializeInputs = initializeInputs;

    function getIdFromEl(el) {
      var $el = $(el);
      var bindingAdapter = $el.data("shiny-output-binding");
      if (!bindingAdapter) return null;else return bindingAdapter.getId();
    }

    // Initialize all input objects in the document, before binding
    initializeInputs(document);

    // The input values returned by _bindAll() each have a structure like this:
    //   { value: 123, opts: { ... } }
    // We want to only keep the value. This is because when the initialValues is
    // passed to ShinyApp.connect(), the ShinyApp object stores the
    // initialValues object for the duration of the session, and the opts may
    // have a reference to the DOM element, which would prevent it from being
    // GC'd.
    var initialValues = mapValues(_bindAll(document), function (x) {
      return x.value;
    });

    // The server needs to know the size of each image and plot output element,
    // in case it is auto-sizing
    $('.shiny-image-output, .shiny-plot-output, .shiny-report-size').each(function () {
      var id = getIdFromEl(this);
      if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
        initialValues['.clientdata_output_' + id + '_width'] = this.offsetWidth;
        initialValues['.clientdata_output_' + id + '_height'] = this.offsetHeight;
      }
    });
    function doSendImageSize() {
      $('.shiny-image-output, .shiny-plot-output, .shiny-report-size').each(function () {
        var id = getIdFromEl(this);
        if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
          inputs.setInput('.clientdata_output_' + id + '_width', this.offsetWidth);
          inputs.setInput('.clientdata_output_' + id + '_height', this.offsetHeight);
        }
      });
      $('.shiny-bound-output').each(function () {
        var $this = $(this),
            binding = $this.data('shiny-output-binding');
        $this.trigger({
          type: 'shiny:visualchange',
          visible: !isHidden(this),
          binding: binding
        });
        binding.onResize();
      });
    }
    var sendImageSizeDebouncer = new Debouncer(null, doSendImageSize, 0);
    function sendImageSize() {
      sendImageSizeDebouncer.normalCall();
    }
    // Make sure sendImageSize actually gets called before the inputBatchSender
    // sends data to the server.
    inputBatchSender.lastChanceCallback.push(function () {
      if (sendImageSizeDebouncer.isPending()) sendImageSizeDebouncer.immediateCall();
    });

    // Return true if the object or one of its ancestors in the DOM tree has
    // style='display:none'; otherwise return false.
    function isHidden(obj) {
      // null means we've hit the top of the tree. If width or height is
      // non-zero, then we know that no ancestor has display:none.
      if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
        return false;
      } else if (getStyle(obj, 'display') === 'none') {
        return true;
      } else {
        return isHidden(obj.parentNode);
      }
    }
    var lastKnownVisibleOutputs = {};
    // Set initial state of outputs to hidden, if needed
    $('.shiny-bound-output').each(function () {
      var id = getIdFromEl(this);
      if (isHidden(this)) {
        initialValues['.clientdata_output_' + id + '_hidden'] = true;
      } else {
        lastKnownVisibleOutputs[id] = true;
        initialValues['.clientdata_output_' + id + '_hidden'] = false;
      }
    });
    // Send update when hidden state changes
    function doSendOutputHiddenState() {
      var visibleOutputs = {};
      $('.shiny-bound-output').each(function () {
        var id = getIdFromEl(this);
        delete lastKnownVisibleOutputs[id];
        // Assume that the object is hidden when width and height are 0
        var hidden = isHidden(this),
            evt = {
          type: 'shiny:visualchange',
          visible: !hidden
        };
        if (hidden) {
          inputs.setInput('.clientdata_output_' + id + '_hidden', true);
        } else {
          visibleOutputs[id] = true;
          inputs.setInput('.clientdata_output_' + id + '_hidden', false);
        }
        var $this = $(this);
        evt.binding = $this.data('shiny-output-binding');
        $this.trigger(evt);
      });
      // Anything left in lastKnownVisibleOutputs is orphaned
      for (var name in lastKnownVisibleOutputs) {
        if (lastKnownVisibleOutputs.hasOwnProperty(name)) inputs.setInput('.clientdata_output_' + name + '_hidden', true);
      }
      // Update the visible outputs for next time
      lastKnownVisibleOutputs = visibleOutputs;
    }
    // sendOutputHiddenState gets called each time DOM elements are shown or
    // hidden. This can be in the hundreds or thousands of times at startup.
    // We'll debounce it, so that we do the actual work once per tick.
    var sendOutputHiddenStateDebouncer = new Debouncer(null, doSendOutputHiddenState, 0);
    function sendOutputHiddenState() {
      sendOutputHiddenStateDebouncer.normalCall();
    }
    // We need to make sure doSendOutputHiddenState actually gets called before
    // the inputBatchSender sends data to the server. The lastChanceCallback
    // here does that - if the debouncer has a pending call, flush it.
    inputBatchSender.lastChanceCallback.push(function () {
      if (sendOutputHiddenStateDebouncer.isPending()) sendOutputHiddenStateDebouncer.immediateCall();
    });

    // Given a namespace and a handler function, return a function that invokes
    // the handler only when e's namespace matches. For example, if the
    // namespace is "bs", it would match when e.namespace is "bs" or "bs.tab".
    // If the namespace is "bs.tab", it would match for "bs.tab", but not "bs".
    function filterEventsByNamespace(namespace, handler) {
      namespace = namespace.split(".");

      return function (e) {
        var eventNamespace = e.namespace.split(".");

        // If any of the namespace strings aren't present in this event, quit.
        for (var i = 0; i < namespace.length; i++) {
          if (eventNamespace.indexOf(namespace[i]) === -1) return;
        }

        handler.apply(this, arguments);
      };
    }

    // The size of each image may change either because the browser window was
    // resized, or because a tab was shown/hidden (hidden elements report size
    // of 0x0). It's OK to over-report sizes because the input pipeline will
    // filter out values that haven't changed.
    $(window).resize(debounce(500, sendImageSize));
    // Need to register callbacks for each Bootstrap 3 class.
    var bs3classes = ['modal', 'dropdown', 'tab', 'tooltip', 'popover', 'collapse'];
    $.each(bs3classes, function (idx, classname) {
      $(document.body).on('shown.bs.' + classname + '.sendImageSize', '*', filterEventsByNamespace('bs', sendImageSize));
      $(document.body).on('shown.bs.' + classname + '.sendOutputHiddenState ' + 'hidden.bs.' + classname + '.sendOutputHiddenState', '*', filterEventsByNamespace('bs', sendOutputHiddenState));
    });

    // This is needed for Bootstrap 2 compatibility and for non-Bootstrap
    // related shown/hidden events (like conditionalPanel)
    $(document.body).on('shown.sendImageSize', '*', sendImageSize);
    $(document.body).on('shown.sendOutputHiddenState hidden.sendOutputHiddenState', '*', sendOutputHiddenState);

    // Send initial pixel ratio, and update it if it changes
    initialValues['.clientdata_pixelratio'] = pixelRatio();
    $(window).resize(function () {
      inputs.setInput('.clientdata_pixelratio', pixelRatio());
    });

    // Send initial URL
    initialValues['.clientdata_url_protocol'] = window.location.protocol;
    initialValues['.clientdata_url_hostname'] = window.location.hostname;
    initialValues['.clientdata_url_port'] = window.location.port;
    initialValues['.clientdata_url_pathname'] = window.location.pathname;

    // Send initial URL search (query string) and update it if it changes
    initialValues['.clientdata_url_search'] = window.location.search;

    $(window).on('pushstate', function (e) {
      inputs.setInput('.clientdata_url_search', window.location.search);
    });

    $(window).on('popstate', function (e) {
      inputs.setInput('.clientdata_url_search', window.location.search);
    });

    // This is only the initial value of the hash. The hash can change, but
    // a reactive version of this isn't sent because watching for changes can
    // require polling on some browsers. The JQuery hashchange plugin can be
    // used if this capability is important.
    initialValues['.clientdata_url_hash_initial'] = window.location.hash;
    initialValues['.clientdata_url_hash'] = window.location.hash;

    $(window).on('hashchange', function (e) {
      inputs.setInput('.clientdata_url_hash', window.location.hash);
    });

    // The server needs to know what singletons were rendered as part of
    // the page loading
    var singletonText = initialValues['.clientdata_singletons'] = $('script[type="application/shiny-singletons"]').text();
    singletons.registerNames(singletonText.split(/,/));

    var dependencyText = $('script[type="application/html-dependencies"]').text();
    $.each(dependencyText.split(/;/), function (i, depStr) {
      var match = /\s*^(.+)\[(.+)\]\s*$/.exec(depStr);
      if (match) {
        registerDependency(match[1], match[2]);
      }
    });

    // IE8 and IE9 have some limitations with data URIs
    initialValues['.clientdata_allowDataUriScheme'] = typeof WebSocket !== 'undefined';

    // We've collected all the initial values--start the server process!
    inputsNoResend.reset(initialValues);
    shinyapp.connect(initialValues);
    $(document).one("shiny:connected", function () {
      initDeferredIframes();
    });
  } // function initShiny()


  // Give any deferred iframes a chance to load.
  function initDeferredIframes() {
    if (!window.Shiny || !window.Shiny.shinyapp || !window.Shiny.shinyapp.isConnected()) {
      // If somehow we accidentally call this before the server connection is
      // established, just ignore the call. At the time of this writing it
      // doesn't happen, but it's easy to imagine a later refactoring putting
      // us in this situation and it'd be hard to notice with either manual
      // testing or automated tests, because the only effect is on HTTP request
      // timing. (Update: Actually Aron saw this being called without even
      // window.Shiny being defined, but it was hard to repro.)
      return;
    }

    $(".shiny-frame-deferred").each(function (i, el) {
      var $el = $(el);
      $el.removeClass("shiny-frame-deferred");
      $el.attr("src", $el.attr("data-deferred-src"));
      $el.attr("data-deferred-src", null);
    });
  }

  $(function () {
    // Init Shiny a little later than document ready, so user code can
    // run first (i.e. to register bindings)
    setTimeout(initShiny, 1);
  });

  //---------------------------------------------------------------------
  // Source file: ../srcjs/reactlog.js

  $(document).on('keydown', function (e) {
    if (e.which !== 114 || !e.ctrlKey && !e.metaKey || e.shiftKey || e.altKey) return;
    var url = 'reactlog?w=' + window.escape(exports.shinyapp.config.workerId) + "&s=" + window.escape(exports.shinyapp.config.sessionId);
    window.open(url);
    e.preventDefault();
  });

  $(document).on('keydown', function (e) {
    if (
    // if not one of the key combos below
    !(
    // cmd/ctrl + fn + f4
    e.which === 115 && (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey ||
    // cmd/ctrl + shift + fn + f3
    e.which === 114 && (e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey)) {
      return;
    }

    var url = 'reactlog/mark?w=' + window.escape(exports.shinyapp.config.workerId) + "&s=" + window.escape(exports.shinyapp.config.sessionId);

    // send notification
    $.get(url, function (result) {
      if (result !== "marked") return;

      var html = '<span id="shiny-reactlog-mark-text">Marked time point in reactlog</span>';

      exports.notifications.show({
        html: html,
        closeButton: true
      });
    }).fail(function () {
      // found returned error while marking, should open webpage
      window.open(url);
    });

    e.preventDefault();
  });

  //---------------------------------------------------------------------
  // Source file: ../srcjs/_end.js
})();

//# sourceMappingURL=shiny.js.map
//# sourceMappingURL=shiny.js.map
