components/pma.ui.components.js

/* perfect-scrollbar v0.6.16 */
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';

var ps = require('../main');

if (typeof define === 'function' && define.amd) {
  // AMD
  define(ps);
} else {
  // Add to a global object.
  window.PerfectScrollbar = ps;
  if (typeof window.Ps === 'undefined') {
    window.Ps = ps;
  }
}

},{"../main":7}],2:[function(require,module,exports){
'use strict';

function oldAdd(element, className) {
  var classes = element.className.split(' ');
  if (classes.indexOf(className) < 0) {
    classes.push(className);
  }
  element.className = classes.join(' ');
}

function oldRemove(element, className) {
  var classes = element.className.split(' ');
  var idx = classes.indexOf(className);
  if (idx >= 0) {
    classes.splice(idx, 1);
  }
  element.className = classes.join(' ');
}

exports.add = function (element, className) {
  if (element.classList) {
    element.classList.add(className);
  } else {
    oldAdd(element, className);
  }
};

exports.remove = function (element, className) {
  if (element.classList) {
    element.classList.remove(className);
  } else {
    oldRemove(element, className);
  }
};

exports.list = function (element) {
  if (element.classList) {
    return Array.prototype.slice.apply(element.classList);
  } else {
    return element.className.split(' ');
  }
};

},{}],3:[function(require,module,exports){
'use strict';

var DOM = {};

DOM.e = function (tagName, className) {
  var element = document.createElement(tagName);
  element.className = className;
  return element;
};

DOM.appendTo = function (child, parent) {
  parent.appendChild(child);
  return child;
};

function cssGet(element, styleName) {
  return window.getComputedStyle(element)[styleName];
}

function cssSet(element, styleName, styleValue) {
  if (typeof styleValue === 'number') {
    styleValue = styleValue.toString() + 'px';
  }
  element.style[styleName] = styleValue;
  return element;
}

function cssMultiSet(element, obj) {
  for (var key in obj) {
    var val = obj[key];
    if (typeof val === 'number') {
      val = val.toString() + 'px';
    }
    element.style[key] = val;
  }
  return element;
}

DOM.css = function (element, styleNameOrObject, styleValue) {
  if (typeof styleNameOrObject === 'object') {
    // multiple set with object
    return cssMultiSet(element, styleNameOrObject);
  } else {
    if (typeof styleValue === 'undefined') {
      return cssGet(element, styleNameOrObject);
    } else {
      return cssSet(element, styleNameOrObject, styleValue);
    }
  }
};

DOM.matches = function (element, query) {
  if (typeof element.matches !== 'undefined') {
    return element.matches(query);
  } else {
    if (typeof element.matchesSelector !== 'undefined') {
      return element.matchesSelector(query);
    } else if (typeof element.webkitMatchesSelector !== 'undefined') {
      return element.webkitMatchesSelector(query);
    } else if (typeof element.mozMatchesSelector !== 'undefined') {
      return element.mozMatchesSelector(query);
    } else if (typeof element.msMatchesSelector !== 'undefined') {
      return element.msMatchesSelector(query);
    }
  }
};

DOM.remove = function (element) {
  if (typeof element.remove !== 'undefined') {
    element.remove();
  } else {
    if (element.parentNode) {
      element.parentNode.removeChild(element);
    }
  }
};

DOM.queryChildren = function (element, selector) {
  return Array.prototype.filter.call(element.childNodes, function (child) {
    return DOM.matches(child, selector);
  });
};

module.exports = DOM;

},{}],4:[function(require,module,exports){
'use strict';

var EventElement = function (element) {
  this.element = element;
  this.events = {};
};

EventElement.prototype.bind = function (eventName, handler) {
  if (typeof this.events[eventName] === 'undefined') {
    this.events[eventName] = [];
  }
  this.events[eventName].push(handler);
  this.element.addEventListener(eventName, handler, false);
};

EventElement.prototype.unbind = function (eventName, handler) {
  var isHandlerProvided = (typeof handler !== 'undefined');
  this.events[eventName] = this.events[eventName].filter(function (hdlr) {
    if (isHandlerProvided && hdlr !== handler) {
      return true;
    }
    this.element.removeEventListener(eventName, hdlr, false);
    return false;
  }, this);
};

EventElement.prototype.unbindAll = function () {
  for (var name in this.events) {
    this.unbind(name);
  }
};

var EventManager = function () {
  this.eventElements = [];
};

EventManager.prototype.eventElement = function (element) {
  var ee = this.eventElements.filter(function (eventElement) {
    return eventElement.element === element;
  })[0];
  if (typeof ee === 'undefined') {
    ee = new EventElement(element);
    this.eventElements.push(ee);
  }
  return ee;
};

EventManager.prototype.bind = function (element, eventName, handler) {
  this.eventElement(element).bind(eventName, handler);
};

EventManager.prototype.unbind = function (element, eventName, handler) {
  this.eventElement(element).unbind(eventName, handler);
};

EventManager.prototype.unbindAll = function () {
  for (var i = 0; i < this.eventElements.length; i++) {
    this.eventElements[i].unbindAll();
  }
};

EventManager.prototype.once = function (element, eventName, handler) {
  var ee = this.eventElement(element);
  var onceHandler = function (e) {
    ee.unbind(eventName, onceHandler);
    handler(e);
  };
  ee.bind(eventName, onceHandler);
};

module.exports = EventManager;

},{}],5:[function(require,module,exports){
'use strict';

module.exports = (function () {
  function s4() {
    return Math.floor((1 + Math.random()) * 0x10000)
               .toString(16)
               .substring(1);
  }
  return function () {
    return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
           s4() + '-' + s4() + s4() + s4();
  };
})();

},{}],6:[function(require,module,exports){
'use strict';

var cls = require('./class');
var dom = require('./dom');

var toInt = exports.toInt = function (x) {
  return parseInt(x, 10) || 0;
};

var clone = exports.clone = function (obj) {
  if (!obj) {
    return null;
  } else if (obj.constructor === Array) {
    return obj.map(clone);
  } else if (typeof obj === 'object') {
    var result = {};
    for (var key in obj) {
      result[key] = clone(obj[key]);
    }
    return result;
  } else {
    return obj;
  }
};

exports.extend = function (original, source) {
  var result = clone(original);
  for (var key in source) {
    result[key] = clone(source[key]);
  }
  return result;
};

exports.isEditable = function (el) {
  return dom.matches(el, "input,[contenteditable]") ||
         dom.matches(el, "select,[contenteditable]") ||
         dom.matches(el, "textarea,[contenteditable]") ||
         dom.matches(el, "button,[contenteditable]");
};

exports.removePsClasses = function (element) {
  var clsList = cls.list(element);
  for (var i = 0; i < clsList.length; i++) {
    var className = clsList[i];
    if (className.indexOf('ps-') === 0) {
      cls.remove(element, className);
    }
  }
};

exports.outerWidth = function (element) {
  return toInt(dom.css(element, 'width')) +
         toInt(dom.css(element, 'paddingLeft')) +
         toInt(dom.css(element, 'paddingRight')) +
         toInt(dom.css(element, 'borderLeftWidth')) +
         toInt(dom.css(element, 'borderRightWidth'));
};

exports.startScrolling = function (element, axis) {
  cls.add(element, 'ps-in-scrolling');
  if (typeof axis !== 'undefined') {
    cls.add(element, 'ps-' + axis);
  } else {
    cls.add(element, 'ps-x');
    cls.add(element, 'ps-y');
  }
};

exports.stopScrolling = function (element, axis) {
  cls.remove(element, 'ps-in-scrolling');
  if (typeof axis !== 'undefined') {
    cls.remove(element, 'ps-' + axis);
  } else {
    cls.remove(element, 'ps-x');
    cls.remove(element, 'ps-y');
  }
};

exports.env = {
  isWebKit: 'WebkitAppearance' in document.documentElement.style,
  supportsTouch: (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch),
  supportsIePointer: window.navigator.msMaxTouchPoints !== null
};

},{"./class":2,"./dom":3}],7:[function(require,module,exports){
'use strict';

var destroy = require('./plugin/destroy');
var initialize = require('./plugin/initialize');
var update = require('./plugin/update');

module.exports = {
  initialize: initialize,
  update: update,
  destroy: destroy
};

},{"./plugin/destroy":9,"./plugin/initialize":17,"./plugin/update":21}],8:[function(require,module,exports){
'use strict';

module.exports = {
  handlers: ['click-rail', 'drag-scrollbar', 'keyboard', 'wheel', 'touch'],
  maxScrollbarLength: null,
  minScrollbarLength: null,
  scrollXMarginOffset: 0,
  scrollYMarginOffset: 0,
  suppressScrollX: false,
  suppressScrollY: false,
  swipePropagation: true,
  useBothWheelAxes: false,
  wheelPropagation: false,
  wheelSpeed: 1,
  theme: 'default'
};

},{}],9:[function(require,module,exports){
'use strict';

var _ = require('../lib/helper');
var dom = require('../lib/dom');
var instances = require('./instances');

module.exports = function (element) {
  var i = instances.get(element);

  if (!i) {
    return;
  }

  i.event.unbindAll();
  dom.remove(i.scrollbarX);
  dom.remove(i.scrollbarY);
  dom.remove(i.scrollbarXRail);
  dom.remove(i.scrollbarYRail);
  _.removePsClasses(element);

  instances.remove(element);
};

},{"../lib/dom":3,"../lib/helper":6,"./instances":18}],10:[function(require,module,exports){
'use strict';

var instances = require('../instances');
var updateGeometry = require('../update-geometry');
var updateScroll = require('../update-scroll');

function bindClickRailHandler(element, i) {
  function pageOffset(el) {
    return el.getBoundingClientRect();
  }
  var stopPropagation = function (e) { e.stopPropagation(); };

  i.event.bind(i.scrollbarY, 'click', stopPropagation);
  i.event.bind(i.scrollbarYRail, 'click', function (e) {
    var positionTop = e.pageY - window.pageYOffset - pageOffset(i.scrollbarYRail).top;
    var direction = positionTop > i.scrollbarYTop ? 1 : -1;

    updateScroll(element, 'top', element.scrollTop + direction * i.containerHeight);
    updateGeometry(element);

    e.stopPropagation();
  });

  i.event.bind(i.scrollbarX, 'click', stopPropagation);
  i.event.bind(i.scrollbarXRail, 'click', function (e) {
    var positionLeft = e.pageX - window.pageXOffset - pageOffset(i.scrollbarXRail).left;
    var direction = positionLeft > i.scrollbarXLeft ? 1 : -1;

    updateScroll(element, 'left', element.scrollLeft + direction * i.containerWidth);
    updateGeometry(element);

    e.stopPropagation();
  });
}

module.exports = function (element) {
  var i = instances.get(element);
  bindClickRailHandler(element, i);
};

},{"../instances":18,"../update-geometry":19,"../update-scroll":20}],11:[function(require,module,exports){
'use strict';

var _ = require('../../lib/helper');
var dom = require('../../lib/dom');
var instances = require('../instances');
var updateGeometry = require('../update-geometry');
var updateScroll = require('../update-scroll');

function bindMouseScrollXHandler(element, i) {
  var currentLeft = null;
  var currentPageX = null;

  function updateScrollLeft(deltaX) {
    var newLeft = currentLeft + (deltaX * i.railXRatio);
    var maxLeft = Math.max(0, i.scrollbarXRail.getBoundingClientRect().left) + (i.railXRatio * (i.railXWidth - i.scrollbarXWidth));

    if (newLeft < 0) {
      i.scrollbarXLeft = 0;
    } else if (newLeft > maxLeft) {
      i.scrollbarXLeft = maxLeft;
    } else {
      i.scrollbarXLeft = newLeft;
    }

    var scrollLeft = _.toInt(i.scrollbarXLeft * (i.contentWidth - i.containerWidth) / (i.containerWidth - (i.railXRatio * i.scrollbarXWidth))) - i.negativeScrollAdjustment;
    updateScroll(element, 'left', scrollLeft);
  }

  var mouseMoveHandler = function (e) {
    updateScrollLeft(e.pageX - currentPageX);
    updateGeometry(element);
    e.stopPropagation();
    e.preventDefault();
  };

  var mouseUpHandler = function () {
    _.stopScrolling(element, 'x');
    i.event.unbind(i.ownerDocument, 'mousemove', mouseMoveHandler);
  };

  i.event.bind(i.scrollbarX, 'mousedown', function (e) {
    currentPageX = e.pageX;
    currentLeft = _.toInt(dom.css(i.scrollbarX, 'left')) * i.railXRatio;
    _.startScrolling(element, 'x');

    i.event.bind(i.ownerDocument, 'mousemove', mouseMoveHandler);
    i.event.once(i.ownerDocument, 'mouseup', mouseUpHandler);

    e.stopPropagation();
    e.preventDefault();
  });
}

function bindMouseScrollYHandler(element, i) {
  var currentTop = null;
  var currentPageY = null;

  function updateScrollTop(deltaY) {
    var newTop = currentTop + (deltaY * i.railYRatio);
    var maxTop = Math.max(0, i.scrollbarYRail.getBoundingClientRect().top) + (i.railYRatio * (i.railYHeight - i.scrollbarYHeight));

    if (newTop < 0) {
      i.scrollbarYTop = 0;
    } else if (newTop > maxTop) {
      i.scrollbarYTop = maxTop;
    } else {
      i.scrollbarYTop = newTop;
    }

    var scrollTop = _.toInt(i.scrollbarYTop * (i.contentHeight - i.containerHeight) / (i.containerHeight - (i.railYRatio * i.scrollbarYHeight)));
    updateScroll(element, 'top', scrollTop);
  }

  var mouseMoveHandler = function (e) {
    updateScrollTop(e.pageY - currentPageY);
    updateGeometry(element);
    e.stopPropagation();
    e.preventDefault();
  };

  var mouseUpHandler = function () {
    _.stopScrolling(element, 'y');
    i.event.unbind(i.ownerDocument, 'mousemove', mouseMoveHandler);
  };

  i.event.bind(i.scrollbarY, 'mousedown', function (e) {
    currentPageY = e.pageY;
    currentTop = _.toInt(dom.css(i.scrollbarY, 'top')) * i.railYRatio;
    _.startScrolling(element, 'y');

    i.event.bind(i.ownerDocument, 'mousemove', mouseMoveHandler);
    i.event.once(i.ownerDocument, 'mouseup', mouseUpHandler);

    e.stopPropagation();
    e.preventDefault();
  });
}

module.exports = function (element) {
  var i = instances.get(element);
  bindMouseScrollXHandler(element, i);
  bindMouseScrollYHandler(element, i);
};

},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],12:[function(require,module,exports){
'use strict';

var _ = require('../../lib/helper');
var dom = require('../../lib/dom');
var instances = require('../instances');
var updateGeometry = require('../update-geometry');
var updateScroll = require('../update-scroll');

function bindKeyboardHandler(element, i) {
  var hovered = false;
  i.event.bind(element, 'mouseenter', function () {
    hovered = true;
  });
  i.event.bind(element, 'mouseleave', function () {
    hovered = false;
  });

  var shouldPrevent = false;
  function shouldPreventDefault(deltaX, deltaY) {
    var scrollTop = element.scrollTop;
    if (deltaX === 0) {
      if (!i.scrollbarYActive) {
        return false;
      }
      if ((scrollTop === 0 && deltaY > 0) || (scrollTop >= i.contentHeight - i.containerHeight && deltaY < 0)) {
        return !i.settings.wheelPropagation;
      }
    }

    var scrollLeft = element.scrollLeft;
    if (deltaY === 0) {
      if (!i.scrollbarXActive) {
        return false;
      }
      if ((scrollLeft === 0 && deltaX < 0) || (scrollLeft >= i.contentWidth - i.containerWidth && deltaX > 0)) {
        return !i.settings.wheelPropagation;
      }
    }
    return true;
  }

  i.event.bind(i.ownerDocument, 'keydown', function (e) {
    if ((e.isDefaultPrevented && e.isDefaultPrevented()) || e.defaultPrevented) {
      return;
    }

    var focused = dom.matches(i.scrollbarX, ':focus') ||
                  dom.matches(i.scrollbarY, ':focus');

    if (!hovered && !focused) {
      return;
    }

    var activeElement = document.activeElement ? document.activeElement : i.ownerDocument.activeElement;
    if (activeElement) {
      if (activeElement.tagName === 'IFRAME') {
        activeElement = activeElement.contentDocument.activeElement;
      } else {
        // go deeper if element is a webcomponent
        while (activeElement.shadowRoot) {
          activeElement = activeElement.shadowRoot.activeElement;
        }
      }
      if (_.isEditable(activeElement)) {
        return;
      }
    }

    var deltaX = 0;
    var deltaY = 0;

    switch (e.which) {
    case 37: // left
      if (e.metaKey) {
        deltaX = -i.contentWidth;
      } else if (e.altKey) {
        deltaX = -i.containerWidth;
      } else {
        deltaX = -30;
      }
      break;
    case 38: // up
      if (e.metaKey) {
        deltaY = i.contentHeight;
      } else if (e.altKey) {
        deltaY = i.containerHeight;
      } else {
        deltaY = 30;
      }
      break;
    case 39: // right
      if (e.metaKey) {
        deltaX = i.contentWidth;
      } else if (e.altKey) {
        deltaX = i.containerWidth;
      } else {
        deltaX = 30;
      }
      break;
    case 40: // down
      if (e.metaKey) {
        deltaY = -i.contentHeight;
      } else if (e.altKey) {
        deltaY = -i.containerHeight;
      } else {
        deltaY = -30;
      }
      break;
    case 33: // page up
      deltaY = 90;
      break;
    case 32: // space bar
      if (e.shiftKey) {
        deltaY = 90;
      } else {
        deltaY = -90;
      }
      break;
    case 34: // page down
      deltaY = -90;
      break;
    case 35: // end
      if (e.ctrlKey) {
        deltaY = -i.contentHeight;
      } else {
        deltaY = -i.containerHeight;
      }
      break;
    case 36: // home
      if (e.ctrlKey) {
        deltaY = element.scrollTop;
      } else {
        deltaY = i.containerHeight;
      }
      break;
    default:
      return;
    }

    updateScroll(element, 'top', element.scrollTop - deltaY);
    updateScroll(element, 'left', element.scrollLeft + deltaX);
    updateGeometry(element);

    shouldPrevent = shouldPreventDefault(deltaX, deltaY);
    if (shouldPrevent) {
      e.preventDefault();
    }
  });
}

module.exports = function (element) {
  var i = instances.get(element);
  bindKeyboardHandler(element, i);
};

},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],13:[function(require,module,exports){
'use strict';

var instances = require('../instances');
var updateGeometry = require('../update-geometry');
var updateScroll = require('../update-scroll');

function bindMouseWheelHandler(element, i) {
  var shouldPrevent = false;

  function shouldPreventDefault(deltaX, deltaY) {
    var scrollTop = element.scrollTop;
    if (deltaX === 0) {
      if (!i.scrollbarYActive) {
        return false;
      }
      if ((scrollTop === 0 && deltaY > 0) || (scrollTop >= i.contentHeight - i.containerHeight && deltaY < 0)) {
        return !i.settings.wheelPropagation;
      }
    }

    var scrollLeft = element.scrollLeft;
    if (deltaY === 0) {
      if (!i.scrollbarXActive) {
        return false;
      }
      if ((scrollLeft === 0 && deltaX < 0) || (scrollLeft >= i.contentWidth - i.containerWidth && deltaX > 0)) {
        return !i.settings.wheelPropagation;
      }
    }
    return true;
  }

  function getDeltaFromEvent(e) {
    var deltaX = e.deltaX;
    var deltaY = -1 * e.deltaY;

    if (typeof deltaX === "undefined" || typeof deltaY === "undefined") {
      // OS X Safari
      deltaX = -1 * e.wheelDeltaX / 6;
      deltaY = e.wheelDeltaY / 6;
    }

    if (e.deltaMode && e.deltaMode === 1) {
      // Firefox in deltaMode 1: Line scrolling
      deltaX *= 10;
      deltaY *= 10;
    }

    if (deltaX !== deltaX && deltaY !== deltaY/* NaN checks */) {
      // IE in some mouse drivers
      deltaX = 0;
      deltaY = e.wheelDelta;
    }

    if (e.shiftKey) {
      // reverse axis with shift key
      return [-deltaY, -deltaX];
    }
    return [deltaX, deltaY];
  }

  function shouldBeConsumedByChild(deltaX, deltaY) {
    var child = element.querySelector('textarea:hover, select[multiple]:hover, .ps-child:hover');
    if (child) {
      if (!window.getComputedStyle(child).overflow.match(/(scroll|auto)/)) {
        // if not scrollable
        return false;
      }

      var maxScrollTop = child.scrollHeight - child.clientHeight;
      if (maxScrollTop > 0) {
        if (!(child.scrollTop === 0 && deltaY > 0) && !(child.scrollTop === maxScrollTop && deltaY < 0)) {
          return true;
        }
      }
      var maxScrollLeft = child.scrollLeft - child.clientWidth;
      if (maxScrollLeft > 0) {
        if (!(child.scrollLeft === 0 && deltaX < 0) && !(child.scrollLeft === maxScrollLeft && deltaX > 0)) {
          return true;
        }
      }
    }
    return false;
  }

  function mousewheelHandler(e) {
    var delta = getDeltaFromEvent(e);

    var deltaX = delta[0];
    var deltaY = delta[1];

    if (shouldBeConsumedByChild(deltaX, deltaY)) {
      return;
    }

    shouldPrevent = false;
    if (!i.settings.useBothWheelAxes) {
      // deltaX will only be used for horizontal scrolling and deltaY will
      // only be used for vertical scrolling - this is the default
      updateScroll(element, 'top', element.scrollTop - (deltaY * i.settings.wheelSpeed));
      updateScroll(element, 'left', element.scrollLeft + (deltaX * i.settings.wheelSpeed));
    } else if (i.scrollbarYActive && !i.scrollbarXActive) {
      // only vertical scrollbar is active and useBothWheelAxes option is
      // active, so let's scroll vertical bar using both mouse wheel axes
      if (deltaY) {
        updateScroll(element, 'top', element.scrollTop - (deltaY * i.settings.wheelSpeed));
      } else {
        updateScroll(element, 'top', element.scrollTop + (deltaX * i.settings.wheelSpeed));
      }
      shouldPrevent = true;
    } else if (i.scrollbarXActive && !i.scrollbarYActive) {
      // useBothWheelAxes and only horizontal bar is active, so use both
      // wheel axes for horizontal bar
      if (deltaX) {
        updateScroll(element, 'left', element.scrollLeft + (deltaX * i.settings.wheelSpeed));
      } else {
        updateScroll(element, 'left', element.scrollLeft - (deltaY * i.settings.wheelSpeed));
      }
      shouldPrevent = true;
    }

    updateGeometry(element);

    shouldPrevent = (shouldPrevent || shouldPreventDefault(deltaX, deltaY));
    if (shouldPrevent) {
      e.stopPropagation();
      e.preventDefault();
    }
  }

  if (typeof window.onwheel !== "undefined") {
    i.event.bind(element, 'wheel', mousewheelHandler);
  } else if (typeof window.onmousewheel !== "undefined") {
    i.event.bind(element, 'mousewheel', mousewheelHandler);
  }
}

module.exports = function (element) {
  var i = instances.get(element);
  bindMouseWheelHandler(element, i);
};

},{"../instances":18,"../update-geometry":19,"../update-scroll":20}],14:[function(require,module,exports){
'use strict';

var instances = require('../instances');
var updateGeometry = require('../update-geometry');

function bindNativeScrollHandler(element, i) {
  i.event.bind(element, 'scroll', function () {
    updateGeometry(element);
  });
}

module.exports = function (element) {
  var i = instances.get(element);
  bindNativeScrollHandler(element, i);
};

},{"../instances":18,"../update-geometry":19}],15:[function(require,module,exports){
'use strict';

var _ = require('../../lib/helper');
var instances = require('../instances');
var updateGeometry = require('../update-geometry');
var updateScroll = require('../update-scroll');

function bindSelectionHandler(element, i) {
  function getRangeNode() {
    var selection = window.getSelection ? window.getSelection() :
                    document.getSelection ? document.getSelection() : '';
    if (selection.toString().length === 0) {
      return null;
    } else {
      return selection.getRangeAt(0).commonAncestorContainer;
    }
  }

  var scrollingLoop = null;
  var scrollDiff = {top: 0, left: 0};
  function startScrolling() {
    if (!scrollingLoop) {
      scrollingLoop = setInterval(function () {
        if (!instances.get(element)) {
          clearInterval(scrollingLoop);
          return;
        }

        updateScroll(element, 'top', element.scrollTop + scrollDiff.top);
        updateScroll(element, 'left', element.scrollLeft + scrollDiff.left);
        updateGeometry(element);
      }, 50); // every .1 sec
    }
  }
  function stopScrolling() {
    if (scrollingLoop) {
      clearInterval(scrollingLoop);
      scrollingLoop = null;
    }
    _.stopScrolling(element);
  }

  var isSelected = false;
  i.event.bind(i.ownerDocument, 'selectionchange', function () {
    if (element.contains(getRangeNode())) {
      isSelected = true;
    } else {
      isSelected = false;
      stopScrolling();
    }
  });
  i.event.bind(window, 'mouseup', function () {
    if (isSelected) {
      isSelected = false;
      stopScrolling();
    }
  });
  i.event.bind(window, 'keyup', function () {
    if (isSelected) {
      isSelected = false;
      stopScrolling();
    }
  });

  i.event.bind(window, 'mousemove', function (e) {
    if (isSelected) {
      var mousePosition = {x: e.pageX, y: e.pageY};
      var containerGeometry = {
        left: element.offsetLeft,
        right: element.offsetLeft + element.offsetWidth,
        top: element.offsetTop,
        bottom: element.offsetTop + element.offsetHeight
      };

      if (mousePosition.x < containerGeometry.left + 3) {
        scrollDiff.left = -5;
        _.startScrolling(element, 'x');
      } else if (mousePosition.x > containerGeometry.right - 3) {
        scrollDiff.left = 5;
        _.startScrolling(element, 'x');
      } else {
        scrollDiff.left = 0;
      }

      if (mousePosition.y < containerGeometry.top + 3) {
        if (containerGeometry.top + 3 - mousePosition.y < 5) {
          scrollDiff.top = -5;
        } else {
          scrollDiff.top = -20;
        }
        _.startScrolling(element, 'y');
      } else if (mousePosition.y > containerGeometry.bottom - 3) {
        if (mousePosition.y - containerGeometry.bottom + 3 < 5) {
          scrollDiff.top = 5;
        } else {
          scrollDiff.top = 20;
        }
        _.startScrolling(element, 'y');
      } else {
        scrollDiff.top = 0;
      }

      if (scrollDiff.top === 0 && scrollDiff.left === 0) {
        stopScrolling();
      } else {
        startScrolling();
      }
    }
  });
}

module.exports = function (element) {
  var i = instances.get(element);
  bindSelectionHandler(element, i);
};

},{"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],16:[function(require,module,exports){
'use strict';

var _ = require('../../lib/helper');
var instances = require('../instances');
var updateGeometry = require('../update-geometry');
var updateScroll = require('../update-scroll');

function bindTouchHandler(element, i, supportsTouch, supportsIePointer) {
  function shouldPreventDefault(deltaX, deltaY) {
    var scrollTop = element.scrollTop;
    var scrollLeft = element.scrollLeft;
    var magnitudeX = Math.abs(deltaX);
    var magnitudeY = Math.abs(deltaY);

    if (magnitudeY > magnitudeX) {
      // user is perhaps trying to swipe up/down the page

      if (((deltaY < 0) && (scrollTop === i.contentHeight - i.containerHeight)) ||
          ((deltaY > 0) && (scrollTop === 0))) {
        return !i.settings.swipePropagation;
      }
    } else if (magnitudeX > magnitudeY) {
      // user is perhaps trying to swipe left/right across the page

      if (((deltaX < 0) && (scrollLeft === i.contentWidth - i.containerWidth)) ||
          ((deltaX > 0) && (scrollLeft === 0))) {
        return !i.settings.swipePropagation;
      }
    }

    return true;
  }

  function applyTouchMove(differenceX, differenceY) {
    updateScroll(element, 'top', element.scrollTop - differenceY);
    updateScroll(element, 'left', element.scrollLeft - differenceX);

    updateGeometry(element);
  }

  var startOffset = {};
  var startTime = 0;
  var speed = {};
  var easingLoop = null;
  var inGlobalTouch = false;
  var inLocalTouch = false;

  function globalTouchStart() {
    inGlobalTouch = true;
  }
  function globalTouchEnd() {
    inGlobalTouch = false;
  }

  function getTouch(e) {
    if (e.targetTouches) {
      return e.targetTouches[0];
    } else {
      // Maybe IE pointer
      return e;
    }
  }
  function shouldHandle(e) {
    if (e.targetTouches && e.targetTouches.length === 1) {
      return true;
    }
    if (e.pointerType && e.pointerType !== 'mouse' && e.pointerType !== e.MSPOINTER_TYPE_MOUSE) {
      return true;
    }
    return false;
  }
  function touchStart(e) {
    if (shouldHandle(e)) {
      inLocalTouch = true;

      var touch = getTouch(e);

      startOffset.pageX = touch.pageX;
      startOffset.pageY = touch.pageY;

      startTime = (new Date()).getTime();

      if (easingLoop !== null) {
        clearInterval(easingLoop);
      }

      e.stopPropagation();
    }
  }
  function touchMove(e) {
    if (!inLocalTouch && i.settings.swipePropagation) {
      touchStart(e);
    }
    if (!inGlobalTouch && inLocalTouch && shouldHandle(e)) {
      var touch = getTouch(e);

      var currentOffset = {pageX: touch.pageX, pageY: touch.pageY};

      var differenceX = currentOffset.pageX - startOffset.pageX;
      var differenceY = currentOffset.pageY - startOffset.pageY;

      applyTouchMove(differenceX, differenceY);
      startOffset = currentOffset;

      var currentTime = (new Date()).getTime();

      var timeGap = currentTime - startTime;
      if (timeGap > 0) {
        speed.x = differenceX / timeGap;
        speed.y = differenceY / timeGap;
        startTime = currentTime;
      }

      if (shouldPreventDefault(differenceX, differenceY)) {
        e.stopPropagation();
        e.preventDefault();
      }
    }
  }
  function touchEnd() {
    if (!inGlobalTouch && inLocalTouch) {
      inLocalTouch = false;

      clearInterval(easingLoop);
      easingLoop = setInterval(function () {
        if (!instances.get(element)) {
          clearInterval(easingLoop);
          return;
        }

        if (!speed.x && !speed.y) {
          clearInterval(easingLoop);
          return;
        }

        if (Math.abs(speed.x) < 0.01 && Math.abs(speed.y) < 0.01) {
          clearInterval(easingLoop);
          return;
        }

        applyTouchMove(speed.x * 30, speed.y * 30);

        speed.x *= 0.8;
        speed.y *= 0.8;
      }, 10);
    }
  }

  if (supportsTouch) {
    i.event.bind(window, 'touchstart', globalTouchStart);
    i.event.bind(window, 'touchend', globalTouchEnd);
    i.event.bind(element, 'touchstart', touchStart);
    i.event.bind(element, 'touchmove', touchMove);
    i.event.bind(element, 'touchend', touchEnd);
  } else if (supportsIePointer) {
    if (window.PointerEvent) {
      i.event.bind(window, 'pointerdown', globalTouchStart);
      i.event.bind(window, 'pointerup', globalTouchEnd);
      i.event.bind(element, 'pointerdown', touchStart);
      i.event.bind(element, 'pointermove', touchMove);
      i.event.bind(element, 'pointerup', touchEnd);
    } else if (window.MSPointerEvent) {
      i.event.bind(window, 'MSPointerDown', globalTouchStart);
      i.event.bind(window, 'MSPointerUp', globalTouchEnd);
      i.event.bind(element, 'MSPointerDown', touchStart);
      i.event.bind(element, 'MSPointerMove', touchMove);
      i.event.bind(element, 'MSPointerUp', touchEnd);
    }
  }
}

module.exports = function (element) {
  if (!_.env.supportsTouch && !_.env.supportsIePointer) {
    return;
  }

  var i = instances.get(element);
  bindTouchHandler(element, i, _.env.supportsTouch, _.env.supportsIePointer);
};

},{"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],17:[function(require,module,exports){
'use strict';

var _ = require('../lib/helper');
var cls = require('../lib/class');
var instances = require('./instances');
var updateGeometry = require('./update-geometry');

// Handlers
var handlers = {
  'click-rail': require('./handler/click-rail'),
  'drag-scrollbar': require('./handler/drag-scrollbar'),
  'keyboard': require('./handler/keyboard'),
  'wheel': require('./handler/mouse-wheel'),
  'touch': require('./handler/touch'),
  'selection': require('./handler/selection')
};
var nativeScrollHandler = require('./handler/native-scroll');

module.exports = function (element, userSettings) {
  userSettings = typeof userSettings === 'object' ? userSettings : {};

  cls.add(element, 'ps-container');

  // Create a plugin instance.
  var i = instances.add(element);

  i.settings = _.extend(i.settings, userSettings);
  cls.add(element, 'ps-theme-' + i.settings.theme);

  i.settings.handlers.forEach(function (handlerName) {
    handlers[handlerName](element);
  });

  nativeScrollHandler(element);

  updateGeometry(element);
};

},{"../lib/class":2,"../lib/helper":6,"./handler/click-rail":10,"./handler/drag-scrollbar":11,"./handler/keyboard":12,"./handler/mouse-wheel":13,"./handler/native-scroll":14,"./handler/selection":15,"./handler/touch":16,"./instances":18,"./update-geometry":19}],18:[function(require,module,exports){
'use strict';

var _ = require('../lib/helper');
var cls = require('../lib/class');
var defaultSettings = require('./default-setting');
var dom = require('../lib/dom');
var EventManager = require('../lib/event-manager');
var guid = require('../lib/guid');

var instances = {};

function Instance(element) {
  var i = this;

  i.settings = _.clone(defaultSettings);
  i.containerWidth = null;
  i.containerHeight = null;
  i.contentWidth = null;
  i.contentHeight = null;

  i.isRtl = dom.css(element, 'direction') === "rtl";
  i.isNegativeScroll = (function () {
    var originalScrollLeft = element.scrollLeft;
    var result = null;
    element.scrollLeft = -1;
    result = element.scrollLeft < 0;
    element.scrollLeft = originalScrollLeft;
    return result;
  })();
  i.negativeScrollAdjustment = i.isNegativeScroll ? element.scrollWidth - element.clientWidth : 0;
  i.event = new EventManager();
  i.ownerDocument = element.ownerDocument || document;

  function focus() {
    cls.add(element, 'ps-focus');
  }

  function blur() {
    cls.remove(element, 'ps-focus');
  }

  i.scrollbarXRail = dom.appendTo(dom.e('div', 'ps-scrollbar-x-rail'), element);
  i.scrollbarX = dom.appendTo(dom.e('div', 'ps-scrollbar-x'), i.scrollbarXRail);
  i.scrollbarX.setAttribute('tabindex', 0);
  i.event.bind(i.scrollbarX, 'focus', focus);
  i.event.bind(i.scrollbarX, 'blur', blur);
  i.scrollbarXActive = null;
  i.scrollbarXWidth = null;
  i.scrollbarXLeft = null;
  i.scrollbarXBottom = _.toInt(dom.css(i.scrollbarXRail, 'bottom'));
  i.isScrollbarXUsingBottom = i.scrollbarXBottom === i.scrollbarXBottom; // !isNaN
  i.scrollbarXTop = i.isScrollbarXUsingBottom ? null : _.toInt(dom.css(i.scrollbarXRail, 'top'));
  i.railBorderXWidth = _.toInt(dom.css(i.scrollbarXRail, 'borderLeftWidth')) + _.toInt(dom.css(i.scrollbarXRail, 'borderRightWidth'));
  // Set rail to display:block to calculate margins
  dom.css(i.scrollbarXRail, 'display', 'block');
  i.railXMarginWidth = _.toInt(dom.css(i.scrollbarXRail, 'marginLeft')) + _.toInt(dom.css(i.scrollbarXRail, 'marginRight'));
  dom.css(i.scrollbarXRail, 'display', '');
  i.railXWidth = null;
  i.railXRatio = null;

  i.scrollbarYRail = dom.appendTo(dom.e('div', 'ps-scrollbar-y-rail'), element);
  i.scrollbarY = dom.appendTo(dom.e('div', 'ps-scrollbar-y'), i.scrollbarYRail);
  i.scrollbarY.setAttribute('tabindex', 0);
  i.event.bind(i.scrollbarY, 'focus', focus);
  i.event.bind(i.scrollbarY, 'blur', blur);
  i.scrollbarYActive = null;
  i.scrollbarYHeight = null;
  i.scrollbarYTop = null;
  i.scrollbarYRight = _.toInt(dom.css(i.scrollbarYRail, 'right'));
  i.isScrollbarYUsingRight = i.scrollbarYRight === i.scrollbarYRight; // !isNaN
  i.scrollbarYLeft = i.isScrollbarYUsingRight ? null : _.toInt(dom.css(i.scrollbarYRail, 'left'));
  i.scrollbarYOuterWidth = i.isRtl ? _.outerWidth(i.scrollbarY) : null;
  i.railBorderYWidth = _.toInt(dom.css(i.scrollbarYRail, 'borderTopWidth')) + _.toInt(dom.css(i.scrollbarYRail, 'borderBottomWidth'));
  dom.css(i.scrollbarYRail, 'display', 'block');
  i.railYMarginHeight = _.toInt(dom.css(i.scrollbarYRail, 'marginTop')) + _.toInt(dom.css(i.scrollbarYRail, 'marginBottom'));
  dom.css(i.scrollbarYRail, 'display', '');
  i.railYHeight = null;
  i.railYRatio = null;
}

function getId(element) {
  return element.getAttribute('data-ps-id');
}

function setId(element, id) {
  element.setAttribute('data-ps-id', id);
}

function removeId(element) {
  element.removeAttribute('data-ps-id');
}

exports.add = function (element) {
  var newId = guid();
  setId(element, newId);
  instances[newId] = new Instance(element);
  return instances[newId];
};

exports.remove = function (element) {
  delete instances[getId(element)];
  removeId(element);
};

exports.get = function (element) {
  return instances[getId(element)];
};

},{"../lib/class":2,"../lib/dom":3,"../lib/event-manager":4,"../lib/guid":5,"../lib/helper":6,"./default-setting":8}],19:[function(require,module,exports){
'use strict';

var _ = require('../lib/helper');
var cls = require('../lib/class');
var dom = require('../lib/dom');
var instances = require('./instances');
var updateScroll = require('./update-scroll');

function getThumbSize(i, thumbSize) {
  if (i.settings.minScrollbarLength) {
    thumbSize = Math.max(thumbSize, i.settings.minScrollbarLength);
  }
  if (i.settings.maxScrollbarLength) {
    thumbSize = Math.min(thumbSize, i.settings.maxScrollbarLength);
  }
  return thumbSize;
}

function updateCss(element, i) {
  var xRailOffset = {width: i.railXWidth};
  if (i.isRtl) {
    xRailOffset.left = i.negativeScrollAdjustment + element.scrollLeft + i.containerWidth - i.contentWidth;
  } else {
    xRailOffset.left = element.scrollLeft;
  }
  if (i.isScrollbarXUsingBottom) {
    xRailOffset.bottom = i.scrollbarXBottom - element.scrollTop;
  } else {
    xRailOffset.top = i.scrollbarXTop + element.scrollTop;
  }
  dom.css(i.scrollbarXRail, xRailOffset);

  var yRailOffset = {top: element.scrollTop, height: i.railYHeight};
  if (i.isScrollbarYUsingRight) {
    if (i.isRtl) {
      yRailOffset.right = i.contentWidth - (i.negativeScrollAdjustment + element.scrollLeft) - i.scrollbarYRight - i.scrollbarYOuterWidth;
    } else {
      yRailOffset.right = i.scrollbarYRight - element.scrollLeft;
    }
  } else {
    if (i.isRtl) {
      yRailOffset.left = i.negativeScrollAdjustment + element.scrollLeft + i.containerWidth * 2 - i.contentWidth - i.scrollbarYLeft - i.scrollbarYOuterWidth;
    } else {
      yRailOffset.left = i.scrollbarYLeft + element.scrollLeft;
    }
  }
  dom.css(i.scrollbarYRail, yRailOffset);

  dom.css(i.scrollbarX, {left: i.scrollbarXLeft, width: i.scrollbarXWidth - i.railBorderXWidth});
  dom.css(i.scrollbarY, {top: i.scrollbarYTop, height: i.scrollbarYHeight - i.railBorderYWidth});
}

module.exports = function (element) {
  var i = instances.get(element);

  i.containerWidth = element.clientWidth;
  i.containerHeight = element.clientHeight;
  i.contentWidth = element.scrollWidth;
  i.contentHeight = element.scrollHeight;

  var existingRails;
  if (!element.contains(i.scrollbarXRail)) {
    existingRails = dom.queryChildren(element, '.ps-scrollbar-x-rail');
    if (existingRails.length > 0) {
      existingRails.forEach(function (rail) {
        dom.remove(rail);
      });
    }
    dom.appendTo(i.scrollbarXRail, element);
  }
  if (!element.contains(i.scrollbarYRail)) {
    existingRails = dom.queryChildren(element, '.ps-scrollbar-y-rail');
    if (existingRails.length > 0) {
      existingRails.forEach(function (rail) {
        dom.remove(rail);
      });
    }
    dom.appendTo(i.scrollbarYRail, element);
  }

  if (!i.settings.suppressScrollX && i.containerWidth + i.settings.scrollXMarginOffset < i.contentWidth) {
    i.scrollbarXActive = true;
    i.railXWidth = i.containerWidth - i.railXMarginWidth;
    i.railXRatio = i.containerWidth / i.railXWidth;
    i.scrollbarXWidth = getThumbSize(i, _.toInt(i.railXWidth * i.containerWidth / i.contentWidth));
    i.scrollbarXLeft = _.toInt((i.negativeScrollAdjustment + element.scrollLeft) * (i.railXWidth - i.scrollbarXWidth) / (i.contentWidth - i.containerWidth));
  } else {
    i.scrollbarXActive = false;
  }

  if (!i.settings.suppressScrollY && i.containerHeight + i.settings.scrollYMarginOffset < i.contentHeight) {
    i.scrollbarYActive = true;
    i.railYHeight = i.containerHeight - i.railYMarginHeight;
    i.railYRatio = i.containerHeight / i.railYHeight;
    i.scrollbarYHeight = getThumbSize(i, _.toInt(i.railYHeight * i.containerHeight / i.contentHeight));
    i.scrollbarYTop = _.toInt(element.scrollTop * (i.railYHeight - i.scrollbarYHeight) / (i.contentHeight - i.containerHeight));
  } else {
    i.scrollbarYActive = false;
  }

  if (i.scrollbarXLeft >= i.railXWidth - i.scrollbarXWidth) {
    i.scrollbarXLeft = i.railXWidth - i.scrollbarXWidth;
  }
  if (i.scrollbarYTop >= i.railYHeight - i.scrollbarYHeight) {
    i.scrollbarYTop = i.railYHeight - i.scrollbarYHeight;
  }

  updateCss(element, i);

  if (i.scrollbarXActive) {
    cls.add(element, 'ps-active-x');
  } else {
    cls.remove(element, 'ps-active-x');
    i.scrollbarXWidth = 0;
    i.scrollbarXLeft = 0;
    updateScroll(element, 'left', 0);
  }
  if (i.scrollbarYActive) {
    cls.add(element, 'ps-active-y');
  } else {
    cls.remove(element, 'ps-active-y');
    i.scrollbarYHeight = 0;
    i.scrollbarYTop = 0;
    updateScroll(element, 'top', 0);
  }
};

},{"../lib/class":2,"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-scroll":20}],20:[function(require,module,exports){
'use strict';

var instances = require('./instances');

var lastTop;
var lastLeft;

var createDOMEvent = function (name) {
  var event = document.createEvent("Event");
  event.initEvent(name, true, true);
  return event;
};

module.exports = function (element, axis, value) {
  if (typeof element === 'undefined') {
    throw 'You must provide an element to the update-scroll function';
  }

  if (typeof axis === 'undefined') {
    throw 'You must provide an axis to the update-scroll function';
  }

  if (typeof value === 'undefined') {
    throw 'You must provide a value to the update-scroll function';
  }

  if (axis === 'top' && value <= 0) {
    element.scrollTop = value = 0; // don't allow negative scroll
    element.dispatchEvent(createDOMEvent('ps-y-reach-start'));
  }

  if (axis === 'left' && value <= 0) {
    element.scrollLeft = value = 0; // don't allow negative scroll
    element.dispatchEvent(createDOMEvent('ps-x-reach-start'));
  }

  var i = instances.get(element);

  if (axis === 'top' && value >= i.contentHeight - i.containerHeight) {
    // don't allow scroll past container
    value = i.contentHeight - i.containerHeight;
    if (value - element.scrollTop <= 1) {
      // mitigates rounding errors on non-subpixel scroll values
      value = element.scrollTop;
    } else {
      element.scrollTop = value;
    }
    element.dispatchEvent(createDOMEvent('ps-y-reach-end'));
  }

  if (axis === 'left' && value >= i.contentWidth - i.containerWidth) {
    // don't allow scroll past container
    value = i.contentWidth - i.containerWidth;
    if (value - element.scrollLeft <= 1) {
      // mitigates rounding errors on non-subpixel scroll values
      value = element.scrollLeft;
    } else {
      element.scrollLeft = value;
    }
    element.dispatchEvent(createDOMEvent('ps-x-reach-end'));
  }

  if (!lastTop) {
    lastTop = element.scrollTop;
  }

  if (!lastLeft) {
    lastLeft = element.scrollLeft;
  }

  if (axis === 'top' && value < lastTop) {
    element.dispatchEvent(createDOMEvent('ps-scroll-up'));
  }

  if (axis === 'top' && value > lastTop) {
    element.dispatchEvent(createDOMEvent('ps-scroll-down'));
  }

  if (axis === 'left' && value < lastLeft) {
    element.dispatchEvent(createDOMEvent('ps-scroll-left'));
  }

  if (axis === 'left' && value > lastLeft) {
    element.dispatchEvent(createDOMEvent('ps-scroll-right'));
  }

  if (axis === 'top') {
    element.scrollTop = lastTop = value;
    element.dispatchEvent(createDOMEvent('ps-scroll-y'));
  }

  if (axis === 'left') {
    element.scrollLeft = lastLeft = value;
    element.dispatchEvent(createDOMEvent('ps-scroll-x'));
  }

};

},{"./instances":18}],21:[function(require,module,exports){
'use strict';

var _ = require('../lib/helper');
var dom = require('../lib/dom');
var instances = require('./instances');
var updateGeometry = require('./update-geometry');
var updateScroll = require('./update-scroll');

module.exports = function (element) {
  var i = instances.get(element);

  if (!i) {
    return;
  }

  // Recalcuate negative scrollLeft adjustment
  i.negativeScrollAdjustment = i.isNegativeScroll ? element.scrollWidth - element.clientWidth : 0;

  // Recalculate rail margins
  dom.css(i.scrollbarXRail, 'display', 'block');
  dom.css(i.scrollbarYRail, 'display', 'block');
  i.railXMarginWidth = _.toInt(dom.css(i.scrollbarXRail, 'marginLeft')) + _.toInt(dom.css(i.scrollbarXRail, 'marginRight'));
  i.railYMarginHeight = _.toInt(dom.css(i.scrollbarYRail, 'marginTop')) + _.toInt(dom.css(i.scrollbarYRail, 'marginBottom'));

  // Hide scrollbars not to affect scrollWidth and scrollHeight
  dom.css(i.scrollbarXRail, 'display', 'none');
  dom.css(i.scrollbarYRail, 'display', 'none');

  updateGeometry(element);

  // Update top/left scroll to trigger events
  updateScroll(element, 'top', element.scrollTop);
  updateScroll(element, 'left', element.scrollLeft);

  dom.css(i.scrollbarXRail, 'display', '');
  dom.css(i.scrollbarYRail, 'display', '');
};

},{"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-geometry":19,"./update-scroll":20}]},{},[1]);

/**
 * jscolor - JavaScript Color Picker
 *
 * @link    http://jscolor.com
 * @license For open source use: GPLv3
 *          For commercial use: JSColor Commercial License
 * @author  Jan Odvarko
 * @version 2.0.4
 *
 * See usage examples at http://jscolor.com/examples/
 */


"use strict";


if (!window.jscolor) { window.jscolor = (function () {


var jsc = {


	register : function () {
		jsc.attachDOMReadyEvent(jsc.init);
		jsc.attachEvent(document, 'mousedown', jsc.onDocumentMouseDown);
		jsc.attachEvent(document, 'touchstart', jsc.onDocumentTouchStart);
		jsc.attachEvent(window, 'resize', jsc.onWindowResize);
	},


	init : function () {
		if (jsc.jscolor.lookupClass) {
			jsc.jscolor.installByClassName(jsc.jscolor.lookupClass);
		}
	},


	tryInstallOnElements : function (elms, className) {
		var matchClass = new RegExp('(^|\\s)(' + className + ')(\\s*(\\{[^}]*\\})|\\s|$)', 'i');

		for (var i = 0; i < elms.length; i += 1) {
			if (elms[i].type !== undefined && elms[i].type.toLowerCase() == 'color') {
				if (jsc.isColorAttrSupported) {
					// skip inputs of type 'color' if supported by the browser
					continue;
				}
			}
			var m;
			if (!elms[i].jscolor && elms[i].className && (m = elms[i].className.match(matchClass))) {
				var targetElm = elms[i];
				var optsStr = null;

				var dataOptions = jsc.getDataAttr(targetElm, 'jscolor');
				if (dataOptions !== null) {
					optsStr = dataOptions;
				} else if (m[4]) {
					optsStr = m[4];
				}

				var opts = {};
				if (optsStr) {
					try {
						opts = (new Function ('return (' + optsStr + ')'))();
					} catch(eParseError) {
						jsc.warn('Error parsing jscolor options: ' + eParseError + ':\n' + optsStr);
					}
				}
				targetElm.jscolor = new jsc.jscolor(targetElm, opts);
			}
		}
	},


	isColorAttrSupported : (function () {
		var elm = document.createElement('input');
		if (elm.setAttribute) {
			elm.setAttribute('type', 'color');
			if (elm.type.toLowerCase() == 'color') {
				return true;
			}
		}
		return false;
	})(),


	isCanvasSupported : (function () {
		var elm = document.createElement('canvas');
		return !!(elm.getContext && elm.getContext('2d'));
	})(),


	fetchElement : function (mixed) {
		return typeof mixed === 'string' ? document.getElementById(mixed) : mixed;
	},


	isElementType : function (elm, type) {
		return elm.nodeName.toLowerCase() === type.toLowerCase();
	},


	getDataAttr : function (el, name) {
		var attrName = 'data-' + name;
		var attrValue = el.getAttribute(attrName);
		if (attrValue !== null) {
			return attrValue;
		}
		return null;
	},


	attachEvent : function (el, evnt, func) {
		if (el.addEventListener) {
			el.addEventListener(evnt, func, false);
		} else if (el.attachEvent) {
			el.attachEvent('on' + evnt, func);
		}
	},


	detachEvent : function (el, evnt, func) {
		if (el.removeEventListener) {
			el.removeEventListener(evnt, func, false);
		} else if (el.detachEvent) {
			el.detachEvent('on' + evnt, func);
		}
	},


	_attachedGroupEvents : {},


	attachGroupEvent : function (groupName, el, evnt, func) {
		if (!jsc._attachedGroupEvents.hasOwnProperty(groupName)) {
			jsc._attachedGroupEvents[groupName] = [];
		}
		jsc._attachedGroupEvents[groupName].push([el, evnt, func]);
		jsc.attachEvent(el, evnt, func);
	},


	detachGroupEvents : function (groupName) {
		if (jsc._attachedGroupEvents.hasOwnProperty(groupName)) {
			for (var i = 0; i < jsc._attachedGroupEvents[groupName].length; i += 1) {
				var evt = jsc._attachedGroupEvents[groupName][i];
				jsc.detachEvent(evt[0], evt[1], evt[2]);
			}
			delete jsc._attachedGroupEvents[groupName];
		}
	},


	attachDOMReadyEvent : function (func) {
		var fired = false;
		var fireOnce = function () {
			if (!fired) {
				fired = true;
				func();
			}
		};

		if (document.readyState === 'complete') {
			setTimeout(fireOnce, 1); // async
			return;
		}

		if (document.addEventListener) {
			document.addEventListener('DOMContentLoaded', fireOnce, false);

			// Fallback
			window.addEventListener('load', fireOnce, false);

		} else if (document.attachEvent) {
			// IE
			document.attachEvent('onreadystatechange', function () {
				if (document.readyState === 'complete') {
					document.detachEvent('onreadystatechange', arguments.callee);
					fireOnce();
				}
			})

			// Fallback
			window.attachEvent('onload', fireOnce);

			// IE7/8
			if (document.documentElement.doScroll && window == window.top) {
				var tryScroll = function () {
					if (!document.body) { return; }
					try {
						document.documentElement.doScroll('left');
						fireOnce();
					} catch (e) {
						setTimeout(tryScroll, 1);
					}
				};
				tryScroll();
			}
		}
	},


	warn : function (msg) {
		if (window.console && window.console.warn) {
			window.console.warn(msg);
		}
	},


	preventDefault : function (e) {
		if (e.preventDefault) { e.preventDefault(); }
		e.returnValue = false;
	},


	captureTarget : function (target) {
		// IE
		if (target.setCapture) {
			jsc._capturedTarget = target;
			jsc._capturedTarget.setCapture();
		}
	},


	releaseTarget : function () {
		// IE
		if (jsc._capturedTarget) {
			jsc._capturedTarget.releaseCapture();
			jsc._capturedTarget = null;
		}
	},


	fireEvent : function (el, evnt) {
		if (!el) {
			return;
		}
		if (document.createEvent) {
			var ev = document.createEvent('HTMLEvents');
			ev.initEvent(evnt, true, true);
			el.dispatchEvent(ev);
		} else if (document.createEventObject) {
			var ev = document.createEventObject();
			el.fireEvent('on' + evnt, ev);
		} else if (el['on' + evnt]) { // alternatively use the traditional event model
			el['on' + evnt]();
		}
	},


	classNameToList : function (className) {
		return className.replace(/^\s+|\s+$/g, '').split(/\s+/);
	},


	// The className parameter (str) can only contain a single class name
	hasClass : function (elm, className) {
		if (!className) {
			return false;
		}
		return -1 != (' ' + elm.className.replace(/\s+/g, ' ') + ' ').indexOf(' ' + className + ' ');
	},


	// The className parameter (str) can contain multiple class names separated by whitespace
	setClass : function (elm, className) {
		var classList = jsc.classNameToList(className);
		for (var i = 0; i < classList.length; i += 1) {
			if (!jsc.hasClass(elm, classList[i])) {
				elm.className += (elm.className ? ' ' : '') + classList[i];
			}
		}
	},


	// The className parameter (str) can contain multiple class names separated by whitespace
	unsetClass : function (elm, className) {
		var classList = jsc.classNameToList(className);
		for (var i = 0; i < classList.length; i += 1) {
			var repl = new RegExp(
				'^\\s*' + classList[i] + '\\s*|' +
				'\\s*' + classList[i] + '\\s*$|' +
				'\\s+' + classList[i] + '(\\s+)',
				'g'
			);
			elm.className = elm.className.replace(repl, '$1');
		}
	},


	getStyle : function (elm) {
		return window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle;
	},


	setStyle : (function () {
		var helper = document.createElement('div');
		var getSupportedProp = function (names) {
			for (var i = 0; i < names.length; i += 1) {
				if (names[i] in helper.style) {
					return names[i];
				}
			}
		};
		var props = {
			borderRadius: getSupportedProp(['borderRadius', 'MozBorderRadius', 'webkitBorderRadius']),
			boxShadow: getSupportedProp(['boxShadow', 'MozBoxShadow', 'webkitBoxShadow'])
		};
		return function (elm, prop, value) {
			switch (prop.toLowerCase()) {
			case 'opacity':
				var alphaOpacity = Math.round(parseFloat(value) * 100);
				elm.style.opacity = value;
				elm.style.filter = 'alpha(opacity=' + alphaOpacity + ')';
				break;
			default:
				elm.style[props[prop]] = value;
				break;
			}
		};
	})(),


	setBorderRadius : function (elm, value) {
		jsc.setStyle(elm, 'borderRadius', value || '0');
	},


	setBoxShadow : function (elm, value) {
		jsc.setStyle(elm, 'boxShadow', value || 'none');
	},


	getElementPos : function (e, relativeToViewport) {
		var x=0, y=0;
		var rect = e.getBoundingClientRect();
		x = rect.left;
		y = rect.top;
		if (!relativeToViewport) {
			var viewPos = jsc.getViewPos();
			x += viewPos[0];
			y += viewPos[1];
		}
		return [x, y];
	},


	getElementSize : function (e) {
		return [e.offsetWidth, e.offsetHeight];
	},


	// get pointer's X/Y coordinates relative to viewport
	getAbsPointerPos : function (e) {
		if (!e) { e = window.event; }
		var x = 0, y = 0;
		if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) {
			// touch devices
			x = e.changedTouches[0].clientX;
			y = e.changedTouches[0].clientY;
		} else if (typeof e.clientX === 'number') {
			x = e.clientX;
			y = e.clientY;
		}
		return { x: x, y: y };
	},


	// get pointer's X/Y coordinates relative to target element
	getRelPointerPos : function (e) {
		if (!e) { e = window.event; }
		var target = e.target || e.srcElement;
		var targetRect = target.getBoundingClientRect();

		var x = 0, y = 0;

		var clientX = 0, clientY = 0;
		if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) {
			// touch devices
			clientX = e.changedTouches[0].clientX;
			clientY = e.changedTouches[0].clientY;
		} else if (typeof e.clientX === 'number') {
			clientX = e.clientX;
			clientY = e.clientY;
		}

		x = clientX - targetRect.left;
		y = clientY - targetRect.top;
		return { x: x, y: y };
	},


	getViewPos : function () {
		var doc = document.documentElement;
		return [
			(window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0),
			(window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
		];
	},


	getViewSize : function () {
		var doc = document.documentElement;
		return [
			(window.innerWidth || doc.clientWidth),
			(window.innerHeight || doc.clientHeight),
		];
	},


	redrawPosition : function () {

		if (jsc.picker && jsc.picker.owner) {
			var thisObj = jsc.picker.owner;

			var tp, vp;

			if (thisObj.fixed) {
				// Fixed elements are positioned relative to viewport,
				// therefore we can ignore the scroll offset
				tp = jsc.getElementPos(thisObj.targetElement, true); // target pos
				vp = [0, 0]; // view pos
			} else {
				tp = jsc.getElementPos(thisObj.targetElement); // target pos
				vp = jsc.getViewPos(); // view pos
			}

			var ts = jsc.getElementSize(thisObj.targetElement); // target size
			var vs = jsc.getViewSize(); // view size
			var ps = jsc.getPickerOuterDims(thisObj); // picker size
			var a, b, c;
			switch (thisObj.position.toLowerCase()) {
				case 'left': a=1; b=0; c=-1; break;
				case 'right':a=1; b=0; c=1; break;
				case 'top':  a=0; b=1; c=-1; break;
				default:     a=0; b=1; c=1; break;
			}
			var l = (ts[b]+ps[b])/2;

			// compute picker position
			if (!thisObj.smartPosition) {
				var pp = [
					tp[a],
					tp[b]+ts[b]-l+l*c
				];
			} else {
				var pp = [
					-vp[a]+tp[a]+ps[a] > vs[a] ?
						(-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) :
						tp[a],
					-vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ?
						(-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) :
						(tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c)
				];
			}

			var x = pp[a];
			var y = pp[b];
			var positionValue = thisObj.fixed ? 'fixed' : 'absolute';
			var contractShadow =
				(pp[0] + ps[0] > tp[0] || pp[0] < tp[0] + ts[0]) &&
				(pp[1] + ps[1] < tp[1] + ts[1]);

			jsc._drawPosition(thisObj, x, y, positionValue, contractShadow);
		}
	},


	_drawPosition : function (thisObj, x, y, positionValue, contractShadow) {
		var vShadow = contractShadow ? 0 : thisObj.shadowBlur; // px

		jsc.picker.wrap.style.position = positionValue;
		jsc.picker.wrap.style.left = x + 'px';
		jsc.picker.wrap.style.top = y + 'px';

		jsc.setBoxShadow(
			jsc.picker.boxS,
			thisObj.shadow ?
				new jsc.BoxShadow(0, vShadow, thisObj.shadowBlur, 0, thisObj.shadowColor) :
				null);
	},


	getPickerDims : function (thisObj) {
		var displaySlider = !!jsc.getSliderComponent(thisObj);
		var dims = [
			2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.width +
				(displaySlider ? 2 * thisObj.insetWidth + jsc.getPadToSliderPadding(thisObj) + thisObj.sliderSize : 0),
			2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.height +
				(thisObj.closable ? 2 * thisObj.insetWidth + thisObj.padding + thisObj.buttonHeight : 0)
		];
		return dims;
	},


	getPickerOuterDims : function (thisObj) {
		var dims = jsc.getPickerDims(thisObj);
		return [
			dims[0] + 2 * thisObj.borderWidth,
			dims[1] + 2 * thisObj.borderWidth
		];
	},


	getPadToSliderPadding : function (thisObj) {
		return Math.max(thisObj.padding, 1.5 * (2 * thisObj.pointerBorderWidth + thisObj.pointerThickness));
	},


	getPadYComponent : function (thisObj) {
		switch (thisObj.mode.charAt(1).toLowerCase()) {
			case 'v': return 'v'; break;
		}
		return 's';
	},


	getSliderComponent : function (thisObj) {
		if (thisObj.mode.length > 2) {
			switch (thisObj.mode.charAt(2).toLowerCase()) {
				case 's': return 's'; break;
				case 'v': return 'v'; break;
			}
		}
		return null;
	},


	onDocumentMouseDown : function (e) {
		if (!e) { e = window.event; }
		var target = e.target || e.srcElement;

		if (target._jscLinkedInstance) {
			if (target._jscLinkedInstance.showOnClick) {
				target._jscLinkedInstance.show();
			}
		} else if (target._jscControlName) {
			jsc.onControlPointerStart(e, target, target._jscControlName, 'mouse');
		} else {
			// Mouse is outside the picker controls -> hide the color picker!
			if (jsc.picker && jsc.picker.owner) {
				jsc.picker.owner.hide();
			}
		}
	},


	onDocumentTouchStart : function (e) {
		if (!e) { e = window.event; }
		var target = e.target || e.srcElement;

		if (target._jscLinkedInstance) {
			if (target._jscLinkedInstance.showOnClick) {
				target._jscLinkedInstance.show();
			}
		} else if (target._jscControlName) {
			jsc.onControlPointerStart(e, target, target._jscControlName, 'touch');
		} else {
			if (jsc.picker && jsc.picker.owner) {
				jsc.picker.owner.hide();
			}
		}
	},


	onWindowResize : function (e) {
		jsc.redrawPosition();
	},


	onParentScroll : function (e) {
		// hide the picker when one of the parent elements is scrolled
		if (jsc.picker && jsc.picker.owner) {
			jsc.picker.owner.hide();
		}
	},


	_pointerMoveEvent : {
		mouse: 'mousemove',
		touch: 'touchmove'
	},
	_pointerEndEvent : {
		mouse: 'mouseup',
		touch: 'touchend'
	},


	_pointerOrigin : null,
	_capturedTarget : null,


	onControlPointerStart : function (e, target, controlName, pointerType) {
		var thisObj = target._jscInstance;

		jsc.preventDefault(e);
		jsc.captureTarget(target);

		var registerDragEvents = function (doc, offset) {
			jsc.attachGroupEvent('drag', doc, jsc._pointerMoveEvent[pointerType],
				jsc.onDocumentPointerMove(e, target, controlName, pointerType, offset));
			jsc.attachGroupEvent('drag', doc, jsc._pointerEndEvent[pointerType],
				jsc.onDocumentPointerEnd(e, target, controlName, pointerType));
		};

		registerDragEvents(document, [0, 0]);

		if (window.parent && window.frameElement) {
			var rect = window.frameElement.getBoundingClientRect();
			var ofs = [-rect.left, -rect.top];
			registerDragEvents(window.parent.window.document, ofs);
		}

		var abs = jsc.getAbsPointerPos(e);
		var rel = jsc.getRelPointerPos(e);
		jsc._pointerOrigin = {
			x: abs.x - rel.x,
			y: abs.y - rel.y
		};

		switch (controlName) {
		case 'pad':
			// if the slider is at the bottom, move it up
			switch (jsc.getSliderComponent(thisObj)) {
			case 's': if (thisObj.hsv[1] === 0) { thisObj.fromHSV(null, 100, null); }; break;
			case 'v': if (thisObj.hsv[2] === 0) { thisObj.fromHSV(null, null, 100); }; break;
			}
			jsc.setPad(thisObj, e, 0, 0);
			break;

		case 'sld':
			jsc.setSld(thisObj, e, 0);
			break;
		}

		jsc.dispatchFineChange(thisObj);
	},


	onDocumentPointerMove : function (e, target, controlName, pointerType, offset) {
		return function (e) {
			var thisObj = target._jscInstance;
			switch (controlName) {
			case 'pad':
				if (!e) { e = window.event; }
				jsc.setPad(thisObj, e, offset[0], offset[1]);
				jsc.dispatchFineChange(thisObj);
				break;

			case 'sld':
				if (!e) { e = window.event; }
				jsc.setSld(thisObj, e, offset[1]);
				jsc.dispatchFineChange(thisObj);
				break;
			}
		}
	},


	onDocumentPointerEnd : function (e, target, controlName, pointerType) {
		return function (e) {
			var thisObj = target._jscInstance;
			jsc.detachGroupEvents('drag');
			jsc.releaseTarget();
			// Always dispatch changes after detaching outstanding mouse handlers,
			// in case some user interaction will occur in user's onchange callback
			// that would intrude with current mouse events
			jsc.dispatchChange(thisObj);
		};
	},


	dispatchChange : function (thisObj) {
		if (thisObj.valueElement) {
			if (jsc.isElementType(thisObj.valueElement, 'input')) {
				jsc.fireEvent(thisObj.valueElement, 'change');
			}
		}
	},


	dispatchFineChange : function (thisObj) {
		if (thisObj.onFineChange) {
			var callback;
			if (typeof thisObj.onFineChange === 'string') {
				callback = new Function (thisObj.onFineChange);
			} else {
				callback = thisObj.onFineChange;
			}
			callback.call(thisObj);
		}
	},


	setPad : function (thisObj, e, ofsX, ofsY) {
		var pointerAbs = jsc.getAbsPointerPos(e);
		var x = ofsX + pointerAbs.x - jsc._pointerOrigin.x - thisObj.padding - thisObj.insetWidth;
		var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth;

		var xVal = x * (360 / (thisObj.width - 1));
		var yVal = 100 - (y * (100 / (thisObj.height - 1)));

		switch (jsc.getPadYComponent(thisObj)) {
		case 's': thisObj.fromHSV(xVal, yVal, null, jsc.leaveSld); break;
		case 'v': thisObj.fromHSV(xVal, null, yVal, jsc.leaveSld); break;
		}
	},


	setSld : function (thisObj, e, ofsY) {
		var pointerAbs = jsc.getAbsPointerPos(e);
		var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth;

		var yVal = 100 - (y * (100 / (thisObj.height - 1)));

		switch (jsc.getSliderComponent(thisObj)) {
		case 's': thisObj.fromHSV(null, yVal, null, jsc.leavePad); break;
		case 'v': thisObj.fromHSV(null, null, yVal, jsc.leavePad); break;
		}
	},


	_vmlNS : 'jsc_vml_',
	_vmlCSS : 'jsc_vml_css_',
	_vmlReady : false,


	initVML : function () {
		if (!jsc._vmlReady) {
			// init VML namespace
			var doc = document;
			if (!doc.namespaces[jsc._vmlNS]) {
				doc.namespaces.add(jsc._vmlNS, 'urn:schemas-microsoft-com:vml');
			}
			if (!doc.styleSheets[jsc._vmlCSS]) {
				var tags = ['shape', 'shapetype', 'group', 'background', 'path', 'formulas', 'handles', 'fill', 'stroke', 'shadow', 'textbox', 'textpath', 'imagedata', 'line', 'polyline', 'curve', 'rect', 'roundrect', 'oval', 'arc', 'image'];
				var ss = doc.createStyleSheet();
				ss.owningElement.id = jsc._vmlCSS;
				for (var i = 0; i < tags.length; i += 1) {
					ss.addRule(jsc._vmlNS + '\\:' + tags[i], 'behavior:url(#default#VML);');
				}
			}
			jsc._vmlReady = true;
		}
	},


	createPalette : function () {

		var paletteObj = {
			elm: null,
			draw: null
		};

		if (jsc.isCanvasSupported) {
			// Canvas implementation for modern browsers

			var canvas = document.createElement('canvas');
			var ctx = canvas.getContext('2d');

			var drawFunc = function (width, height, type) {
				canvas.width = width;
				canvas.height = height;

				ctx.clearRect(0, 0, canvas.width, canvas.height);

				var hGrad = ctx.createLinearGradient(0, 0, canvas.width, 0);
				hGrad.addColorStop(0 / 6, '#F00');
				hGrad.addColorStop(1 / 6, '#FF0');
				hGrad.addColorStop(2 / 6, '#0F0');
				hGrad.addColorStop(3 / 6, '#0FF');
				hGrad.addColorStop(4 / 6, '#00F');
				hGrad.addColorStop(5 / 6, '#F0F');
				hGrad.addColorStop(6 / 6, '#F00');

				ctx.fillStyle = hGrad;
				ctx.fillRect(0, 0, canvas.width, canvas.height);

				var vGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
				switch (type.toLowerCase()) {
				case 's':
					vGrad.addColorStop(0, 'rgba(255,255,255,0)');
					vGrad.addColorStop(1, 'rgba(255,255,255,1)');
					break;
				case 'v':
					vGrad.addColorStop(0, 'rgba(0,0,0,0)');
					vGrad.addColorStop(1, 'rgba(0,0,0,1)');
					break;
				}
				ctx.fillStyle = vGrad;
				ctx.fillRect(0, 0, canvas.width, canvas.height);
			};

			paletteObj.elm = canvas;
			paletteObj.draw = drawFunc;

		} else {
			// VML fallback for IE 7 and 8

			jsc.initVML();

			var vmlContainer = document.createElement('div');
			vmlContainer.style.position = 'relative';
			vmlContainer.style.overflow = 'hidden';

			var hGrad = document.createElement(jsc._vmlNS + ':fill');
			hGrad.type = 'gradient';
			hGrad.method = 'linear';
			hGrad.angle = '90';
			hGrad.colors = '16.67% #F0F, 33.33% #00F, 50% #0FF, 66.67% #0F0, 83.33% #FF0'

			var hRect = document.createElement(jsc._vmlNS + ':rect');
			hRect.style.position = 'absolute';
			hRect.style.left = -1 + 'px';
			hRect.style.top = -1 + 'px';
			hRect.stroked = false;
			hRect.appendChild(hGrad);
			vmlContainer.appendChild(hRect);

			var vGrad = document.createElement(jsc._vmlNS + ':fill');
			vGrad.type = 'gradient';
			vGrad.method = 'linear';
			vGrad.angle = '180';
			vGrad.opacity = '0';

			var vRect = document.createElement(jsc._vmlNS + ':rect');
			vRect.style.position = 'absolute';
			vRect.style.left = -1 + 'px';
			vRect.style.top = -1 + 'px';
			vRect.stroked = false;
			vRect.appendChild(vGrad);
			vmlContainer.appendChild(vRect);

			var drawFunc = function (width, height, type) {
				vmlContainer.style.width = width + 'px';
				vmlContainer.style.height = height + 'px';

				hRect.style.width =
				vRect.style.width =
					(width + 1) + 'px';
				hRect.style.height =
				vRect.style.height =
					(height + 1) + 'px';

				// Colors must be specified during every redraw, otherwise IE won't display
				// a full gradient during a subsequential redraw
				hGrad.color = '#F00';
				hGrad.color2 = '#F00';

				switch (type.toLowerCase()) {
				case 's':
					vGrad.color = vGrad.color2 = '#FFF';
					break;
				case 'v':
					vGrad.color = vGrad.color2 = '#000';
					break;
				}
			};
			
			paletteObj.elm = vmlContainer;
			paletteObj.draw = drawFunc;
		}

		return paletteObj;
	},


	createSliderGradient : function () {

		var sliderObj = {
			elm: null,
			draw: null
		};

		if (jsc.isCanvasSupported) {
			// Canvas implementation for modern browsers

			var canvas = document.createElement('canvas');
			var ctx = canvas.getContext('2d');

			var drawFunc = function (width, height, color1, color2) {
				canvas.width = width;
				canvas.height = height;

				ctx.clearRect(0, 0, canvas.width, canvas.height);

				var grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
				grad.addColorStop(0, color1);
				grad.addColorStop(1, color2);

				ctx.fillStyle = grad;
				ctx.fillRect(0, 0, canvas.width, canvas.height);
			};

			sliderObj.elm = canvas;
			sliderObj.draw = drawFunc;

		} else {
			// VML fallback for IE 7 and 8

			jsc.initVML();

			var vmlContainer = document.createElement('div');
			vmlContainer.style.position = 'relative';
			vmlContainer.style.overflow = 'hidden';

			var grad = document.createElement(jsc._vmlNS + ':fill');
			grad.type = 'gradient';
			grad.method = 'linear';
			grad.angle = '180';

			var rect = document.createElement(jsc._vmlNS + ':rect');
			rect.style.position = 'absolute';
			rect.style.left = -1 + 'px';
			rect.style.top = -1 + 'px';
			rect.stroked = false;
			rect.appendChild(grad);
			vmlContainer.appendChild(rect);

			var drawFunc = function (width, height, color1, color2) {
				vmlContainer.style.width = width + 'px';
				vmlContainer.style.height = height + 'px';

				rect.style.width = (width + 1) + 'px';
				rect.style.height = (height + 1) + 'px';

				grad.color = color1;
				grad.color2 = color2;
			};
			
			sliderObj.elm = vmlContainer;
			sliderObj.draw = drawFunc;
		}

		return sliderObj;
	},


	leaveValue : 1<<0,
	leaveStyle : 1<<1,
	leavePad : 1<<2,
	leaveSld : 1<<3,


	BoxShadow : (function () {
		var BoxShadow = function (hShadow, vShadow, blur, spread, color, inset) {
			this.hShadow = hShadow;
			this.vShadow = vShadow;
			this.blur = blur;
			this.spread = spread;
			this.color = color;
			this.inset = !!inset;
		};

		BoxShadow.prototype.toString = function () {
			var vals = [
				Math.round(this.hShadow) + 'px',
				Math.round(this.vShadow) + 'px',
				Math.round(this.blur) + 'px',
				Math.round(this.spread) + 'px',
				this.color
			];
			if (this.inset) {
				vals.push('inset');
			}
			return vals.join(' ');
		};

		return BoxShadow;
	})(),


	//
	// Usage:
	// var myColor = new jscolor(<targetElement> [, <options>])
	//

	jscolor : function (targetElement, options) {

		// General options
		//
		this.value = null; // initial HEX color. To change it later, use methods fromString(), fromHSV() and fromRGB()
		this.valueElement = targetElement; // element that will be used to display and input the color code
		this.styleElement = targetElement; // element that will preview the picked color using CSS backgroundColor
		this.required = true; // whether the associated text <input> can be left empty
		this.refine = true; // whether to refine the entered color code (e.g. uppercase it and remove whitespace)
		this.hash = false; // whether to prefix the HEX color code with # symbol
		this.uppercase = true; // whether to uppercase the color code
		this.onFineChange = null; // called instantly every time the color changes (value can be either a function or a string with javascript code)
		this.activeClass = 'jscolor-active'; // class to be set to the target element when a picker window is open on it
		this.minS = 0; // min allowed saturation (0 - 100)
		this.maxS = 100; // max allowed saturation (0 - 100)
		this.minV = 0; // min allowed value (brightness) (0 - 100)
		this.maxV = 100; // max allowed value (brightness) (0 - 100)

		// Accessing the picked color
		//
		this.hsv = [0, 0, 100]; // read-only  [0-360, 0-100, 0-100]
		this.rgb = [255, 255, 255]; // read-only  [0-255, 0-255, 0-255]

		// Color Picker options
		//
		this.width = 181; // width of color palette (in px)
		this.height = 101; // height of color palette (in px)
		this.showOnClick = true; // whether to display the color picker when user clicks on its target element
		this.mode = 'HSV'; // HSV | HVS | HS | HV - layout of the color picker controls
		this.position = 'bottom'; // left | right | top | bottom - position relative to the target element
		this.smartPosition = true; // automatically change picker position when there is not enough space for it
		this.sliderSize = 16; // px
		this.crossSize = 8; // px
		this.closable = false; // whether to display the Close button
		this.closeText = 'Close';
		this.buttonColor = '#000000'; // CSS color
		this.buttonHeight = 18; // px
		this.padding = 12; // px
		this.backgroundColor = '#FFFFFF'; // CSS color
		this.borderWidth = 1; // px
		this.borderColor = '#BBBBBB'; // CSS color
		this.borderRadius = 8; // px
		this.insetWidth = 1; // px
		this.insetColor = '#BBBBBB'; // CSS color
		this.shadow = true; // whether to display shadow
		this.shadowBlur = 15; // px
		this.shadowColor = 'rgba(0,0,0,0.2)'; // CSS color
		this.pointerColor = '#4C4C4C'; // px
		this.pointerBorderColor = '#FFFFFF'; // px
        this.pointerBorderWidth = 1; // px
        this.pointerThickness = 2; // px
		this.zIndex = 10000;
		this.container = null; // where to append the color picker (BODY element by default)


		for (var opt in options) {
			if (options.hasOwnProperty(opt)) {
				this[opt] = options[opt];
			}
		}


		this.hide = function () {
			if (isPickerOwner()) {
				detachPicker();
			}
		};


		this.show = function () {
			drawPicker();
		};


		this.redraw = function () {
			if (isPickerOwner()) {
				drawPicker();
			}
		};


		this.importColor = function () {
			if (!this.valueElement) {
				this.exportColor();
			} else {
				if (jsc.isElementType(this.valueElement, 'input')) {
					if (!this.refine) {
						if (!this.fromString(this.valueElement.value, jsc.leaveValue)) {
							if (this.styleElement) {
								this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage;
								this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor;
								this.styleElement.style.color = this.styleElement._jscOrigStyle.color;
							}
							this.exportColor(jsc.leaveValue | jsc.leaveStyle);
						}
					} else if (!this.required && /^\s*$/.test(this.valueElement.value)) {
						this.valueElement.value = '';
						if (this.styleElement) {
							this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage;
							this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor;
							this.styleElement.style.color = this.styleElement._jscOrigStyle.color;
						}
						this.exportColor(jsc.leaveValue | jsc.leaveStyle);

					} else if (this.fromString(this.valueElement.value)) {
						// managed to import color successfully from the value -> OK, don't do anything
					} else {
						this.exportColor();
					}
				} else {
					// not an input element -> doesn't have any value
					this.exportColor();
				}
			}
		};


		this.exportColor = function (flags) {
			if (!(flags & jsc.leaveValue) && this.valueElement) {
				var value = this.toString();
				if (this.uppercase) { value = value.toUpperCase(); }
				if (this.hash) { value = '#' + value; }

				if (jsc.isElementType(this.valueElement, 'input')) {
					this.valueElement.value = value;
				} else {
					this.valueElement.innerHTML = value;
				}
			}
			if (!(flags & jsc.leaveStyle)) {
				if (this.styleElement) {
					this.styleElement.style.backgroundImage = 'none';
					this.styleElement.style.backgroundColor = '#' + this.toString();
					this.styleElement.style.color = this.isLight() ? '#000' : '#FFF';
				}
			}
			if (!(flags & jsc.leavePad) && isPickerOwner()) {
				redrawPad();
			}
			if (!(flags & jsc.leaveSld) && isPickerOwner()) {
				redrawSld();
			}
		};


		// h: 0-360
		// s: 0-100
		// v: 0-100
		//
		this.fromHSV = function (h, s, v, flags) { // null = don't change
			if (h !== null) {
				if (isNaN(h)) { return false; }
				h = Math.max(0, Math.min(360, h));
			}
			if (s !== null) {
				if (isNaN(s)) { return false; }
				s = Math.max(0, Math.min(100, this.maxS, s), this.minS);
			}
			if (v !== null) {
				if (isNaN(v)) { return false; }
				v = Math.max(0, Math.min(100, this.maxV, v), this.minV);
			}

			this.rgb = HSV_RGB(
				h===null ? this.hsv[0] : (this.hsv[0]=h),
				s===null ? this.hsv[1] : (this.hsv[1]=s),
				v===null ? this.hsv[2] : (this.hsv[2]=v)
			);

			this.exportColor(flags);
		};


		// r: 0-255
		// g: 0-255
		// b: 0-255
		//
		this.fromRGB = function (r, g, b, flags) { // null = don't change
			if (r !== null) {
				if (isNaN(r)) { return false; }
				r = Math.max(0, Math.min(255, r));
			}
			if (g !== null) {
				if (isNaN(g)) { return false; }
				g = Math.max(0, Math.min(255, g));
			}
			if (b !== null) {
				if (isNaN(b)) { return false; }
				b = Math.max(0, Math.min(255, b));
			}

			var hsv = RGB_HSV(
				r===null ? this.rgb[0] : r,
				g===null ? this.rgb[1] : g,
				b===null ? this.rgb[2] : b
			);
			if (hsv[0] !== null) {
				this.hsv[0] = Math.max(0, Math.min(360, hsv[0]));
			}
			if (hsv[2] !== 0) {
				this.hsv[1] = hsv[1]===null ? null : Math.max(0, this.minS, Math.min(100, this.maxS, hsv[1]));
			}
			this.hsv[2] = hsv[2]===null ? null : Math.max(0, this.minV, Math.min(100, this.maxV, hsv[2]));

			// update RGB according to final HSV, as some values might be trimmed
			var rgb = HSV_RGB(this.hsv[0], this.hsv[1], this.hsv[2]);
			this.rgb[0] = rgb[0];
			this.rgb[1] = rgb[1];
			this.rgb[2] = rgb[2];

			this.exportColor(flags);
		};


		this.fromString = function (str, flags) {
			var m;
			if (m = str.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i)) {
				// HEX notation
				//

				if (m[1].length === 6) {
					// 6-char notation
					this.fromRGB(
						parseInt(m[1].substr(0,2),16),
						parseInt(m[1].substr(2,2),16),
						parseInt(m[1].substr(4,2),16),
						flags
					);
				} else {
					// 3-char notation
					this.fromRGB(
						parseInt(m[1].charAt(0) + m[1].charAt(0),16),
						parseInt(m[1].charAt(1) + m[1].charAt(1),16),
						parseInt(m[1].charAt(2) + m[1].charAt(2),16),
						flags
					);
				}
				return true;

			} else if (m = str.match(/^\W*rgba?\(([^)]*)\)\W*$/i)) {
				var params = m[1].split(',');
				var re = /^\s*(\d*)(\.\d+)?\s*$/;
				var mR, mG, mB;
				if (
					params.length >= 3 &&
					(mR = params[0].match(re)) &&
					(mG = params[1].match(re)) &&
					(mB = params[2].match(re))
				) {
					var r = parseFloat((mR[1] || '0') + (mR[2] || ''));
					var g = parseFloat((mG[1] || '0') + (mG[2] || ''));
					var b = parseFloat((mB[1] || '0') + (mB[2] || ''));
					this.fromRGB(r, g, b, flags);
					return true;
				}
			}
			return false;
		};


		this.toString = function () {
			return (
				(0x100 | Math.round(this.rgb[0])).toString(16).substr(1) +
				(0x100 | Math.round(this.rgb[1])).toString(16).substr(1) +
				(0x100 | Math.round(this.rgb[2])).toString(16).substr(1)
			);
		};


		this.toHEXString = function () {
			return '#' + this.toString().toUpperCase();
		};


		this.toRGBString = function () {
			return ('rgb(' +
				Math.round(this.rgb[0]) + ',' +
				Math.round(this.rgb[1]) + ',' +
				Math.round(this.rgb[2]) + ')'
			);
		};


		this.isLight = function () {
			return (
				0.213 * this.rgb[0] +
				0.715 * this.rgb[1] +
				0.072 * this.rgb[2] >
				255 / 2
			);
		};


		this._processParentElementsInDOM = function () {
			if (this._linkedElementsProcessed) { return; }
			this._linkedElementsProcessed = true;

			var elm = this.targetElement;
			do {
				// If the target element or one of its parent nodes has fixed position,
				// then use fixed positioning instead
				//
				// Note: In Firefox, getComputedStyle returns null in a hidden iframe,
				// that's why we need to check if the returned style object is non-empty
				var currStyle = jsc.getStyle(elm);
				if (currStyle && currStyle.position.toLowerCase() === 'fixed') {
					this.fixed = true;
				}

				if (elm !== this.targetElement) {
					// Ensure to attach onParentScroll only once to each parent element
					// (multiple targetElements can share the same parent nodes)
					//
					// Note: It's not just offsetParents that can be scrollable,
					// that's why we loop through all parent nodes
					if (!elm._jscEventsAttached) {
						jsc.attachEvent(elm, 'scroll', jsc.onParentScroll);
						elm._jscEventsAttached = true;
					}
				}
			} while ((elm = elm.parentNode) && !jsc.isElementType(elm, 'body'));
		};


		// r: 0-255
		// g: 0-255
		// b: 0-255
		//
		// returns: [ 0-360, 0-100, 0-100 ]
		//
		function RGB_HSV (r, g, b) {
			r /= 255;
			g /= 255;
			b /= 255;
			var n = Math.min(Math.min(r,g),b);
			var v = Math.max(Math.max(r,g),b);
			var m = v - n;
			if (m === 0) { return [ null, 0, 100 * v ]; }
			var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m);
			return [
				60 * (h===6?0:h),
				100 * (m/v),
				100 * v
			];
		}


		// h: 0-360
		// s: 0-100
		// v: 0-100
		//
		// returns: [ 0-255, 0-255, 0-255 ]
		//
		function HSV_RGB (h, s, v) {
			var u = 255 * (v / 100);

			if (h === null) {
				return [ u, u, u ];
			}

			h /= 60;
			s /= 100;

			var i = Math.floor(h);
			var f = i%2 ? h-i : 1-(h-i);
			var m = u * (1 - s);
			var n = u * (1 - s * f);
			switch (i) {
				case 6:
				case 0: return [u,n,m];
				case 1: return [n,u,m];
				case 2: return [m,u,n];
				case 3: return [m,n,u];
				case 4: return [n,m,u];
				case 5: return [u,m,n];
			}
		}


		function detachPicker () {
			jsc.unsetClass(THIS.targetElement, THIS.activeClass);
			jsc.picker.wrap.parentNode.removeChild(jsc.picker.wrap);
			delete jsc.picker.owner;
		}


		function drawPicker () {

			// At this point, when drawing the picker, we know what the parent elements are
			// and we can do all related DOM operations, such as registering events on them
			// or checking their positioning
			THIS._processParentElementsInDOM();

			if (!jsc.picker) {
				jsc.picker = {
					owner: null,
					wrap : document.createElement('div'),
					box : document.createElement('div'),
					boxS : document.createElement('div'), // shadow area
					boxB : document.createElement('div'), // border
					pad : document.createElement('div'),
					padB : document.createElement('div'), // border
					padM : document.createElement('div'), // mouse/touch area
					padPal : jsc.createPalette(),
					cross : document.createElement('div'),
					crossBY : document.createElement('div'), // border Y
					crossBX : document.createElement('div'), // border X
					crossLY : document.createElement('div'), // line Y
					crossLX : document.createElement('div'), // line X
					sld : document.createElement('div'),
					sldB : document.createElement('div'), // border
					sldM : document.createElement('div'), // mouse/touch area
					sldGrad : jsc.createSliderGradient(),
					sldPtrS : document.createElement('div'), // slider pointer spacer
					sldPtrIB : document.createElement('div'), // slider pointer inner border
					sldPtrMB : document.createElement('div'), // slider pointer middle border
					sldPtrOB : document.createElement('div'), // slider pointer outer border
					btn : document.createElement('div'),
					btnT : document.createElement('span') // text
				};

				jsc.picker.pad.appendChild(jsc.picker.padPal.elm);
				jsc.picker.padB.appendChild(jsc.picker.pad);
				jsc.picker.cross.appendChild(jsc.picker.crossBY);
				jsc.picker.cross.appendChild(jsc.picker.crossBX);
				jsc.picker.cross.appendChild(jsc.picker.crossLY);
				jsc.picker.cross.appendChild(jsc.picker.crossLX);
				jsc.picker.padB.appendChild(jsc.picker.cross);
				jsc.picker.box.appendChild(jsc.picker.padB);
				jsc.picker.box.appendChild(jsc.picker.padM);

				jsc.picker.sld.appendChild(jsc.picker.sldGrad.elm);
				jsc.picker.sldB.appendChild(jsc.picker.sld);
				jsc.picker.sldB.appendChild(jsc.picker.sldPtrOB);
				jsc.picker.sldPtrOB.appendChild(jsc.picker.sldPtrMB);
				jsc.picker.sldPtrMB.appendChild(jsc.picker.sldPtrIB);
				jsc.picker.sldPtrIB.appendChild(jsc.picker.sldPtrS);
				jsc.picker.box.appendChild(jsc.picker.sldB);
				jsc.picker.box.appendChild(jsc.picker.sldM);

				jsc.picker.btn.appendChild(jsc.picker.btnT);
				jsc.picker.box.appendChild(jsc.picker.btn);

				jsc.picker.boxB.appendChild(jsc.picker.box);
				jsc.picker.wrap.appendChild(jsc.picker.boxS);
				jsc.picker.wrap.appendChild(jsc.picker.boxB);
			}

			var p = jsc.picker;

			var displaySlider = !!jsc.getSliderComponent(THIS);
			var dims = jsc.getPickerDims(THIS);
			var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize);
			var padToSliderPadding = jsc.getPadToSliderPadding(THIS);
			var borderRadius = Math.min(
				THIS.borderRadius,
				Math.round(THIS.padding * Math.PI)); // px
			var padCursor = 'crosshair';

			// wrap
			p.wrap.style.clear = 'both';
			p.wrap.style.width = (dims[0] + 2 * THIS.borderWidth) + 'px';
			p.wrap.style.height = (dims[1] + 2 * THIS.borderWidth) + 'px';
			p.wrap.style.zIndex = THIS.zIndex;

			// picker
			p.box.style.width = dims[0] + 'px';
			p.box.style.height = dims[1] + 'px';

			p.boxS.style.position = 'absolute';
			p.boxS.style.left = '0';
			p.boxS.style.top = '0';
			p.boxS.style.width = '100%';
			p.boxS.style.height = '100%';
			jsc.setBorderRadius(p.boxS, borderRadius + 'px');

			// picker border
			p.boxB.style.position = 'relative';
			p.boxB.style.border = THIS.borderWidth + 'px solid';
			p.boxB.style.borderColor = THIS.borderColor;
			p.boxB.style.background = THIS.backgroundColor;
			jsc.setBorderRadius(p.boxB, borderRadius + 'px');

			// IE hack:
			// If the element is transparent, IE will trigger the event on the elements under it,
			// e.g. on Canvas or on elements with border
			p.padM.style.background =
			p.sldM.style.background =
				'#FFF';
			jsc.setStyle(p.padM, 'opacity', '0');
			jsc.setStyle(p.sldM, 'opacity', '0');

			// pad
			p.pad.style.position = 'relative';
			p.pad.style.width = THIS.width + 'px';
			p.pad.style.height = THIS.height + 'px';

			// pad palettes (HSV and HVS)
			p.padPal.draw(THIS.width, THIS.height, jsc.getPadYComponent(THIS));

			// pad border
			p.padB.style.position = 'absolute';
			p.padB.style.left = THIS.padding + 'px';
			p.padB.style.top = THIS.padding + 'px';
			p.padB.style.border = THIS.insetWidth + 'px solid';
			p.padB.style.borderColor = THIS.insetColor;

			// pad mouse area
			p.padM._jscInstance = THIS;
			p.padM._jscControlName = 'pad';
			p.padM.style.position = 'absolute';
			p.padM.style.left = '0';
			p.padM.style.top = '0';
			p.padM.style.width = (THIS.padding + 2 * THIS.insetWidth + THIS.width + padToSliderPadding / 2) + 'px';
			p.padM.style.height = dims[1] + 'px';
			p.padM.style.cursor = padCursor;

			// pad cross
			p.cross.style.position = 'absolute';
			p.cross.style.left =
			p.cross.style.top =
				'0';
			p.cross.style.width =
			p.cross.style.height =
				crossOuterSize + 'px';

			// pad cross border Y and X
			p.crossBY.style.position =
			p.crossBX.style.position =
				'absolute';
			p.crossBY.style.background =
			p.crossBX.style.background =
				THIS.pointerBorderColor;
			p.crossBY.style.width =
			p.crossBX.style.height =
				(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px';
			p.crossBY.style.height =
			p.crossBX.style.width =
				crossOuterSize + 'px';
			p.crossBY.style.left =
			p.crossBX.style.top =
				(Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2) - THIS.pointerBorderWidth) + 'px';
			p.crossBY.style.top =
			p.crossBX.style.left =
				'0';

			// pad cross line Y and X
			p.crossLY.style.position =
			p.crossLX.style.position =
				'absolute';
			p.crossLY.style.background =
			p.crossLX.style.background =
				THIS.pointerColor;
			p.crossLY.style.height =
			p.crossLX.style.width =
				(crossOuterSize - 2 * THIS.pointerBorderWidth) + 'px';
			p.crossLY.style.width =
			p.crossLX.style.height =
				THIS.pointerThickness + 'px';
			p.crossLY.style.left =
			p.crossLX.style.top =
				(Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2)) + 'px';
			p.crossLY.style.top =
			p.crossLX.style.left =
				THIS.pointerBorderWidth + 'px';

			// slider
			p.sld.style.overflow = 'hidden';
			p.sld.style.width = THIS.sliderSize + 'px';
			p.sld.style.height = THIS.height + 'px';

			// slider gradient
			p.sldGrad.draw(THIS.sliderSize, THIS.height, '#000', '#000');

			// slider border
			p.sldB.style.display = displaySlider ? 'block' : 'none';
			p.sldB.style.position = 'absolute';
			p.sldB.style.right = THIS.padding + 'px';
			p.sldB.style.top = THIS.padding + 'px';
			p.sldB.style.border = THIS.insetWidth + 'px solid';
			p.sldB.style.borderColor = THIS.insetColor;

			// slider mouse area
			p.sldM._jscInstance = THIS;
			p.sldM._jscControlName = 'sld';
			p.sldM.style.display = displaySlider ? 'block' : 'none';
			p.sldM.style.position = 'absolute';
			p.sldM.style.right = '0';
			p.sldM.style.top = '0';
			p.sldM.style.width = (THIS.sliderSize + padToSliderPadding / 2 + THIS.padding + 2 * THIS.insetWidth) + 'px';
			p.sldM.style.height = dims[1] + 'px';
			p.sldM.style.cursor = 'default';

			// slider pointer inner and outer border
			p.sldPtrIB.style.border =
			p.sldPtrOB.style.border =
				THIS.pointerBorderWidth + 'px solid ' + THIS.pointerBorderColor;

			// slider pointer outer border
			p.sldPtrOB.style.position = 'absolute';
			p.sldPtrOB.style.left = -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px';
			p.sldPtrOB.style.top = '0';

			// slider pointer middle border
			p.sldPtrMB.style.border = THIS.pointerThickness + 'px solid ' + THIS.pointerColor;

			// slider pointer spacer
			p.sldPtrS.style.width = THIS.sliderSize + 'px';
			p.sldPtrS.style.height = sliderPtrSpace + 'px';

			// the Close button
			function setBtnBorder () {
				var insetColors = THIS.insetColor.split(/\s+/);
				var outsetColor = insetColors.length < 2 ? insetColors[0] : insetColors[1] + ' ' + insetColors[0] + ' ' + insetColors[0] + ' ' + insetColors[1];
				p.btn.style.borderColor = outsetColor;
			}
			p.btn.style.display = THIS.closable ? 'block' : 'none';
			p.btn.style.position = 'absolute';
			p.btn.style.left = THIS.padding + 'px';
			p.btn.style.bottom = THIS.padding + 'px';
			p.btn.style.padding = '0 15px';
			p.btn.style.height = THIS.buttonHeight + 'px';
			p.btn.style.border = THIS.insetWidth + 'px solid';
			setBtnBorder();
			p.btn.style.color = THIS.buttonColor;
			p.btn.style.font = '12px sans-serif';
			p.btn.style.textAlign = 'center';
			try {
				p.btn.style.cursor = 'pointer';
			} catch(eOldIE) {
				p.btn.style.cursor = 'hand';
			}
			p.btn.onmousedown = function () {
				THIS.hide();
			};
			p.btnT.style.lineHeight = THIS.buttonHeight + 'px';
			p.btnT.innerHTML = '';
			p.btnT.appendChild(document.createTextNode(THIS.closeText));

			// place pointers
			redrawPad();
			redrawSld();

			// If we are changing the owner without first closing the picker,
			// make sure to first deal with the old owner
			if (jsc.picker.owner && jsc.picker.owner !== THIS) {
				jsc.unsetClass(jsc.picker.owner.targetElement, THIS.activeClass);
			}

			// Set the new picker owner
			jsc.picker.owner = THIS;

			// The redrawPosition() method needs picker.owner to be set, that's why we call it here,
			// after setting the owner
			if (jsc.isElementType(container, 'body')) {
				jsc.redrawPosition();
			} else {
				jsc._drawPosition(THIS, 0, 0, 'relative', false);
			}

			if (p.wrap.parentNode != container) {
				container.appendChild(p.wrap);
			}

			jsc.setClass(THIS.targetElement, THIS.activeClass);
		}


		function redrawPad () {
			// redraw the pad pointer
			switch (jsc.getPadYComponent(THIS)) {
			case 's': var yComponent = 1; break;
			case 'v': var yComponent = 2; break;
			}
			var x = Math.round((THIS.hsv[0] / 360) * (THIS.width - 1));
			var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1));
			var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize);
			var ofs = -Math.floor(crossOuterSize / 2);
			jsc.picker.cross.style.left = (x + ofs) + 'px';
			jsc.picker.cross.style.top = (y + ofs) + 'px';

			// redraw the slider
			switch (jsc.getSliderComponent(THIS)) {
			case 's':
				var rgb1 = HSV_RGB(THIS.hsv[0], 100, THIS.hsv[2]);
				var rgb2 = HSV_RGB(THIS.hsv[0], 0, THIS.hsv[2]);
				var color1 = 'rgb(' +
					Math.round(rgb1[0]) + ',' +
					Math.round(rgb1[1]) + ',' +
					Math.round(rgb1[2]) + ')';
				var color2 = 'rgb(' +
					Math.round(rgb2[0]) + ',' +
					Math.round(rgb2[1]) + ',' +
					Math.round(rgb2[2]) + ')';
				jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2);
				break;
			case 'v':
				var rgb = HSV_RGB(THIS.hsv[0], THIS.hsv[1], 100);
				var color1 = 'rgb(' +
					Math.round(rgb[0]) + ',' +
					Math.round(rgb[1]) + ',' +
					Math.round(rgb[2]) + ')';
				var color2 = '#000';
				jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2);
				break;
			}
		}


		function redrawSld () {
			var sldComponent = jsc.getSliderComponent(THIS);
			if (sldComponent) {
				// redraw the slider pointer
				switch (sldComponent) {
				case 's': var yComponent = 1; break;
				case 'v': var yComponent = 2; break;
				}
				var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1));
				jsc.picker.sldPtrOB.style.top = (y - (2 * THIS.pointerBorderWidth + THIS.pointerThickness) - Math.floor(sliderPtrSpace / 2)) + 'px';
			}
		}


		function isPickerOwner () {
			return jsc.picker && jsc.picker.owner === THIS;
		}


		function blurValue () {
			THIS.importColor();
		}


		// Find the target element
		if (typeof targetElement === 'string') {
			var id = targetElement;
			var elm = document.getElementById(id);
			if (elm) {
				this.targetElement = elm;
			} else {
				jsc.warn('Could not find target element with ID \'' + id + '\'');
			}
		} else if (targetElement) {
			this.targetElement = targetElement;
		} else {
			jsc.warn('Invalid target element: \'' + targetElement + '\'');
		}

		if (this.targetElement._jscLinkedInstance) {
			jsc.warn('Cannot link jscolor twice to the same element. Skipping.');
			return;
		}
		this.targetElement._jscLinkedInstance = this;

		// Find the value element
		this.valueElement = jsc.fetchElement(this.valueElement);
		// Find the style element
		this.styleElement = jsc.fetchElement(this.styleElement);

		var THIS = this;
		var container =
			this.container ?
			jsc.fetchElement(this.container) :
			document.getElementsByTagName('body')[0];
		var sliderPtrSpace = 3; // px

		// For BUTTON elements it's important to stop them from sending the form when clicked
		// (e.g. in Safari)
		if (jsc.isElementType(this.targetElement, 'button')) {
			if (this.targetElement.onclick) {
				var origCallback = this.targetElement.onclick;
				this.targetElement.onclick = function (evt) {
					origCallback.call(this, evt);
					return false;
				};
			} else {
				this.targetElement.onclick = function () { return false; };
			}
		}

		/*
		var elm = this.targetElement;
		do {
			// If the target element or one of its offsetParents has fixed position,
			// then use fixed positioning instead
			//
			// Note: In Firefox, getComputedStyle returns null in a hidden iframe,
			// that's why we need to check if the returned style object is non-empty
			var currStyle = jsc.getStyle(elm);
			if (currStyle && currStyle.position.toLowerCase() === 'fixed') {
				this.fixed = true;
			}

			if (elm !== this.targetElement) {
				// attach onParentScroll so that we can recompute the picker position
				// when one of the offsetParents is scrolled
				if (!elm._jscEventsAttached) {
					jsc.attachEvent(elm, 'scroll', jsc.onParentScroll);
					elm._jscEventsAttached = true;
				}
			}
		} while ((elm = elm.offsetParent) && !jsc.isElementType(elm, 'body'));
		*/

		// valueElement
		if (this.valueElement) {
			if (jsc.isElementType(this.valueElement, 'input')) {
				var updateField = function () {
					THIS.fromString(THIS.valueElement.value, jsc.leaveValue);
					jsc.dispatchFineChange(THIS);
				};
				jsc.attachEvent(this.valueElement, 'keyup', updateField);
				jsc.attachEvent(this.valueElement, 'input', updateField);
				jsc.attachEvent(this.valueElement, 'blur', blurValue);
				this.valueElement.setAttribute('autocomplete', 'off');
			}
		}

		// styleElement
		if (this.styleElement) {
			this.styleElement._jscOrigStyle = {
				backgroundImage : this.styleElement.style.backgroundImage,
				backgroundColor : this.styleElement.style.backgroundColor,
				color : this.styleElement.style.color
			};
		}

		if (this.value) {
			// Try to set the color from the .value option and if unsuccessful,
			// export the current color
			this.fromString(this.value) || this.exportColor();
		} else {
			this.importColor();
		}
	}

};


//================================
// Public properties and methods
//================================


// By default, search for all elements with class="jscolor" and install a color picker on them.
//
// You can change what class name will be looked for by setting the property jscolor.lookupClass
// anywhere in your HTML document. To completely disable the automatic lookup, set it to null.
//
jsc.jscolor.lookupClass = 'jscolor';


jsc.jscolor.installByClassName = function (className) {
	var inputElms = document.getElementsByTagName('input');
	var buttonElms = document.getElementsByTagName('button');

	jsc.tryInstallOnElements(inputElms, className);
	jsc.tryInstallOnElements(buttonElms, className);
};


jsc.register();


return jsc.jscolor;


})(); }

/**
 * Contains static methods that are used to translate text displayed by the PMA.UI framework
 * @namespace PMA.UI.Resources
 */

window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};

; (function () {
    if (typeof PMA.UI.Resources === "object" && typeof PMA.UI.Resources.interpolate === "function") {
        // prevent re-definition
        return;
    }

    var _culture = "";

    PMA.UI.Resources = PMA.UI.Resources || {};

    /**
     * Static hashmap that holds one array per available culture, e.g. PMA.UI.Resources.Labels['en-US'] or PMA.UI.Resources.Labels.en-US
     * @type {Object}
     */
    PMA.UI.Resources.Labels = PMA.UI.Resources.Labels || {};

    /**
     * Sets the current culture
     * @param {string} culture - E.g. "en-US"
     */
    PMA.UI.Resources.setCulture = function (culture) {
        _culture = culture;
    };

    PMA.UI.Resources.interpolate = function (text, o) {
        return text.replace(/{([^{}]*)}/g,
            function (a, b) {
                var r = o[b];
                return typeof r === 'string' || typeof r === 'number' ? r : a;
            });
    };

    /**
     * Translates a string
     * @param {string} text - The text to translate. It can be a simple string literal or it can also contain values that should be interpolated such as "{pageCount} of {totalResultCount} items"
     * @param {object} [o] - If text needs interpolation, o then holds all the properties that should be replaced within it
     * @returns {string} The translated string
     */
    PMA.UI.Resources.translate = function (text, o) {
        if (_culture !== "" && PMA.UI.Resources.Labels.hasOwnProperty(_culture) && PMA.UI.Resources.Labels[_culture].hasOwnProperty(text)) {
            text = PMA.UI.Resources.Labels[_culture][text];
        }
        //else {
        //    console.log("Translation missing: '" + text + "'");
        //}

        if (typeof o === "object") {
            return PMA.UI.Resources.interpolate(text, o);
        }
        else {
            return text;
        }
    };

}());

/**
 * PMA.UI.Components contains UI and utility components that interact with a viewport and PMA.core
 * @namespace PMA.UI.Components
 */

window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Types = window.PMA.UI.Types || {};
window.PMA.UI.Components = window.PMA.UI.Components || {};

(function () {
    PMA.UI.Components = PMA.UI.Components || {};

    /**
     * Events fired by components
     * @readonly
     * @enum {string}
     * @namespace PMA.UI.Components.Events
     */
    PMA.UI.Components.Events = {
        /**
         * Fires when a directory has been selected by a PMA.UI.Components.Tree instance
         * @event PMA.UI.Components.Events#DirectorySelected
         * @param {Object} args
         * @param {string} args.serverUrl
         * @param {string} args.path
         */
        DirectorySelected: "directorySelected",

        /**
         * Fires when a slide image has been selected by a PMA.UI.Components.Tree or PMA.UI.Components.Gallery or a PMA.UI.Components.MetadataTree instance
         * @event PMA.UI.Components.Events#SlideSelected
         * @param {Object} args
         * @param {string} args.serverUrl - The server url this slide belongs to
         * @param {string} args.path - The path to the slide
         * @param {Number} [args.index] - The index of the slide selected in the gallery components (PMA.UI.Components.Gallery only)
         * @param {boolean} [args.userInteraction] - Whether this event was fired by a user interaction or programmatically (PMA.UI.Components.Gallery only)
         */
        SlideSelected: "slideSelected",

        /**
        * Fires when a server node has been selected by a PMA.UI.Components.Tree instance
        * @event PMA.UI.Components.Events#ServerSelected
        * @param {Object} args
        * @param {string} args.serverUrl
        */
        ServerSelected: "serverSelected",

        /**
        * Fires when a server node has been expanded by a PMA.UI.Components.Tree instance
        * @event PMA.UI.Components.Events#ServerExpanded
        * @param {Object} args
        * @param {string} args.serverUrl
        */
        ServerExpanded: "serverExpanded",

        /**
        * Fires when a folder node has been expanded by a PMA.UI.Components.Tree instance
        * @event PMA.UI.Components.Events#DirectoryExpanded
        * @param {Object} args
        * @param {string} args.serverUrl
        * @param {string} args.path
        */
        DirectoryExpanded: "directoryExpanded",

        /**
         * Fires when slide images have been checked by a PMA.UI.Components.Tree
         * @event PMA.UI.Components.Events#MultiSelectionChanged
         * @param {Object[]} args
         * @param {string} args.serverUrl
         * @param {string} args.path
         */
        MultiSelectionChanged: "multiSelectionChanged",

        /**
         * Fires when a slide image has been deselected by a PMA.UI.Components.Tree or PMA.UI.Components.Gallery instance
         * @event PMA.UI.Components.Events#SlideDeSelected
         */
        SlideDeSelected: "slideDeSelected",

        /**
         * Fires when a form has been saved to PMA.core
         * @event PMA.UI.Components.Events#TreeNodeDoubleClicked
         * @param {Object} args
         * @param {string} args.serverUrl
         * @param {string} args.path
         * @param {boolean} isSlide
         */
        TreeNodeDoubleClicked: "treeNodeDoubleClicked",

        /**
         * Fires when image info could not be loaded
         * @event PMA.UI.Components.Events#SlideInfoError
         */
        SlideInfoError: "SlideInfoError",

        /**
         * Fires before a viewport attempts to load an image
         * @event PMA.UI.Components.Events#BeforeSlideLoad
         */
        BeforeSlideLoad: "BeforeSlideLoad",

        /**
         * Fires after a viewport has finished loading an image
         * @event PMA.UI.Components.Events#SlideLoaded
         * @param {Object} args - The slide loaded arguments
         * @param {string} args.serverUrl - The server the slide belongs to
         * @param {string} args.path - The slide path
         * @param {boolean} args.dropped - Whether the slide was loaded via a drag and drop operation
         */
        SlideLoaded: "SlideLoaded",

        /**
         * Fires after when a new annotation has been added
         * @event PMA.UI.Components.Events#AnnotationAdded
         * @param {Object} args
         * @param {Object} args.feature - The annotation object
         */
        AnnotationAdded: "annotationAdded",

        /**
         * Fires when annotation drawing begins
         * @event PMA.UI.Components.Events#AnnotationDrawing
         */
        AnnotationDrawing: "annotationDrawing",

        /**
         * Fires when an annotation has been deleted
         * @event PMA.UI.Components.Events#AnnotationDeleted
         * @param {Object}  args
         * @param {Number}  args.annotationId
         * @param {Object}  args.feature
         */
        AnnotationDeleted: "annotationDeleted",

        /**
         * Fires when an annotation has been modified
         * @event PMA.UI.Components.Events#AnnotationModified
         */
        AnnotationModified: "annotationModified",

        /**
         * Fires when the annotations have been saved to PMA.core
         * @event PMA.UI.Components.Events#AnnotationsSaved
         * @param {Object} args
         * @param {boolean} args.success
         */
        AnnotationsSaved: "annotationsSaved",

        /**
         * Fires when the currently selected annotation(s) have changed
         * @event PMA.UI.Components.Events#AnnotationsSelectionChanged
         * @param {Object[]} args - The selected annotation objects
         */
        AnnotationsSelectionChanged: "annotationsSelectionChanged",

        /**
         * Fires when a form has been saved to PMA.core
         * @event PMA.UI.Components.Events#FormSaved
         * @param {Object} args - The result of the save operation
         * @param {string} args.message - The error message if any
         * @param {boolean} args.success - True if save was successful
         */
        FormSaved: "formSaved",

        /**
         * Fires when a the edit button of a read only form is clicked
         * @event PMA.UI.Components.Events#FormEditClick
         */
        FormEditClick: "formEditClick",

        /**
         * Fires when the state of the synchronization of viewports has changed (enabled/disabled)
         * @event PMA.UI.Components.Events#SyncChanged
         * @param {boolean} enabled - Whether syncronization was enabled
         */
        SyncChanged: "syncChanged",

        /**
         * Fires when a search has started
         * @event PMA.UI.Components.Events#SearchStarted
         */
        SearchStarted: "searchStarted",

        /**
         * Fires when a search has finished
         * @event PMA.UI.Components.Events#SearchFinished
         * @param {Object.<string, string[]>} results - The object containing the result for each server available
         */
        SearchFinished: "searchFinished",

        /**
         * Fires when a search has failed
         * @event PMA.UI.Components.Events#SearchFailed
         * @param {PMA.UI.Components.Tree~server} server - The object containing the server failed to fetch search results for
         */
        SearchFailed: "searchFailed",

        /** 
         * Fires when a form value is expanded by a PMA.UI.Components.MetadataTree
         * @event PMA.UI.Components.Events#ValueExpanded
         * @param {Object} args - The arguments passed to this event
         * @param {string} args.serverUrl - The server url that the form value belongs to
         * @param {string[]} args.slides - An array of the paths for the slides belonging to the formvalue expanded
        */
        ValueExpanded: "valueExpanded",

        /** 
         * Fires when data are dropped on a PMA.UI.Components.Gallery
         * @event PMA.UI.Components.Events#Dropped
         * @param {Object} args - The arguments passed to this event
         * @param {string} args.serverUrl - The server url
         * @param {string} args.path - The path to a slide or directory dropped 
         * @param {boolean} args.isFolder - Whether a directory was dropped  
         * @param {boolean} args.append - Whether the slide/directory was appended or replaced(alt key was pressed)
        */
        Dropped: "dropped",

        /**
         * Fires when a session id authentication fails
         * @event PMA.UI.Components.Events#SessionIdLoginFailed
         * @param {Object} args - The arguments passed to this event
         * @param {string} args.serverUrl - The server url
         */
        SessionIdLoginFailed: "sessionIdLoginFailed",

        /** 
         * Fires when a slide is dropped in the {@link PMA.UI.Components.SlideLoader}
         * @event PMA.UI.Components.Events#BeforeDrop
         * @param {Object} args - The arguments passed to this event
         * @param {string} args.serverUrl - The serverUrl
         * @param {string} args.path - The path to the slide  dropped
         * @param {Object} args.node - The node metadata object
         * @returns {boolean} - Whether to cancel the slide loading or not
        */
        BeforeDrop: "beforeDrop"
    };

    /**
     * PMA.core API methods
     * @readonly
     * @enum {string}
     */
    PMA.UI.Components.ApiMethods = {
        Authenticate: "Authenticate",
        GetFiles: "GetFiles",
        GetDirectories: "GetDirectories",
        GetImageInfo: "GetImageInfo",
        GetImagesInfo: "GetImagesInfo",
        DeAuthenticate: "DeAuthenticate",
        GetForms: "GetForms",
        GetFormDefinitions: "GetFormDefinitions",
        GetFormSubmissions: "GetFormSubmissions",
        GetForm: "GetForm",
        SaveFormDefinition: "SaveFormDefinition",
        DeleteFormDefinition: "DeleteFormDefinition",
        SaveFormData: "SaveFormData",
        GetFormData: "GetFormData",
        GetAnnotations: "GetAnnotations",
        AddAnnotation: "AddAnnotation",
        UpdateAnnotation: "UpdateAnnotation",
        DeleteAnnotation: "DeleteAnnotation",
        GetVersionInfo: "GetVersionInfo",
        QueryFilename: "Filename",
        SaveAnnotations: "SaveAnnotations",
        DistinctValues: "DistinctValues",
        Metadata: "Metadata",
        GetEvents: "GetEvents"
    };

    /**
     * PMA.core scopes for the GetSlides API method
     * @readonly
     * @enum {string}
     */
    PMA.UI.Components.GetSlidesScope = {
        Normal: 0,
        OneLevel: 1,
        Recursive: 2
    };

    /** 
     * The mime type used for drag n drop data transfer
     * @typedef {string} PMA.UI.Components~DragDropMimeType
     */
    PMA.UI.Components.DragDropMimeType = "application/x-pma-node";

    /**
     * An object expected for drag and drop features 
     * @typedef {Object} PMA.UI.Components~dragDropObject
     * @property {string} serverUrl - The serverUrl
     * @property {string} path - The path to a slide or directory
     * @property {boolean} isFolder - Whether this is a path or a directory
     */

    // static variables

    PMA.UI.Components.sessionList = {};
    var alertedForIncompatibility = false;

    // static methods
    var _ = PMA.UI.Resources.translate;

    function combinePath(parts) {
        var rx = /^\/+|\/+$/g; // trim left and right trailing slash
        var rxLeftOnly = /^\/+/g; // trim left trailing slash
        for (var i = 0, max = parts.length; i < max; i++) {
            if (i === max - 1) {
                parts[i] = parts[i].replace(rxLeftOnly, "");
            } else {
                parts[i] = parts[i].replace(rx, "");
            }
        }

        return parts.join("/");
    }

    PMA.UI.Components.parseJson = function (response) {
        if (response === null || response === undefined || response === "") {
            return null;
        }

        var obj = JSON.parse(response);

        if (obj.hasOwnProperty("d")) {
            return obj.d;
        }
        else {
            return obj;
        }
    };

    /**
     * Tries to parse the data passed from a drag and drop operation
     * @param {Object} dataTransfer - The browser dataTransfer object
     * @returns {PMA.UI.Components~dragDropObject} - The parsed drag and drop object or null
     */
    PMA.UI.Components.parseDragData = function (dataTransfer) {
        try {
            var d1 = dataTransfer.getData(PMA.UI.Components.DragDropMimeType);
            return JSON.parse(d1);
        } catch (e) {
        }

        //Try the text for IE compatibility
        try {
            var d2 = dataTransfer.getData("text");
            return JSON.parse(d2);
        } catch (e) {
        }

        return null;
    };

    // browser compatibility
    PMA.UI.Components.checkBrowserCompatibility = function () {
        var supported = true;

        if (typeof window.HTMLElement === "undefined") {
            supported = false;
        } else if (document.all && !document.querySelector) {
            supported = false;
        } else {
            var elem = document.createElement('canvas');
            if (!elem.getContext || !elem.getContext('2d')) {
                supported = false;
            } else if (!window.XMLHttpRequest) {
                supported = false;
            } else {
                // try to initiate a CORS GET request to test if it's supported
                try {
                    var http = new XMLHttpRequest();

                    // the domain doesn't have to exist, we never actually send the request
                    if (window.location.protocol != "https:") {
                        http.open("GET", "http://fake.pathomation.com/", true);
                    } else {
                        http.open("GET", "https://fake.pathomation.com/", true);
                    }
                } catch (e) {
                    supported = false;
                }
            }
        }

        if (!supported && !alertedForIncompatibility) {
            alertedForIncompatibility = true;
            alert(_("Browser not supported."));
        }

        return supported;
    };

    // encodes an object so that it can be passed as an ajax data object
    PMA.UI.Components.formify = function (data) {
        if (data && typeof data === "object") {
            var sData = "";

            var i = 0;
            for (var property in data) {
                if (data.hasOwnProperty(property)) {
                    if (i > 0) {
                        sData += "&";
                    }

                    sData += encodeURIComponent(property) + "=" + encodeURIComponent(data[property]);
                    i++;
                }
            }

            return sData;
        }

        return data;
    };

    PMA.UI.Components.ajax = function (url, method, contentType, data, success, failure) {
        method = method.toUpperCase();
        var sData = null;

        if (contentType && contentType.toLowerCase && contentType.toLowerCase() === "application/json") {
            sData = JSON.stringify(data);
        } else {
            sData = PMA.UI.Components.formify(data);
        }

        if (sData && method == "GET") {
            url = url + "?" + sData;
            sData = null;
        }

        var http = new XMLHttpRequest();
        http.open(method, url, true);

        if (method == "POST" && !contentType) {
            http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        }
        else if (contentType) {
            http.setRequestHeader('Content-Type', contentType);
        }

        http.onreadystatechange = function () {
            if (http.readyState === 4) {
                if (http.status === 200) {
                    if (typeof success === "function") {
                        success(http);
                    }
                }
                else if (typeof failure === "function") {
                    failure(http);
                }
            }
        };

        if (sData) {
            http.send(sData);
        }
        else {
            http.send();
        }
    };

    /**
     * Invokes a PMA.core API method. This method will properly encode all provided parameters.
     * @param {Object} options
     * @param {string} options.serverUrl - The URL of the PMA.core instance
     * @param {string} options.method - The API method to call
     * @param {string} options.httpMethod - The HTTP verb
     * @param {object} options.data - The parameters to pass to the API method
     * @param {string} [options.apiPath="api"] - The API path to append to the server URL
     * @param {string} [options.contentType=""]
     * @param {function} [options.success] - Function to call upon successful method invocation
     * @param {function} [options.failure] - Function to call upon unsuccessful method invocation
     */
    PMA.UI.Components.callApiMethod = function (options) {
        var httpMethod = "GET";
        if (options.httpMethod) {
            httpMethod = options.httpMethod;
        }

        if (!options.apiPath) {
            options.apiPath = "api";
        }

        PMA.UI.Components.ajax(combinePath([options.serverUrl, options.apiPath + "/json/", options.method]), httpMethod, options.contentType, options.data, options.success, options.failure);
    };

    /**
     * Authenticates against a PMA.core server
     * @param  {string} serverUrl
     * @param  {string} username
     * @param  {string} password
     * @param  {string} caller
     * @param  {function} success
     * @param  {function} failure
     */
    PMA.UI.Components.login = function (serverUrl, username, password, caller, success, failure) {
        if (typeof caller !== "string") {
            throw "Caller parameter not supplied";
        }

        PMA.UI.Components.callApiMethod({
            serverUrl: serverUrl,
            method: PMA.UI.Components.ApiMethods.Authenticate,
            data: { username: username, password: password, caller: caller },
            success: function (http) {
                var response = PMA.UI.Components.parseJson(http.responseText);
                if (response && response.Success === true) {
                    // store session ID for this server in global cache
                    PMA.UI.Components.sessionList[serverUrl] = response;

                    if (typeof success === "function") {
                        success(response.SessionId);
                    }
                }
                else if (typeof failure === "function") {
                    failure(response);
                }
            },
            failure: function (http) {
                if (typeof failure === "function") {
                    if (!http.responseText || http.responseText.length === 0) {
                        failure({ Message: _("Authentication failed") });
                    }
                    else {
                        var response = PMA.UI.Components.parseJson(http.responseText);
                        failure(response);
                    }
                }
            }
        });
    };
    /**
     * Returns a URL that points to the thumbnail of a slide image
     * @param  {string} serverUrl
     * @param  {string} sessionId
     * @param  {string} pathOrUid
     * @param  {Number} [orientation=0]
     * @param  {Number} [width=0]
     * @param  {Number} [height=0]
     */
    PMA.UI.Components.getThumbnailUrl = function (serverUrl, sessionId, pathOrUid, orientation, width, height) {
        return combinePath([serverUrl, "thumbnail"]) +
            "?sessionID=" + encodeURIComponent(sessionId) +
            "&pathOrUid=" + encodeURIComponent(pathOrUid) +
            "&orientation=" + (orientation ? orientation : 0) +
            "&w=" + (width ? width : 0) +
            "&h=" + (height ? height : 0);
    };

    /**
        * snapshot parameters
        * @typedef {Object} PMA.UI.Components~snapshotParameters
        * @property {number} x - The x coordinate of the top left point
        * @property {number} y - The y coordinate of the top left point
        * @property {number} width - The width of the viewport
        * @property {number} height - The height of the viewport
        * @property {number} rotation - The rotation of the viewport (in degrees)
        * @property {boolean} flipHorizontally - Wheter the snapshot is flipped horizontally
        * @property {boolean} flipVertically - Wheter the snapshot is flipped vertically
        * @property {Array} [channels] - The selected channels
        * @property {number} [layer=0] - The selected layer
        * @property {number} [timeframe=0] - The selected timeframe
    */

    /**
     * Returns a URL that points to the snapshot of a slide image
     * @param  {string} serverUrl
     * @param  {string} sessionId
     * @param  {string} pathOrUid
     * @param {PMA.UI.Components~snapshotParameters} snapshotParameters - The parameters required for the snapshot
     * @param {Number} thumbWidth - The final snapshot width (this may be smaller due to aspect ratio)
     * @param {Number} thumbHeight - The final snapshot height (this may be smaller due to aspect ratio)
     * @param {string} [format=jpg] - The snapshot image format
     */
    PMA.UI.Components.getSnapshotUrl = function (serverUrl, sessionId, pathOrUid, snapshotParameters, thumbWidth, thumbHeight, format) {
        if (!format) {
            format = "jpg";
        }

        thumbWidth = thumbWidth ? thumbWidth : 150;
        thumbHeight = thumbHeight ? thumbHeight : 150;

        var channelsString = "0";
        if (snapshotParameters.channels) {
            channelsString = snapshotParameters.channels.join(",");
        }
        var selectedTimeFrame = snapshotParameters.timeframe ? snapshotParameters.timeframe : 0;
        var selectedLayer = snapshotParameters.layer ? snapshotParameters.layer : 0;

        var scale = Math.max(thumbWidth / snapshotParameters.width, thumbHeight / snapshotParameters.height);
        if (snapshotParameters.width * scale < 1 || snapshotParameters.height < 1) {
            scale = 1 / Math.max(snapshotParameters.width, snapshotParameters.height);
        }

        var rotation = snapshotParameters.rotation ? snapshotParameters.rotation : 0;

        return combinePath([serverUrl, "region"]) +
            "?sessionID=" + encodeURIComponent(sessionId) +
            "&pathOrUid=" + encodeURIComponent(pathOrUid) +
            "&format=" + encodeURIComponent(format) +
            "&timeframe=" + selectedTimeFrame +
            "&layer=" + selectedLayer +
            "&channels=" + channelsString +
            "&x=" + Math.floor(snapshotParameters.x) +
            "&y=" + Math.floor(snapshotParameters.y) +
            "&width=" + Math.floor(snapshotParameters.width) +
            "&height=" + Math.floor(snapshotParameters.height) +
            "&scale=" + scale +
            "&rotation=" + rotation +
            "&flipHorizontal=" + snapshotParameters.flipHorizontally +
            "&flipVertical=" + snapshotParameters.flipVertically;
    };

    /**
     * Returns a URL that points to the barcode of a slide image. This method doesn't guarantee that a barcode image actually exists.
     * @param  {string} serverUrl
     * @param  {string} sessionId
     * @param  {string} pathOrUid
     * @param  {Number} [rotation=0]
     */
    PMA.UI.Components.getBarcodeUrl = function (serverUrl, sessionId, pathOrUid, rotation) {
        return combinePath([serverUrl, "barcode"]) +
            "?sessionID=" + encodeURIComponent(sessionId) +
            "&pathOrUid=" + encodeURIComponent(pathOrUid) +
            "&rotation=" + (rotation ? rotation : 0);
    };

    // end static methods
}());
// namespace

window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Components = window.PMA.UI.Components || {};

(function ($) {
    var _ = PMA.UI.Resources.translate;
    var ellipseSides = 32;

    function supportsColorPicker() {
        var colorInput;
        colorInput = $('<input type="color" value="!" />')[0];
        return colorInput.type === 'color' && colorInput.value !== '!';
    }

    function toggleDragPanInteraction(map, enabled) {
        map.getInteractions().forEach(function (element, index, array) {
            if (element instanceof ol.interaction.DragPan) {
                element.setActive(enabled);
                return;
            }
        });
    }

    function getStrokeColor() {
        if (this.element === null) {
            return "#000000";
        }

        var scolor = $(this.element).find("li.draw a.active").data("color");
        if (!scolor) {
            return "#000000";
        }
        else {
            return scolor;
        }
    }

    function getPenSize() {
        if (this.element === null) {
            return 1;
        }

        var width = parseInt($(this.element).find("li.draw a.active").data("size"));
        if (isNaN(width) || width < 1) {
            width = 1;
        }

        return width;
    }

    function getAnnotationStyle(strokeColor, width, fillColor, opt_icon, feature, styleGeometryFunction) {
        var imageStyle = null;

        if (!fillColor) {
            fillColor = PMA.UI.View.DefaultFillColor;
        }

        if (!strokeColor) {
            strokeColor = getStrokeColor.call(this);
        }

        if (!width) {
            width = getPenSize.call(this);
        }

        var fill = new ol.style.Fill({
            color: fillColor
        });

        var stroke = new ol.style.Stroke({
            color: strokeColor,
            width: width
        });

        if (!opt_icon) {
            imageStyle = new ol.style.Circle({
                fill: fill,
                stroke: stroke,
                radius: 5
            });
        }
        else {
            imageStyle = new ol.style.Icon({
                anchor: [0.5, 0.5],
                anchorXUnits: 'fraction',
                anchorYUnits: 'fraction',
                opacity: 1,
                src: this.viewport.options.annotations.imageBaseUrl + opt_icon,
                scale: isNaN(this.viewport.options.annotations.imageScale) ? 1 : this.viewport.options.annotations.imageScale
            });
        }

        return new ol.style.Style({
            image: imageStyle,
            fill: fill,
            stroke: stroke,
            text: this.viewport.getAnnotationTextStyle(feature),
            geometry: styleGeometryFunction
        });
    }

    // fixes the y-coordinates of annotations by inverting it
    function annotationTransform(input, output, dimension) {
        for (var i = 0; i < input.length; i += dimension) {
            var x = input[i];
            var y = input[i + 1];

            if (this.flip.vertically !== true) {
                y = this.extent[3] - y;
            }

            if (this.flip.horizontally === true) {
                x = this.extent[2] - x;
            }

            output[i] = x;
            output[i + 1] = y;
        }
    }

    function styleCircleGeometryFunction(feature) {
        var g = feature.getGeometry();

        if (g.getType() === "Point") {
            return new ol.geom.Circle(g.getFirstCoordinate(), this.size[0] / 2);
        }

        return g;
    }

    function styleBoxGeometryFunction(feature) {
        var g = feature.getGeometry();

        if (g.getType() === "Point") {
            var dx = Math.abs(this.size[0] / 2);
            var dy = Math.abs(this.size[1] / 2);
            var c = g.getFirstCoordinate();

            return new ol.geom.Polygon([
                [[c[0] - dx, c[1] - dy],
                [c[0] - dx, c[1] + dy],
                [c[0] + dx, c[1] + dy],
                [c[0] + dx, c[1] - dy],
                [c[0] - dx, c[1] - dy]]
            ]);
        }

        return g;
    }

    function multiPointGeometryFunction(coordinates, geometry) {
        if (!geometry) {
            geometry = new ol.geom.MultiPoint(null);
        }

        geometry.setCoordinates(coordinates);

        return geometry;
    }

    function boxGeometryFunction(coordinates, geometry) {
        var options = this;
        if (options.size) {
            // when we are drawing a rectangle with fixed size, the geometry type is "Point" coordinates has just one X,Y pair, which is our center
            var dx = Math.abs(options.size[0] / 2);
            var dy = Math.abs(options.size[1] / 2);
            var c = coordinates;

            return new ol.geom.Polygon([
                [[c[0] - dx, c[1] - dy],
                [c[0] - dx, c[1] + dy],
                [c[0] + dx, c[1] + dy],
                [c[0] + dx, c[1] - dy],
                [c[0] - dx, c[1] - dy]]
            ]);
        }

        if (!geometry) {
            geometry = new ol.geom.Polygon(null);
        }

        var start = coordinates[0];
        var end = coordinates[1];

        if (!start || !end) {
            return null;
        }

        geometry.setCoordinates([
            [start, [start[0], end[1]], end, [end[0], start[1]], start]
        ]);

        return geometry;
    }

    function circleGeometryFunction(coordinates, opt_geometry) {
        if (this.size) {
            // when we are drawing a rectangle with fixed size, the geometry type is "Point" coordinates has just one X,Y pair, which is our center
            // the size option gives us the circle's diameter
            return new ol.geom.Circle(coordinates, this.size[0] / 2.0);
        }

        var circle = opt_geometry ? /** @type {Circle} */ (opt_geometry) : new ol.geom.Circle([NaN, NaN]);
        var c = coordinates[0];
        var dx = coordinates[0][0] - coordinates[1][0];
        var dy = coordinates[0][1] - coordinates[1][1];
        var radius = Math.sqrt(dx * dx + dy * dy);

        circle.setCenterAndRadius(c, radius);
        return circle;
    }

    function ellipseGeometryFunction(coordinates, geometry) {
        if (!geometry) {
            geometry = new ol.geom.Polygon(null);
        }

        var start = coordinates[0];
        var end = coordinates[1];

        if (!start || !end) {
            return null;
        }

        var radiusY = (start[1] - end[1]) / 2;
        var s = [start[0], start[1] + radiusY];
        var e = end;

        var center = [(e[0] + s[0]) / 2, (e[1] + s[1]) / 2];
        var radius = [center[0] - e[0], center[1] - e[1]];
        var ellipseCoords = [];
        var step = 2 * Math.PI / ellipseSides;

        for (var i = 0; i < ellipseSides; i++) {
            var t = i * step;
            var p = [Math.round(center[0] + radius[0] * Math.cos(t)), Math.round(center[1] + radius[1] * Math.sin(t))];

            ellipseCoords.push(p);
        }

        ellipseCoords.push(ellipseCoords[0]);

        geometry.setCoordinates([ellipseCoords]);

        return geometry;
    }

    // 3d cross product
    function crossProduct(v1, v2) {
        return [
            (v1[1] * v2[2]) - (v1[2] * v2[1]),
            (v1[2] * v2[0]) - (v1[0] * v2[2]),
            (v1[0] * v2[1]) - (v1[1] * v2[0])
        ];
    }

    function vectorLength(v) {
        if (v.length === 3) {
            return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
        }

        return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
    }

    function normalizeVector(v, length) {
        if (v.length === 2) {
            v.push(0);
        }

        if (!length) {
            length = vectorLength(v);
        }

        if (length > 0.00001) {
            var inv = 1.0 / length;
            v[0] *= inv;
            v[1] *= inv;
            v[2] *= inv;
        }

        if (v.length === 3) {
            return [v[0], v[1], v[2]];
        }

        return [v[0], v[1]];
    }

    function vectorMultScalar(v, scalar) {
        if (v.length === 3) {
            return [scalar * v[0], scalar * v[1], scalar * v[2]];
        }

        return [scalar * v[0], scalar * v[1]];
    }

    // calculates v2 - v1
    function vectorDiff(v1, v2) {
        if (v1.lenth === 3 && v2.length === 3) {
            return [v2[0] - v1[0], v2[1] - v1[1], v2[2] - v1[2]];
        }

        return [v2[0] - v1[0], v2[1] - v1[1]];
    }

    function arrowGeometryFunction(coordinates, geometry) {
        if (!geometry) {
            geometry = new ol.geom.Polygon(null);
        }

        var start = coordinates[1];
        var end = coordinates[0];

        var axisZ = [0, 0, 1];

        var diff = vectorDiff(start, end);
        var len = vectorLength(diff);
        var normDirection = normalizeVector(diff, len);

        // a normalized vector, vertical to the arrow axis
        var vertical = crossProduct(normDirection, axisZ);

        // Back        Body          Spearhead
        //                          C
        //                          |\
        //    A____________________B| \
        //    |                     |  \D
        //    |_____________________|  /
        //    G                    F| /
        //                          |/
        //                          E

        // body width is set to 15% of the total length
        var bodyWidth = len * 0.15;
        var bodyLength = len * 0.75;
        var spearheadLength = len * 0.25;
        var spearheadWidth = len * 0.15;

        var a = [start[0] - (vertical[0] * bodyWidth / 2), start[1] - (vertical[1] * bodyWidth / 2)];
        var g = [start[0] + (vertical[0] * bodyWidth / 2), start[1] + (vertical[1] * bodyWidth / 2)];

        var b = [a[0] + normDirection[0] * bodyLength, a[1] + normDirection[1] * bodyLength];
        var f = [g[0] + normDirection[0] * bodyLength, g[1] + normDirection[1] * bodyLength];

        var c = [b[0] - (vertical[0] * spearheadWidth / 2), b[1] - (vertical[1] * spearheadWidth / 2)];
        var e = [f[0] + (vertical[0] * spearheadWidth / 2), f[1] + (vertical[1] * spearheadWidth / 2)];

        var d = [end[0], end[1]];

        geometry.setCoordinates([
            [a, b, c, d, e, f, g, a]
        ]);

        return geometry;
    }

    function showDrawingControls(show, annotationType, feature) {
        if (this.drawingControlsContainer && this.viewport.element.contains(this.drawingControlsContainer)) {
            this.viewport.element.removeChild(this.drawingControlsContainer);
        }

        this.drawingControlsContainer = null;
        this.endDrawingButton = null;
        this.removeLastButton = null;
        this.continueDrawingButton = null;
        this.cancelDrawingButton = null;
        this.counterButton = null;

        if (this.changeFeatureKey != null) {
            ol.Observable.unByKey(this.changeFeatureKey);
        }

        if (!show) {
            return;
        }

        this.changeFeatureKey = null;
        var self = this;
        this.drawingControlsContainer = document.createElement("div");
        this.drawingControlsContainer.className = "ol-control pma-ui-viewport-annotations-drawing";

        this.cancelDrawingButton = document.createElement("button");
        this.cancelDrawingButton.innerHTML = _("Cancel");

        $(this.cancelDrawingButton).click(function (evt) {
            evt.preventDefault();
            showDrawingControls.call(self, false, annotationType);
            self.finishDrawing(false, annotationType);
        });


        // do not add the toolbar while drawing a compound polygon
        if (!(annotationType === PMA.UI.Types.Annotation.CompoundFreehand && self.drawing)) {
            this.viewport.element.appendChild(this.drawingControlsContainer);
        }

        if (annotationType === PMA.UI.Types.Annotation.MultiPoint) {
            this.removeLastButton = document.createElement("button");
            this.removeLastButton.innerHTML = _("Remove last point");
            this.drawingControlsContainer.appendChild(this.removeLastButton);

            $(this.removeLastButton).click(function (evt) {
                evt.preventDefault();
                if (self.draw) {
                    self.draw.removeLastPoint();
                }
            });

            this.endDrawingButton = document.createElement("button");
            this.endDrawingButton.innerHTML = _("Finish");

            $(this.endDrawingButton).click(function (evt) {
                evt.preventDefault();
                showDrawingControls.call(self, false, PMA.UI.Types.Annotation.MultiPoint);
                self.finishDrawing(true);
            });

            this.drawingControlsContainer.appendChild(this.endDrawingButton);

            this.counterButton = document.createElement("button");
            this.counterButton.innerHTML = "0";

            if (feature) {
                if (feature.getGeometry() && feature.getGeometry().getCoordinates()){
                    this.counterButton.innerHTML = feature.getGeometry().getCoordinates().length;
                }
                
                if (this.changeFeatureKey) {
                    ol.Observable.unByKey(this.changeFeatureKey);
                }

                this.changeFeatureKey = feature.on("change", function (e) {
                    if (self.counterButton) {
                        self.counterButton.innerHTML = feature.getGeometry().getCoordinates().length - 1;
                    }
                });
            }

            this.drawingControlsContainer.insertBefore(this.counterButton, this.removeLastButton);
        }
        else if (annotationType === PMA.UI.Types.Annotation.CompoundFreehand) {
            if (!self.drawing) {
                this.continueDrawingButton = document.createElement("button");
                this.continueDrawingButton.innerHTML = _("Draw");
                this.drawingControlsContainer.appendChild(this.continueDrawingButton);

                $(this.continueDrawingButton).click(function (evt) {
                    evt.preventDefault();
                    self.startDrawing({
                        type: PMA.UI.Types.Annotation.CompoundFreehand,
                        color: self.lastAnnotationStyle.color,
                        fillColor: self.lastAnnotationStyle.fillColor,
                        penWidth: self.lastAnnotationStyle.penSize,
                        iconRelativePath: self.lastAnnotationStyle.iconPath
                    });

                    if (self.continueDrawingButton) {
                        self.continueDrawingButton.style.display = "none";
                    }
                });
            }

            this.removeLastButton = document.createElement("button");
            this.removeLastButton.innerHTML = _("Remove last");


            $(this.removeLastButton).click(function (evt) {
                evt.preventDefault();
                if (self.compoundFreehandList.length > 0) {
                    var ft = self.compoundFreehandList.pop();
                    self.deleteAnnotation(ft.getId());
                }
            });

            this.endDrawingButton = document.createElement("button");
            this.endDrawingButton.innerHTML = _("Finish");

            $(this.endDrawingButton).click(function (evt) {
                evt.preventDefault();
                showDrawingControls.call(self, false, PMA.UI.Types.Annotation.CompoundFreehand);
                self.finishDrawing(true);

                if (self.compoundFreehandList.length > 0) {
                    self.mergeSelection(self.compoundFreehandList);
                    self.compoundFreehandList = [];
                }
            });

            this.drawingControlsContainer.appendChild(this.endDrawingButton);
            this.drawingControlsContainer.appendChild(this.removeLastButton);
        }

        this.drawingControlsContainer.appendChild(this.cancelDrawingButton);
    }

    function createMeasureTooltip() {
        var el = document.createElement('div');
        el.className = 'pma-ui-viewport-tooltip pma-ui-viewport-tooltip-measure';
        var measureTooltip = new ol.Overlay({
            element: el,
            offset: [0, -15],
            positioning: 'bottom-center'
        });

        this.viewport.map.addOverlay(measureTooltip);
        measureTooltip.element = el;

        var self = this;
        // addEvent(el, 'click', function () {
        //     self.viewport.map.removeOverlay(measureTooltip);
        //     self.viewport.annotationsLayer.getSource().removeFeature(measureTooltip.sketch);

        //     el.parentNode.removeChild(el);
        //     el = null;
        // });

        return measureTooltip;
    }

    /**
    * Instructs the viewport to enter annotation drawing mode
    * @param {PMA.UI.Components.Annotations~startDrawingOptions} options - Options to start drawing
    */
    function addInteraction(options) {
        if (options.type !== PMA.UI.Types.Annotation.CompoundFreehand) {
            this.compoundFreehandList = [];
        }

        showDrawingControls.call(this, false, options.type);

        var type = options.type;
        var maxPoints, minPoints, geometryFunction, finishCondition, condition = ol.events.condition.noModifierKeys,
            freehandCondition = ol.events.condition.shiftKeyOnly;

        this.stopDrawingOnMouseUp = false;

        this.lastAnnotationStyle = {
            color: options.color,
            fillColor: options.fillColor,
            penSize: options.penWidth,
            iconPath: options.iconRelativePath
        };

        if (this.selectionAdded === true) {
            this.selectionAdded = false;
            this.hoverInteraction.getFeatures().clear();
            this.viewport.map.removeInteraction(this.hoverInteraction);

            this.selectInteraction.getFeatures().clear();
            this.viewport.map.removeInteraction(this.selectInteraction);
        }

        var styleGeometryFunction;
        switch (type) {
            case PMA.UI.Types.Annotation.Rectangle:
                geometryFunction = boxGeometryFunction.bind(options);
                if (options.size) {
                    maxPoints = 1;
                    type = "Point";
                    styleGeometryFunction = styleBoxGeometryFunction.bind(options);
                }
                else {
                    maxPoints = 2;
                    type = "LineString";
                }

                break;
            case PMA.UI.Types.Annotation.Arrow:
                geometryFunction = arrowGeometryFunction;
                maxPoints = 2;
                type = "LineString";
                break;
            case PMA.UI.Types.Annotation.Line:
                maxPoints = 2;
                type = "LineString";
                break;
            case PMA.UI.Types.Annotation.Icon:
                type = "Point";
                break;
            case PMA.UI.Types.Annotation.MultiPoint:
                geometryFunction = multiPointGeometryFunction;
                type = "LineString";
                finishCondition = ol.events.condition.never;
                minPoints = 1;
                break;
            case PMA.UI.Types.Annotation.Freehand:
            case PMA.UI.Types.Annotation.CompoundFreehand:
                // disable dragging when drawing freehand
                toggleDragPanInteraction(this.viewport.map, false);
                type = "LineString";
                freehandCondition = ol.events.condition.noModifierKeys;
                condition = ol.events.condition.singleClick;
                this.stopDrawingOnMouseUp = true;
                break;
            case PMA.UI.Types.Annotation.ClosedFreehand:
                // disable dragging when drawing freehand
                toggleDragPanInteraction(this.viewport.map, false);
                type = "Polygon";
                freehandCondition = ol.events.condition.noModifierKeys;
                condition = ol.events.condition.singleClick;
                this.stopDrawingOnMouseUp = true;
                break;
            case PMA.UI.Types.Annotation.Ellipse:
                geometryFunction = ellipseGeometryFunction.bind(options);
                maxPoints = 2;
                type = "LineString";
                break;
            case PMA.UI.Types.Annotation.Circle:
                if (options.size) {
                    maxPoints = 1;
                    type = "Point";
                    styleGeometryFunction = styleCircleGeometryFunction.bind(options);
                }

                geometryFunction = circleGeometryFunction.bind(options);
                break;
        }

        var tmpStl = getAnnotationStyle.call(this, options.color, options.penWidth, options.fillColor, options.iconRelativePath, null, styleGeometryFunction);

        this.draw = new ol.interaction.Draw({
            source: this.viewport.annotationsLayer.getSource(),
            type: type,
            geometryFunction: geometryFunction,
            maxPoints: maxPoints,
            minPoints: minPoints,
            style: tmpStl,
            condition: condition,
            finishCondition: finishCondition,
            freehandCondition: freehandCondition
        });

        var updateExisting = false;
        if (options.feature) {
            updateExisting = true;
            this.draw.extend(options.feature);
            this.drawing = true;
            options.feature.drawingType = options.type;
            options.feature.color = options.feature.metaData.Color;
            options.feature.penSize = options.feature.metaData.LineThickness;
            options.feature.fillColor = options.feature.metaData.FillColor;

            showDrawingControls.call(this, true, options.type, options.feature);
            options.feature = null;
        }

        this.viewport.map.addInteraction(this.draw);

        if (this.viewport.getAnnotationLabelsVisible() === true) {
            var tooltip = createMeasureTooltip.call(this);
        }

        var self = this;
        var featureChangeKey = null;    // used to unhook the event later

        this.draw.on('drawstart', function (evt) {
            // only called when a new annotation is created, not when extending an existing one
            self.drawing = true;
            self.shouldRejectDrawing = false;
            showDrawingControls.call(self, true, options.type, evt.feature);

            evt.feature.setId((Math.random() * 10000) | 0);

            /** @type {ol.Coordinate|undefined} */
            var tooltipCoord = evt.coordinate;

            featureChangeKey = evt.feature.on("change", function (e) {
                var evtArgs = {
                    temporary: evt.feature.temporary,
                    feature: evt.feature
                };

                self.fireEvent(PMA.UI.Components.Events.AnnotationDrawing, evtArgs);
            });

            if (tooltip) {
                evt.feature.getGeometry().on('change', function (e) {
                    var geom = e.target;
                    var output;
                    switch (options.type) {
                        case PMA.UI.Types.Annotation.Rectangle:
                        case PMA.UI.Types.Annotation.Ellipse:
                        case PMA.UI.Types.Annotation.Polygon:
                        case PMA.UI.Types.Annotation.ClosedFreehand:
                            output = self.viewport.formatArea(self.viewport.calculateArea(geom));
                            tooltipCoord = geom.getInteriorPoint().getCoordinates();
                            break;
                        case PMA.UI.Types.Annotation.Line:
                        case PMA.UI.Types.Annotation.LineString:
                        case PMA.UI.Types.Annotation.Freehand:
                        case PMA.UI.Types.Annotation.Circle:
                            output = self.viewport.formatLength(self.viewport.calculateLength(geom));
                            tooltipCoord = geom.getLastCoordinate();
                            break;
                        default:
                            break;
                    }

                    tooltip.element.innerHTML = output;
                    tooltip.setPosition(tooltipCoord);
                });
            }

            evt.feature.notes = options.notes;
            evt.feature.drawingType = options.type;
            evt.feature.temporary = true;
            evt.feature.penSize = options.penWidth;
            evt.feature.color = options.color;
            evt.feature.fillColor = options.fillColor;
            evt.feature.icon = options.iconRelativePath;
        });

        this.draw.on('drawend', function (evt) {
            self.drawing = false;
            showDrawingControls.call(self, evt.feature.drawingType === PMA.UI.Types.Annotation.CompoundFreehand, evt.feature.drawingType, evt.feature);

            if (evt.feature.drawingType === PMA.UI.Types.Annotation.CompoundFreehand) {
                if (self.drawingControlsContainer) {
                    var c = evt.feature.getGeometry().getLastCoordinate();
                    var pixelCoord = self.viewport.map.getPixelFromCoordinate(c);

                    self.drawingControlsContainer.style.left = (pixelCoord[0] + 25) + "px";
                    self.drawingControlsContainer.style.top = (pixelCoord[1] - 50) + "px";
                }
            }

            if (!self.getEnabled()) {
                evt.preventDefault();
                return false;
            }

            if (tooltip) {
                self.viewport.map.removeOverlay(tooltip);
                // tooltip = null;
            }

            if (self.shouldRejectDrawing) {
                removeInteraction.call(self);
            }
            else {
                // remove the interaction with a delay, otherwise the last double click
                // will result in a zoom in
                setTimeout(function () {
                    removeInteraction.call(self);
                }, 100);
            }

            if (evt.feature.drawingType === PMA.UI.Types.Annotation.CompoundFreehand) {
                self.compoundFreehandList.push(evt.feature);
            }

            ol.Observable.unByKey(featureChangeKey);

            addFeature.call(self, evt.feature, updateExisting);
        });
    }

    function addFeature(feature, updateExisting) {
        var self = this;
        var stl = getAnnotationStyle.call(self, feature.color, feature.penSize, feature.fillColor, feature.icon, feature);
        if (feature.getId() === undefined) {
            feature.setId((Math.random() * 10000) | 0);
        }

        feature.setStyle(self.viewport.getAnnotationStyle(stl, feature));

        feature.originalStyle = stl;
        feature.temporary = false;

        // disable the toolbar once finished drawing
        if (self.element !== null) {
            $(self.element).find("li.draw a").removeClass("active");
            $(self.element).find(".color-picker").hide();
        }

        var evtArgs = {
            temporary: feature.temporary,
            feature: feature
        };

        var extent = self.viewport.map.getView().getProjection().getExtent();
        var extentObj = { extent: extent, flip: self.viewport.options.flip };

        var geomClone = feature.getGeometry().clone();
        geomClone.applyTransform(annotationTransform.bind(extentObj));

        var geometryWkt = "";

        if (feature.drawingType === "Freehand" || feature.drawingType === "ClosedFreehand") {
            geometryWkt = self.format.writeGeometry(geomClone.simplify(2));
        }
        else if (feature.drawingType === "Circle") {
            geometryWkt = self.format.writeGeometry(ol.geom.Polygon.fromCircle(geomClone));
        }
        else {
            geometryWkt = self.format.writeGeometry(geomClone);
        }

        var dimensions = 2;
        switch (feature.drawingType) {
            case PMA.UI.Types.Annotation.Arrow:
            case PMA.UI.Types.Annotation.Icon:
            case PMA.UI.Types.Annotation.Point:
            case PMA.UI.Types.Annotation.MultiPoint:
                dimensions = 0;
                break;
            case PMA.UI.Types.Annotation.Line:
            case PMA.UI.Types.Annotation.Freehand:
            case PMA.UI.Types.Annotation.CompoundFreehand:
            case PMA.UI.Types.Annotation.LineString:
                dimensions = 1;
                break;
            case PMA.UI.Types.Annotation.Rectangle:
            case PMA.UI.Types.Annotation.Ellipse:
            case PMA.UI.Types.Annotation.Polygon:
            case PMA.UI.Types.Annotation.ClosedFreehand:
            case PMA.UI.Types.Annotation.Circle:
                dimensions = 2;
                break;
        }

        if (!updateExisting) {
            feature.metaData = {
                AnnotationID: null,
                Classification: _("Generic"),
                Color: feature.icon ? feature.icon : feature.originalStyle.getStroke().getColor(),
                Image: self.viewport.imageInfo.Filename,
                LayerID: 1,
                Notes: feature.notes ? feature.notes : "",
                UpdateInfo: "",
                State: PMA.UI.Types.AnnotationState.Added,
                FillColor: feature.originalStyle.getFill().getColor(),
                Dimensions: dimensions,
                LineThickness: feature.penSize,
            };
        }

        var tmpGeom = feature.getGeometry();
        if (dimensions > 0) {
            var length = self.viewport.calculateLength(tmpGeom);
            feature.metaData.Length = length;
            feature.metaData.FormattedLength = self.viewport.formatLength(length);

            if (dimensions > 1) {
                var area = self.viewport.calculateArea(tmpGeom);
                feature.metaData.Area = area;
                feature.metaData.FormattedArea = self.viewport.formatArea(area);
            }
        }

        feature.metaData.DrawingType = feature.drawingType;
        if (feature.metaData.DrawingType == PMA.UI.Types.Annotation.MultiPoint){
            feature.metaData.PointCount = tmpGeom.getCoordinates ? tmpGeom.getCoordinates().length : 0;
        }

        feature.metaData.Geometry = geometryWkt;
        feature.metaData.UpdatedOn = new Date();

        if (updateExisting && feature.metaData.State !== PMA.UI.Types.AnnotationState.Added) {
            feature.metaData.State = PMA.UI.Types.AnnotationState.Modified;
        }

        // fire with a time out to give the chance to OL to add the feature in the list
        setTimeout(function () {
            if (geometryWkt.indexOf("MULTIPOINT EMPTY") !== -1) {
                self.deleteAnnotation(feature.getId());
            }
            else {
                feature.setStyle(self.viewport.getAnnotationStyle(stl, feature));

                if (updateExisting) {
                    self.fireEvent(PMA.UI.Components.Events.AnnotationModified, evtArgs);
                }
                else {
                    if (!self.shouldRejectDrawing && feature && feature.drawingType !== PMA.UI.Types.Annotation.CompoundFreehand) {
                        self.fireEvent(PMA.UI.Components.Events.AnnotationAdded, evtArgs);
                    }
                }
            }
        }, 10);
    }

    function removeInteraction() {
        if (this.draw) {
            toggleDragPanInteraction(this.viewport.map, true);
            this.viewport.map.removeInteraction(this.draw);
            this.draw = null;
        }

        if (this.selectionAdded !== true) {
            this.selectionAdded = true;
            this.selectInteraction.getFeatures().clear();
            this.viewport.map.addInteraction(this.selectInteraction);

            this.hoverInteraction.getFeatures().clear();
            this.viewport.map.addInteraction(this.hoverInteraction);
        }
    }

    function updateCurrentInteraction() {
        if (!this.draw) {
            return;
        }

        if (this.element !== null) {
            var el = $(this.element).find("li.draw a.active");
            if (el.length === 1) {
                el.removeClass("active");
                el.click();
            }
        }
    }

    function drawClick(element) {
        if (!this.getEnabled()) {
            return;
        }

        this.finishDrawing(true);

        if (element.hasClass("active")) {
            element.removeClass("active");
            return;
        }

        var tp = element.data("type");
        var notes = "";
        if (tp === "Text") {
            tp = "Point";
            notes = prompt(_("Enter text"));
            if (!notes) {
                return;
            }
        }

        var selection = this.getSelection();
        this.startDrawing({
            type: tp,
            color: element.data("color"),
            penWidth: element.data("size"),
            iconRelativePath: element.data("icon"),
            feature: (selection && selection.length > 0 ? selection[0] : null),
            fillColor: PMA.UI.View.DefaultFillColor,
            notes: notes
        });

        element.addClass("active");

        if (this.jsColorPicker !== null) {
            this.jsColorPicker.fromString(element.data("color"));
        }
        else {
            $(this.element).find(".color-picker input[type='color']").val(element.data("color"));
        }

        $(this.element).find(".color-picker").show();
    }

    function createControls() {
        if (this.element === null) {
            return;
        }

        var html = "<ul class='pma-ui-annotations'>";
        if (supportsColorPicker()) {
            html += "<li class='option color-picker'><input type='color' value='#000000' /></li>";
        }
        else {
            html += "<li class='option color-picker'><input type='button' value=' ' /><input type='hidden' value='#000000' /></li>";
        }

        html += "<li class='option draw'><a data-type='Freehand' data-size='2' data-color='#008000' style='color: #008000' class='size-2' href='#' title='" + _("Freehand size 1") + "'><i class='fa fa-circle' aria-hidden='true'></i></a></li>";
        // html += "<li class='option draw'><a data-type='Freehand' data-size='4' data-color='#ff0000' style='color: #ff0000' class='size-4' href='#' title='" + _("Freehand size 2") + "'><i class='fa fa-circle' aria-hidden='true'></i></a></li>";
        // html += "<li class='option draw'><a data-type='Freehand' data-size='8' data-color='#0000ff' style='color: #0000ff' class='size-8' href='#' title='" + _("Freehand size 3") + "'><i class='fa fa-circle' aria-hidden='true'></i></a></li>";
        html += "<li class='option draw'><a data-type='ClosedFreehand' data-size='2' data-color='#008000' style='color: #008000' class='size-2' href='#' title='" + _("Closed Freehand size 1") + "'><i class='fa fa-pencil-square' aria-hidden='true'></i></a></li>";
        html += "<li class='option draw'><a data-type='CompoundFreehand' data-size='2' data-color='#008080' style='color: #008080' class='size-2' href='#' title='" + _("Compound Freehand size 2") + "'><i class='fa fa-chain' aria-hidden='true'></i></a></li>";
        // html += "<li class='option draw'><a data-type='Point' data-size='3' data-color='#f00ff0' style='color: #f00ff0' class='size-8' href='#' title='" + _("Point size 3") + "'><i class='fa fa-circle' aria-hidden='true'></i></a></li>";
        html += "<li class='option draw'><a data-type='Text' data-size='1' data-color='#000000' style='color: #000000' class='size-2' href='#' title='" + _("Text") + "'><i class='fa fa-font' aria-hidden='true'></i></a></li>";
        // html += "<li class='option draw'><a data-type='Icon' data-size='1' data-color='#ffffff' data-icon='wim.jpg' style='background: url(\"http://www.smartcode.gr/annotations/wim.jpg\") center center no-repeat; background-size: contain' class='size-8' href='#' title='" + _("Wim") + "'></a></li>";
        // html += "<li class='option draw'><a data-type='Icon' data-size='1' data-color='#ffffff' data-icon='yves.jpg' style='background: url(\"http://www.smartcode.gr/annotations/yves.jpg\") center center no-repeat; background-size: contain' class='size-8' href='#' title='" + _("Yves") + "'></a></li>";
        // html += "<li class='option draw'><a data-type='Rectangle' data-size='2' data-color='#000000' style='color: #000000' class='size-8' href='#' title='" + _("Rectangle") + "'><i class='fa fa-square-o' aria-hidden='true'></i></a></li>";
        // html += "<li class='option draw'><a data-type='Circle' data-size='2' data-color='#000000' style='color: #000000' class='size-8' href='#' title='" + _("Circle") + "'><i class='fa fa-circle-o' aria-hidden='true'></i></a></li>";
        // html += "<li class='option draw'><a data-type='Ellipse' data-size='2' data-color='#000000' style='color: #000000' class='size-8' href='#' title='" + _("Ellipse") + "'><i class='fa fa-circle' aria-hidden='true'></i></a></li>";
        html += "<li class='option draw'><a data-type='Arrow' data-size='2' data-color='#000000' style='color: #000000' class='size-2' href='#' title='" + _("Arrow") + "'><i class='fa fa-arrow-right' aria-hidden='true'></i></a></li>";
        html += "<li class='option draw'><a data-type='Line' data-size='1' data-color='#F00F00' style='color: #F00F00' class='size-2' href='#' title='" + _("Measure") + "'><i class='fa fa-minus' aria-hidden='true'></i></a></li>";
        // html += "<li class='option draw'><a data-type='LineString' data-size='2' data-color='#000000' style='color: #000000' class='size-2' href='#' title='" + _("Polyline") + "'>L</a></li>";
        // html += "<li class='option draw'><a data-type='Polygon' data-size='2' data-color='#000000' style='color: #000000' class='size-2' href='#' title='" + _("Polygon") + "'>P</a></li>";
        // html += "<li class='option draw'><a data-type='MultiPoint' data-size='2' data-color='#000000' style='color: #000000' class='size-2' href='#' title='" + _("MultiPoint") + "'>MP</a></li>";
        // html += "<li class='option delete'><a style='color: #000000' class='size-2' href='#' title='" + _("Delete") + "'><i class='fa fa-trash' aria-hidden='true'></i></a></li>";
        // html += "<li class='option save'><a style='color: #000000' class='size-2' href='#' title='" + _("Save") + "'><i class='fa fa-floppy-o' aria-hidden='true'></i></a></li>";



        html += "</ul>";

        var self = this;
        var el = $(this.element);

        el.html(html);

        if (!supportsColorPicker()) {
            self.jsColorPicker = new jscolor(el.find("li.color-picker input")[0], { valueElement: el.find("li.color-picker input[type=hidden]")[0], hash: true, closable: true, closeText: _("Close") });
        }

        el.find("li.color-picker input").change(function () {
            var newcol = $(this).val();

            el.find("li.draw a.active").data("color", newcol);
            el.find("li.draw a.active").css("color", newcol);
            updateCurrentInteraction.call(self);
        });

        el.find("li.option.delete a").click(function (evt) {
            evt.preventDefault();
            var d = self.getSelection();
            if (d && d.length > 0) {
                self.deleteAnnotation(d[0].getId());
            }
        });

        el.find("li.option.save a").click(function (evt) {
            evt.preventDefault();
            self.saveAnnotations();
        });

        el.find("li.draw a").click(function (ev) {
            ev.preventDefault();
            var e = $(this);
            drawClick.call(self, e);
        });
    }

    function createSelectInteraction() {
        this.selectInteraction = new ol.interaction.Select({
            condition: ol.events.condition.click,
            layers: [this.viewport.annotationsLayer]
        });

        this.hoverInteraction = new ol.interaction.Select({
            condition: ol.events.condition.pointerMove,
            layers: [this.viewport.annotationsLayer]
        });

        var self = this;

        var selection = this.selectInteraction.getFeatures();
        var hover = this.hoverInteraction.getFeatures();

        selection.on('add', function (evt) {
            if (!evt.element.originalStyle) {
                return;
            }

            var sqrt2 = Math.sqrt(2);
            // when an annotation is clicked, select it
            hover.clear();

            var radius = 5;
            var img = evt.element.originalStyle.getImage();
            if (img && img.getSize) {
                var sz = img.getSize();
                var side = sz[0] > sz[1] ? sz[0] : sz[1];
                radius = (sqrt2 * side) / 2;
            }

            var selectStyle = new ol.style.Style({
                stroke: new ol.style.Stroke({
                    color: "#ff0000",
                    width: evt.element.originalStyle.getStroke().getWidth() + 3
                }),
                image: new ol.style.RegularShape({
                    fill: new ol.style.Fill({ color: 'rgba(255, 255, 255, 0.2)' }),
                    stroke: new ol.style.Stroke({ color: '#ff0000', width: 4 }),
                    points: 4,
                    radius: radius,
                    angle: Math.PI / 4
                })
            });

            evt.element.setStyle([selectStyle, evt.element.originalStyle]);
            self.fireEvent(PMA.UI.Components.Events.AnnotationsSelectionChanged, selection.getArray());
        });

        selection.on('remove', function (evt) {
            if (!evt.element.originalStyle) {
                return;
            }

            evt.element.setStyle(evt.element.originalStyle);
            self.fireEvent(PMA.UI.Components.Events.AnnotationsSelectionChanged, selection.getArray());
        });

        hover.on('add', function (evt) {
            var sqrt2 = Math.sqrt(2);
            // when an annotation is about to get highlighted, check if it's already selected
            // before highlighting it
            if (selection.getArray().indexOf(evt.element) === -1) {
                var radius = 5;
                if (!evt.element.originalStyle) {
                    return;
                }

                var img = evt.element.originalStyle.getImage();
                if (img && img.getSize) {
                    var sz = img.getSize();
                    var side = sz[0] > sz[1] ? sz[0] : sz[1];
                    radius = (sqrt2 * side) / 2;
                }

                var hoverStyle = new ol.style.Style({
                    stroke: new ol.style.Stroke({
                        color: "#ffff00",
                        width: evt.element.originalStyle.getStroke().getWidth() + 3
                    }),
                    image: new ol.style.RegularShape({
                        fill: new ol.style.Fill({ color: 'rgba(255, 255, 255, 0.2)' }),
                        stroke: new ol.style.Stroke({ color: '#ffff00', width: 4 }),
                        points: 4,
                        radius: radius,
                        angle: Math.PI / 4
                    })
                });

                evt.element.setStyle([hoverStyle, evt.element.originalStyle]);
            }
        });

        hover.on('remove', function (evt) {
            // when an annotation is about to get DE-highlighted, check if it's already selected
            // before DE-highlighting it
            if (!evt.element.originalStyle) {
                return;
            }

            if (selection.getArray().indexOf(evt.element) === -1) {
                evt.element.setStyle(evt.element.originalStyle);
            }
        });

        // add the interactions initially
        this.selectionAdded = true;
        this.selectInteraction.getFeatures().clear();
        this.viewport.map.addInteraction(this.selectInteraction);

        this.hoverInteraction.getFeatures().clear();
        this.viewport.map.addInteraction(this.hoverInteraction);
    }

    function saveAnnotationsNew(added, edited) {
        var i, a;
        var toAdd = [];
        if (added && added.length) {
            for (i = 0; i < added.length; i++) {
                a = added[i];
                if (!a.metaData.Notes) {
                    a.metaData.Notes = _(' ');
                }

                if (!a.metaData.Classification) {
                    a.metaData.Classification = _('no classification');
                }

                if (a.metaData.AnnotationID === null) {
                    a.metaData.AnnotationID = 0;
                }

                toAdd.push(a.metaData);
            }
        }

        var toEdit = edited.map(function (x) { return x.metaData; });

        var toDelete = [];
        if (this.deletedAnnotations && this.deletedAnnotations.length > 0) {
            for (i = 0; i < this.deletedAnnotations.length; i++) {
                // if the annotation ID is null, it means that the annotation was never saved in the first place
                if (this.deletedAnnotations[i].metaData.AnnotationID !== null) {
                    toDelete.push(this.deletedAnnotations[i].metaData);
                }
            }
        }

        var self = this;
        this.context.saveAnnotations(
            this.serverUrl,
            this.path,
            toAdd,
            toEdit,
            toDelete,
            function (sessionId, annotationIds) {
                if (added && added.length) {
                    if (annotationIds == null || annotationIds.length != added.length) {
                        console.error("Annotations saved but cannot update annotation ids");
                        return;
                    }

                    for (i = 0; i < annotationIds.length; i++) {
                        added[i].metaData.AnnotationID = annotationIds[i];
                        added[i].setId(annotationIds[i]);
                        added[i].metaData.State = PMA.UI.Types.AnnotationState.Pristine;
                    }
                }

                if (edited && edited.length) {
                    for (i = 0; i < edited.length; i++) {
                        edited[i].metaData.State = PMA.UI.Types.AnnotationState.Pristine;
                    }
                }

                self.deletedAnnotations = [];

                self.fireEvent(PMA.UI.Components.Events.AnnotationsSaved, { success: true });
            },
            function () {
                self.fireEvent(PMA.UI.Components.Events.AnnotationsSaved, { success: false });
                console.error("Saving annotations failed");
                console.log(arguments);
            });
    }

    // saves all newly added annotations and then calls saveEditedAnnotations 
    function saveAddedAnnotations(added, edited) {
        if (!added || added.length === 0) {
            saveEditedAnnotations.call(this, edited);
            return;
        }

        var a = added.pop();
        var self = this;

        if (!a.metaData.Notes) {
            a.metaData.Notes = _(' ');
        }

        if (!a.metaData.Classification) {
            a.metaData.Classification = _('no classification');
        }

        self.context.addAnnotation(
            self.serverUrl,
            self.path,
            a.metaData.Classification,
            a.metaData.LayerID,
            a.metaData.Notes,
            a.metaData.Geometry,
            a.metaData.Color,
            function (sessionId, annotationId) {
                a.metaData.AnnotationID = annotationId;
                a.setId(annotationId);
                a.metaData.State = PMA.UI.Types.AnnotationState.Pristine;

                saveAddedAnnotations.call(self, added, edited);
            },
            function () {
                self.fireEvent(PMA.UI.Components.Events.AnnotationsSaved, { success: false });
                console.error("Saving annotation (add) failed");
                console.log(arguments);
            });
    }

    // saves all modified annotations and then calls saveDeletedAnnotations 
    function saveEditedAnnotations(edited) {
        if (!edited || edited.length === 0) {
            saveDeletedAnnotations.call(this);
            return;
        }

        var a = edited.pop();
        var self = this;

        self.context.updateAnnotation(
            self.serverUrl,
            self.path,
            a.metaData.LayerID,
            a.metaData.AnnotationID,
            a.metaData.Notes,
            a.metaData.Geometry,
            a.metaData.Color,
            function () {
                a.metaData.State = PMA.UI.Types.AnnotationState.Pristine;
                saveEditedAnnotations.call(self, edited);
            },
            function () {
                self.fireEvent(PMA.UI.Components.Events.AnnotationsSaved, { success: false });
                console.error("Saving annotation (edit) failed");
                console.log(arguments);
            });
    }

    // saves all deletd annotations and then AnnotationsSaved event
    function saveDeletedAnnotations() {
        if (!this.deletedAnnotations || this.deletedAnnotations.length === 0) {
            this.fireEvent(PMA.UI.Components.Events.AnnotationsSaved, { success: true });
            return;
        }

        var entity = this.deletedAnnotations.pop();
        var self = this;

        if (entity.metaData.AnnotationID === null) {
            // if the annotation ID is null, it means that the annotation was never saved in the first place
            saveDeletedAnnotations.call(self);
            return;
        }

        self.context.deleteAnnotation(
            self.serverUrl,
            self.path,
            entity.metaData.LayerID,
            entity.metaData.AnnotationID,
            function () {
                saveDeletedAnnotations.call(self);
            },
            function () {
                self.fireEvent(PMA.UI.Components.Events.AnnotationsSaved, { success: false });
                console.error("Saving annotation (delete) failed");
                console.log(arguments);
            });
    }

    function checkPMACore2(cb) {
        var self = this;
        if (self.isPMACore2 !== null) {
            if (typeof cb === "function") {
                cb.call(self, self.isPMACore2);
            }

            return;
        }

        this.context.getVersionInfo(self.serverUrl, function (version) {
            if (version && version.substring(0, "1.".length) === "1.") {
                self.isPMACore2 = false;
            }
            else {
                self.isPMACore2 = true;
            }

            if (typeof cb === "function") {
                cb.call(self, self.isPMACore2);
            }
        }, function () {
            console.error("Cannot reach server");
        });
    }

    PMA.UI.Components.Annotations = (function () {
        /**
         * Provides programmatic interaction with a {@link PMA.UI.View.Viewport} instance to manipulate annotations
         * @param  {Object} options
         * @param  {PMA.UI.Components.Context} options.context
         * @param  {PMA.UI.View.Viewport} options.viewport
         * @param  {string} options.serverUrl - PMA.core server URL
         * @param  {string} options.path - Path of an slide to save annotations for
         * @param  {boolean} options.enabled
         * @constructor
         * @memberof PMA.UI.Components
         * @tutorial 05-annotations
         * @fires PMA.UI.Components.Events#AnnotationAdded
         * @fires PMA.UI.Components.Events#AnnotationDrawing
         * @fires PMA.UI.Components.Events#AnnotationDeleted
         * @fires PMA.UI.Components.Events#AnnotationModified
         * @fires PMA.UI.Components.Events#AnnotationsSaved
         * @fires PMA.UI.Components.Events#AnnotationsSelectionChanged
         */
        function Annotations(options) {
            // options: context, element, viewport, serverUrl, path, enabled
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            // if the element is null, no controls are created
            if (options.element instanceof HTMLElement) {
                this.element = options.element;
            }
            else if (typeof options.element == "string") {
                var el = document.querySelector(options.element);
                if (!el) {
                    console.error("Invalid selector for element");
                }
                else {
                    this.element = el;
                }
            }
            else {
                this.element = null;
            }

            if (!(options.viewport instanceof PMA.UI.View.Viewport) || !options.viewport.map) {
                console.error("Invalid viewport instance");
                return;
            }

            if (!options.viewport.annotationsLayer) {
                console.error("Annotations must be enabled in the viewport for this to work");
                return;
            }

            this.serverUrl = options.serverUrl;
            this.path = options.path;
            this.context = options.context;
            this.viewport = options.viewport;
            this.drawing = false;
            this.selectionAdded = false;
            this.stopDrawingOnMouseUp = false;
            this.format = new ol.format.WKT();
            this.jsColorPicker = null;
            this.isPMACore2 = null;

            checkPMACore2.call(this);

            this.listeners = {};
            this.listeners[PMA.UI.Components.Events.AnnotationAdded] = [];
            this.listeners[PMA.UI.Components.Events.AnnotationDrawing] = [];
            this.listeners[PMA.UI.Components.Events.AnnotationDeleted] = [];
            this.listeners[PMA.UI.Components.Events.AnnotationModified] = [];
            this.listeners[PMA.UI.Components.Events.AnnotationsSaved] = [];
            this.listeners[PMA.UI.Components.Events.AnnotationsSelectionChanged] = [];

            createSelectInteraction.call(this);

            createControls.call(this);

            var self = this;
            this.viewport.map.on('pointerup', function (evt) {
                if (self.drawing && self.stopDrawingOnMouseUp === true) {
                    self.finishDrawing(true);
                }
            });

            this.setEnabled(options.enabled === true);
            this.compoundFreehandList = [];
            this.lastAnnotationStyle = null;

            // used to control whether or not an annotation should actually be added after drawing is finished
            // e.g. if finishDrawing(false) is invoked, the annotation should not be added and no events should fire
            this.shouldRejectDrawing = false;
        }

        /**
         * Annotation entity
         * @typedef {Object} PMA.UI.Components.Annotations~annotationEntity
         * @property {Number} LayerID - The layer id
         * @property {string} Geometry - The annotation geometry in wkt format
         * @property {string} [Notes] - Optional notes for the annotation
         * @property {string} [Classification] - Optional classification string (Necrosis, tumor etc)
         * @property {string} [Color] - Optional stroke color (e.g. #ff0000)
         * @property {string} [UpdateInfo] - Optional update info
         * @property {string} [FillColor] - Optional fill color (e.g. #ff0000)
         * @property {Number} [Dimensions] - Optional dimensionality of the annotation
         * @property {Number} [LineThickness=1] - Optional stroke line thickness
         */

        /**
         * Replaces the currently loaded annotations with the provided ones (without saving them to the server)
         * @param  {PMA.UI.Components.Annotations~annotationEntity[]} annotations - Array of annotation objects
         */
        Annotations.prototype.replaceAnnotations = function (annotations) {
            if (!annotations || !(annotations instanceof Array)) {
                annotations = [];
            }

            var i = 0;
            for (i = 0; i < annotations.length; i++) {
                annotations[i].AnnotationID = null;
                annotations[i].Image = this.viewport.image;
            }

            this.finishDrawing(true);

            // Clear all existing annotations and mark them as deleted
            var current = this.viewport.getAnnotations();
            this.deletedAnnotations = this.deletedAnnotations || [];
            for (i = 0; i < current.length; i++) {
                switch (current[i].metaData.State) {
                    case PMA.UI.Types.AnnotationState.Pristine:
                    case PMA.UI.Types.AnnotationState.Modified:
                        this.deletedAnnotations.push(current[i]);
                        break;
                }
            }

            // load the provided annotations
            var annotationsSource = this.viewport.annotationsLayer.getSource();
            annotationsSource.clear();

            var features = this.viewport.initializeFeatures(annotations, this.viewport.mainLayer.getSource().getProjection());
            for (i = 0; i < features.length; i++) {
                features[i].metaData.State = PMA.UI.Types.AnnotationState.Added;
            }

            annotationsSource.addFeatures(features);

            this.viewport.redraw();
        };

        /**
         * Adds an annotation to the current ones (without saving them to the server)
         * @param  {PMA.UI.Components.Annotations~annotationEntity} annotation - An annotation object
         */
        Annotations.prototype.addAnnotation = function (annotation) {
            annotation.AnnotationID = null;
            annotation.Image = this.viewport.image;

            this.finishDrawing(true);

            // load the provided annotations
            var annotationsSource = this.viewport.annotationsLayer.getSource();

            var features = this.viewport.initializeFeatures([annotation], this.viewport.mainLayer.getSource().getProjection());
            for (i = 0; i < features.length; i++) {
                features[i].metaData.State = PMA.UI.Types.AnnotationState.Added;
            }

            annotationsSource.addFeatures(features);

            var evtArgs = {
                temporary: false,
                feature: features && features.length ? features[0] : null
            };

            this.fireEvent(PMA.UI.Components.Events.AnnotationAdded, evtArgs);
        };

        Annotations.prototype.getActive = function () {
            if (this.element === null) {
                return false;
            }

            return $(this.element).find("li.draw a.active").length > 0;
        };

        /**
         * Gets the state of the annotation component
         * @returns {boolean}
         */
        Annotations.prototype.getEnabled = function () {
            return this.enabled;
        };

        /**
         * Enables or disables annotation drawing
         * @param  {boolean} enabled
         */
        Annotations.prototype.setEnabled = function (enabled) {
            this.enabled = enabled === true;

            if (!this.getEnabled()) {
                this.finishDrawing(false);

                if (this.element !== null) {
                    $(this.element).find("li.draw a").removeClass("active");
                    $(this.element).find("li.draw a").addClass("disabled");
                    $(this.element).find(".color-picker").hide();
                }
            }
            else {
                if (this.element !== null) {
                    $(this.element).find("li.draw a").removeClass("disabled");
                }
            }
        };

        /**
        * Options parameter required to start drawing
        * @typedef {Object} PMA.UI.Components.Annotations~startDrawingOptions
        * @property {PMA.UI.Types.Annotation} type - The type of the annotation to start drawing
        * @property {string} color - Annotation outline color as HTML hex string
        * @property {string} fillColor - Annotation fill color as HTML hex string
        * @property {Number} penWidth - The line thickness
        * @property {string} notes - Text to add to the annotation
        * @property {string} [iconRelativePath] - Relative path to an image that will be used when drawing a point. The base URL is defined by the imageBaseUrl property of the {@link PMA.UI.View.Viewport~annotationOptions|annotations} initialization option supplied in the {@link PMA.UI.View.Viewport} constructor
        * @property {ol.Feature} [feature] - An existing {@link http://openlayers.org/en/master/apidoc/ol.Feature.html | feature} to edit. If this argument has a value, the viewport goes into edit mode, instead of drawing a new annotation
        * @property {Number[]} [size] - An optional array of [width, height] in microns. Applies only to circles and rectangles. When drawing a circle only width is taken into account and it's the diameter of the circle.
        */

        /**
        * Instructs the viewport to enter annotation drawing mode
        * @param {PMA.UI.Components.Annotations~startDrawingOptions} options - Options to start drawing
        */
        Annotations.prototype.startDrawing = function (options) {
            if (typeof options === "string") {
                // function is called with old arguments without options object
                // to keep backwards compatibility wrap the arguments to the new options object
                options = {
                    type: arguments[0],
                    color: arguments[1],
                    penWidth: arguments[2],
                    iconRelativePath: arguments[3],
                    feature: arguments[4],
                    fillColor: PMA.UI.View.DefaultFillColor, // fill color was missing in old parameter list
                    notes: "" // notes was missing in the old parameter list
                };
            }

            if (!this.getEnabled()) {
                return;
            }

            if (this.drawing) {
                console.error("Drawing already in progress. Finish drawing before starting a new one.");
                return;
            }

            if (!options.fillColor) {
                options.fillColor = PMA.UI.View.DefaultFillColor;
            }

            if (options.size) {
                if (!options.size.length || options.size.length != 2) {
                    options.size = undefined;
                }
                else if (options.size[0] <= 0 || options.size[1] <= 0) {
                    options.size = undefined;
                }
                else if (!this.viewport.imageInfo || !this.viewport.imageInfo.MicrometresPerPixelX || !this.viewport.imageInfo.MicrometresPerPixelY) {
                    options.size = undefined;
                }
                else {
                    options.size = [
                        options.size[0] / this.viewport.imageInfo.MicrometresPerPixelX,
                        options.size[1] / this.viewport.imageInfo.MicrometresPerPixelY];
                }
            }

            this.finishDrawing(false);
            addInteraction.call(this, options);
        };

        /**
         * Exits drawing mode
         * @param  {boolean} accept - True to accept the annotation that was currently being drawn
         * @param {PMA.UI.Types.Annotation} annotationType - The annotation type that was drawing
         */
        Annotations.prototype.finishDrawing = function (accept, annotationType) {
            this.shouldRejectDrawing = !accept;
            var mustRemove = accept === false;
            if (this.drawing) {
                if (this.draw != null) {
                    this.draw.finishDrawing();
                }
            }
            else {
                mustRemove = false;
            }

            removeInteraction.call(this);

            if (this.element !== null) {
                $(this.element).find(".color-picker").hide();
                $(this.element).find("li.draw a").removeClass("active");
            }

            if (mustRemove || annotationType == PMA.UI.Types.Annotation.CompoundFreehand) {
                var source = this.viewport.annotationsLayer.getSource();
                var features = source.getFeatures();

                if (this.compoundFreehandList.length > 0) {
                    while (this.compoundFreehandList.length > 0) {
                        var ft = this.compoundFreehandList.pop();
                        source.removeFeature(ft);
                    }
                }
                else {
                    if (features.length > 0) {
                        source.removeFeature(features[features.length - 1]);
                    }
                }
            }
        };

        /**
         * Attaches an event listener
         * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
         * @param {function} callback - The function to call when the event occurs
         */
        Annotations.prototype.listen = function (eventName, callback) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " is not a valid event");
            }

            this.listeners[eventName].push(callback);
        };

        // fires an event
        Annotations.prototype.fireEvent = function (eventName, eventArgs) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " does not exist");
                return;
            }

            for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
                this.listeners[eventName][i].call(this, eventArgs);
            }
        };

        /**
         * Get the currently selected annotations
         * @return {Array.<{metaData: { PointCount: Number } }>} Array of PMA.core annotation instances
         */
        Annotations.prototype.getSelection = function () {
            return this.selectInteraction.getFeatures().getArray();
        };

        /**
         * Saves all the annotations to PMA.core
         */
        Annotations.prototype.saveAnnotations = function () {
            var current = this.viewport.getAnnotations();

            var added = [];
            var updated = [];

            for (var i = 0; i < current.length; i++) {
                switch (current[i].metaData.State) {
                    case PMA.UI.Types.AnnotationState.Added:
                        added.push(current[i]);
                        break;
                    case PMA.UI.Types.AnnotationState.Pristine:
                    case PMA.UI.Types.AnnotationState.Modified:
                        updated.push(current[i]);
                        break;
                }
            }

            checkPMACore2.call(this, function () {
                if (this.isPMACore2) {
                    saveAnnotationsNew.call(this, added, updated);
                }
                else {
                    saveAddedAnnotations.call(this, added, updated);
                }
            });
        };

        /**
         *  Returns whether any annotation has unsaved changes
         * @returns {bool}
         */
        Annotations.prototype.hasChanges = function () {
            if (this.deletedAnnotations && this.deletedAnnotations.length > 0) {
                return true;
            }

            var current = this.viewport.getAnnotations();

            for (var i = 0; i < current.length; i++) {
                switch (current[i].metaData.State) {
                    case PMA.UI.Types.AnnotationState.Added:
                        return true;
                    case PMA.UI.Types.AnnotationState.Pristine:
                        break;
                    case PMA.UI.Types.AnnotationState.Modified:
                        return true;
                }
            }

            return false;
        };

        /**
         * Deletes an annotation
         * @param  {number} id - The id of the annotation to delete
         * @fires PMA.UI.Components.Events#AnnotationDeleted
         */
        Annotations.prototype.deleteAnnotation = function (id) {
            if (id === null || isNaN(id)) {
                return;
            }

            var f = this.viewport.annotationsLayer.getSource().getFeatureById(id);
            if (f) {
                this.clearHighlight();
                this.clearSelection();
                this.viewport.annotationsLayer.getSource().removeFeature(f);

                f.metaData.State = PMA.UI.Types.AnnotationState.Deleted;

                if (!this.deletedAnnotations) {
                    this.deletedAnnotations = [];
                }

                this.deletedAnnotations.push(f);
                this.fireEvent(PMA.UI.Components.Events.AnnotationDeleted, { annotationId: id, feature: f });
            }
        };

        /**
         * Renders the requested annotation using the highlight style
         * @param  {Number} id - The id of the annotation to render
         */
        Annotations.prototype.highlightAnnotation = function (id) {
            if (id === null || isNaN(id)) {
                return;
            }

            var f = this.viewport.annotationsLayer.getSource().getFeatureById(id);
            if (f) {
                var feats = this.hoverInteraction.getFeatures();
                feats.clear();
                feats.push(f);
            }
        };

        /**
         * Clears all highlighted annotations
         */
        Annotations.prototype.clearHighlight = function () {
            this.hoverInteraction.getFeatures().clear();
        };

        /**
         * Adds the requested annotation in the selection list
         * @param  {Number} id - The id of the annotation to select
         */
        Annotations.prototype.selectAnnotation = function (id) {
            if (id === null || isNaN(id)) {
                return;
            }

            var f = this.viewport.annotationsLayer.getSource().getFeatureById(id);
            if (f) {
                var feats = this.selectInteraction.getFeatures();
                feats.clear();
                feats.push(f);
            }
        };

        /**
         * Clears all selected annotations
         */
        Annotations.prototype.clearSelection = function () {
            this.selectInteraction.getFeatures().clear();
        };

        /**
         * Merges the selected annotations into one geometry
         * @param  {Array.<ol.Feature>} [selection=null] - An array of annotations to merge. If this parameter is not supplied, the currently selected annotations are used.
         */
        Annotations.prototype.mergeSelection = function (selection) {
            if (!selection) {
                selection = this.getSelection();
            }

            if (!selection || selection.length < 1) {
                return;
            }

            var tmpSelection = [];

            for (var i = 0; i < selection.length; i++) {
                var g = selection[i].getGeometry();
                if (g.getFirstCoordinate) {
                    tmpSelection.push({
                        id: selection[i].getId(),
                        geometry: g,
                        first: g.getFirstCoordinate(),
                        last: g.getLastCoordinate(),
                        coordinates: g.getCoordinates(),
                        sqDistanceFirst: 0,
                        sqDistanceLast: 0,
                        sqDistanceFirstFirst: 0,
                        sqDistanceLastFirst: 0
                    });
                }
            }

            for (i = 0; i < tmpSelection.length; i++) {
                this.deleteAnnotation(tmpSelection[i].id);
            }

            var coordinates = tmpSelection[0].coordinates;
            var first = coordinates[0];
            var last = coordinates[coordinates.length - 1];

            tmpSelection.splice(0, 1);

            while (tmpSelection.length > 0) {
                var minDistance = -1;
                var minIndex = -1;
                for (i = 0; i < tmpSelection.length; i++) {
                    tmpSelection[i].sqDistanceFirst = Math.pow(last[0] - tmpSelection[i].first[0], 2) + Math.pow(last[1] - tmpSelection[i].first[1], 2);
                    tmpSelection[i].sqDistanceLast = Math.pow(last[0] - tmpSelection[i].last[0], 2) + Math.pow(last[1] - tmpSelection[i].last[1], 2);

                    tmpSelection[i].sqDistanceFirstFirst = Math.pow(first[0] - tmpSelection[i].first[0], 2) + Math.pow(first[1] - tmpSelection[i].first[1], 2);
                    tmpSelection[i].sqDistanceLastFirst = Math.pow(first[0] - tmpSelection[i].last[0], 2) + Math.pow(first[1] - tmpSelection[i].last[1], 2);

                    if (minDistance == -1 || tmpSelection[i].sqDistanceFirst < minDistance) {
                        minDistance = tmpSelection[i].sqDistanceFirst;
                        minIndex = i;
                    }

                    if (minDistance == -1 || tmpSelection[i].sqDistanceLast < minDistance) {
                        minDistance = tmpSelection[i].sqDistanceLast;
                        minIndex = i;
                    }
                }

                var minItem = tmpSelection.splice(minIndex, 1)[0];

                var reversed = minItem.sqDistanceLast < minItem.sqDistanceFirst;
                for (var c = 0; c < minItem.coordinates.length; c++) {
                    if (!reversed) {
                        coordinates.push(minItem.coordinates[c]);
                    }
                    else {
                        coordinates.push(minItem.coordinates[minItem.coordinates.length - 1 - c]);
                    }
                }

                first = coordinates[0];
                last = coordinates[coordinates.length - 1];
            }

            coordinates.push(coordinates[0]);

            var resultGeometry = new ol.geom.Polygon([coordinates]);

            var feature = new ol.Feature({
                geometry: resultGeometry,
            });

            feature.color = this.lastAnnotationStyle.color;
            feature.fillColor = this.lastAnnotationStyle.fillColor;
            feature.penSize = this.lastAnnotationStyle.penSize;
            feature.icon = this.lastAnnotationStyle.iconPath;

            addFeature.call(this, feature, false);

            this.viewport.annotationsLayer.getSource().addFeatures([feature]);

            this.clearSelection();
        };

        return Annotations;
    })();
}(window.jQuery));
// namespace

window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Authentication = window.PMA.UI.Authentication || {};

(function() {
    // auto login class
    PMA.UI.Authentication.AutoLogin = (function() {

        /**
         * Holds information required to perform an automatic login against a PMA.core server
         * @typedef {Object} PMA.UI.Authentication.AutoLogin~serverCredentials
         * @property {string} serverUrl
         * @property {string} username
         * @property {string} password
         */

        /**
         * Authenticates against a PMA.core server without user interaction. Upon creation, the instance will register itself automatically as an authentication provider for the given context.
         * @param  {PMA.UI.Components.Context} context
         * @param  {PMA.UI.Authentication.AutoLogin~serverCredentials[]} serverCredentials - Array of PMA.core server credentials this authentication provider can manage.
         * @constructor
         * @memberof PMA.UI.Authentication
         * @tutorial 03-gallery
         */
        function AutoLogin(context, serverCredentials) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            if (!(serverCredentials instanceof Array)) {
                console.error("Expected array of server credentials");
                return;
            }

            this.serverCredentials = serverCredentials;
            this.context = context;
            this.context.registerAuthenticationProvider(this);
        }

        /**
         * Authenticates against a PMA.core server. This method is usually invoked by the context itself and should rarely be used outside it.
         * @param  {string} serverUrl - The URL of the PMA.core server to authenticate against
         * @param  {function} [success]
         * @param  {function} [failure]
         * @returns {boolean} True or false depending on whether this instance has enough information to authenticate against this PMA.core server
         */
        AutoLogin.prototype.authenticate = function(serverUrl, success, failure) {
            for (var i = 0, max = this.serverCredentials.length; i < max; i++) {
                if (this.serverCredentials[i].serverUrl == serverUrl) {

                    PMA.UI.Components.login(
                        this.serverCredentials[i].serverUrl,
                        this.serverCredentials[i].username,
                        this.serverCredentials[i].password,
                        this.context.getCaller(),
                        success,
                        failure);

                    return true;
                }
            }
            // Requested server is not handled by this provider
            return false;
        };

        return AutoLogin;
    })();
}());
window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Components = window.PMA.UI.Components || {};

(function () {
    var _ = PMA.UI.Resources.translate;

    // context class
    PMA.UI.Components.Context = (function () {
        /**
         * The Context class glues component instances together. It provides API method implementations and simplifies the interaction with PMA.core by automatically managing authentication and sessionID handling, via the authentication provider classes.
         * @param  {Object} options
         * @param  {string} options.caller
         * @constructor
         * @memberof PMA.UI.Components
         * @tutorial 03-gallery
         * @tutorial 04-tree
         * @tutorial 05-annotations
         */
        function Context(options) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            if (!options || typeof options.caller !== "string") {
                throw "Caller parameter not supplied";
            }

            this.options = options;

            // create event listeners object and add one array for each event type
            this.listeners = {};
            for (var ev in PMA.UI.Components.Events) {
                if (PMA.UI.Components.Events.hasOwnProperty(ev)) {
                    this.listeners[PMA.UI.Components.Events[ev]] = [];
                }
            }

            // list of components that can authenticate against a PMA.core server
            this.authenticationProviders = [];
        }

        // private methods

        // finds the first authentication provider that handles the requested server
        // and calls it's authenticate method 
        function authenticateWithProvider(serverUrl, success, failure) {
            // search all authentication providers apart from PromptLogin
            var i = 0;
            for (i = 0; i < this.authenticationProviders.length; i++) {
                if (this.authenticationProviders[i] instanceof PMA.UI.Authentication.PromptLogin) {
                    continue;
                }

                if (this.authenticationProviders[i].authenticate(serverUrl, success, failure)) {
                    return;
                }
            }

            // if none of the previous providers were able to provide a session, search for a PromptLogin and attempt to authenticate with it
            for (i = 0; i < this.authenticationProviders.length; i++) {
                if (this.authenticationProviders[i] instanceof PMA.UI.Authentication.PromptLogin) {
                    if (this.authenticationProviders[i].authenticate(serverUrl, success, failure)) {
                        return;
                    }

                    break;
                }
            }

            // no provider found, call failure
            if (typeof failure === "function") {
                failure({ Message: _("Authentication failed at server {serverUrl}.", { serverUrl: serverUrl }) });
            }
        }

        /**
         * calls an API method that requires a session ID. This method will first attempt to acquire 
         * a session ID (possibly a cached one) and then call the requested method. 
         * If the method call fails because the call was unauthorized (so possible the session ID was not good),
         * the method will be called again for a second time, this time forcing an authentication before the
         * actual call.
         * @param  {object} options - The parameters to pass to the ajax request
         * @param  {number} options.attemptCount - Current attempt count
         * @param  {string} options.serverUrl - The URL of the server to send the request to
         * @param  {string} options.method - The API method to call
         * @param  {object} options.data - The data to send
         * @param  {string} options.httpMethod - The HTTP method to use
         * @param  {string} options.contentType - The content type of the request
         * @param  {function} options.success - Called upon success
         * @param  {function} options.failure - Called upon failure
         * @param {string} [options.apiPath="api"] - The API path to append to the server URL
         * @fires PMA.UI.Components.Events#SessionIdLoginFailed
         */
        function callApiMethodWithAuthentication(options) {
            var _this = this;

            if (!options.apiPath) {
                options.apiPath = "api";
            }

            _this.getSession(options.serverUrl,
                function (sessionId) {
                    // we have a session ID, try to call the actual method
                    options.data.sessionID = sessionId;

                    PMA.UI.Components.callApiMethod({
                        serverUrl: options.serverUrl,
                        method: options.method,
                        data: options.data,
                        contentType: options.contentType,
                        httpMethod: options.httpMethod,
                        apiPath: options.apiPath,
                        success: function (http) {
                            // the call succeeded, parse data and call success
                            if (typeof options.success === "function") {
                                var response = PMA.UI.Components.parseJson(http.responseText);
                                options.success(options.data.sessionID, response);
                            }
                        },
                        failure: function (http) {
                            // failed, clean up the session ID that was possibly cached for this server
                            PMA.UI.Components.sessionList[options.serverUrl] = null;

                            // if it's the first attempt, try again, otherwise fail for good
                            if (http.status == 0 && options.attemptCount === 0) {
                                options.attemptCount = 1;
                                callApiMethodWithAuthentication.call(_this, options);
                            }
                            else {
                                if (typeof options.failure === "function") {
                                    if (http.responseText && http.responseText.length !== 0) {
                                        try {
                                            var response = PMA.UI.Components.parseJson(http.responseText);
                                            options.failure(response);
                                            _this.fireEvent(PMA.UI.Components.Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
                                        }
                                        catch (ex) {
                                            options.failure(http.responseText);
                                            _this.fireEvent(PMA.UI.Components.Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
                                        }
                                    }
                                    else {
                                        options.failure({ Message: _("Authentication failed") });
                                        _this.fireEvent(PMA.UI.Components.Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
                                    }
                                }
                            }
                        }
                    });
                },
                function (error) {
                    if (!error.Message && error.Reason) {
                        error.Message = error.Reason;
                    }

                    // session acquisition failed in the first place, so fail
                    if (typeof options.failure === "function") {
                        options.failure(error);
                    }

                    _this.fireEvent(PMA.UI.Components.Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
                });
        }

        // end private methods

        // public methods 

        /**
         * Gets the caller value
         * @return {string}
         */
        Context.prototype.getCaller = function () {
            return this.options.caller;
        };

        /**
         * Adds an authentication provider to the list of available authentication methods
         * @param  {PMA.UI.Authentication.AutoLogin|PMA.UI.Authentication.PromptLogin|PMA.UI.Authentication.SessionLogin} provider
         */
        Context.prototype.registerAuthenticationProvider = function (provider) {
            if (typeof provider.authenticate !== "function") {
                console.error("Invalid authentication provider");
            }
            else {
                this.authenticationProviders.push(provider);
            }
        };

        /**
         * Removes an authentication provider from the list of available authentication methods
         * @param  {PMA.UI.Authentication.AutoLogin|PMA.UI.Authentication.PromptLogin|PMA.UI.Authentication.SessionLogin} provider - The provider to remove
         * @param {bool} clearCache - Clears the session ids cache
         */
        Context.prototype.removeAuthenticationProvider = function (provider, clearCache) {
            if (typeof provider.authenticate !== "function") {
                console.error("Invalid authentication provider");
            }
            else {
                for (var i = 0; i < this.authenticationProviders.length; i++) {
                    if (this.authenticationProviders[i] === provider) {
                        this.authenticationProviders.splice(i, 1);

                        // Clear all cache
                        if (clearCache !== false) {
                            PMA.UI.Components.sessionList = {};
                        }
                        break;
                    }
                }
            }
        };

        /**
         * Gets the list of available authentication methods
         * @returns  [{PMA.UI.Authentication.AutoLogin|PMA.UI.Authentication.PromptLogin|PMA.UI.Authentication.SessionLogin}] providers
         */
        Context.prototype.getAuthenticationProviders = function () {
            return this.authenticationProviders;
        };

        /**
        * PMA.core authentication response
        * @typedef {Object} PMA.UI.Components~authenticationResponse
        * @property {string} SessionId - The session ID
        * @property {string} Username - The username
        * @property {string} Email - The user's email
        * @property {string} FirstName - The user's first name
        * @property {string} LastName - The user's last name
        */

        /**
         * A function called after successfully pinging a list of servers
         * @callback PMA.UI.Components.Context~pingServersDoneCallback
         * @param {String[]} servers - A sorted list of server url's from fastest to slowest
         * @param {Object[]} detailInfo - An array of detailed information from pinging the servers (not sorted)
         * @param {string} detailInfo.serverUrl - The server url
         * @param {bool} detailInfo.success - Whether the server responded to any pinging
         * @param {Number[]} detailInfo.times - An array of all the times the server took to respond (in miliseconds)
         * @param {Number} detailInfo.avgTime - The average time the server took to respond (in miliseconds)
         * @param {Number} detailInfo.attempts - The number of attempted pings to the server
         */

        /** 
         * Pings a list of servers to find the fastests
         * @param {string[]} servers - An array of server url to ping
         * @param {PMA.UI.Components.Context~pingServersDoneCallback} done - The done callback to run
         * @param {Number} [maxAttempts=5] - The number of attempts for each server
         * */
        Context.prototype.pingServers = function (servers, done, maxAttempts) {
            if (maxAttempts <= 1 || !maxAttempts || maxAttempts === undefined) {
                maxAttempts = 6;
            }

            var instances = servers.map(function (s) { return { serverUrl: s, success: false, times: [], avgTime: null, attempts: 0 }; });

            var cb = function (startTime, instanceIndex, success) {
                instances[instanceIndex].attempts++;
                if (success && instances[instanceIndex].attempts > 1) {
                    // We ignore first attempt for warm up
                    var time = performance.now() - startTime;
                    instances[instanceIndex].times.push(time);
                    instances[instanceIndex].avgTime = instances[instanceIndex].avgTime != null ?
                        ((instances[instanceIndex].avgTime * instances[instanceIndex].times.length) + time) / (instances[instanceIndex].times.length + 1) : time;
                    instances[instanceIndex].success = true;
                }

                if (instances[instanceIndex].attempts >= maxAttempts) {
                    instanceIndex++;
                }

                if (instanceIndex >= instances.length) {
                    // done testing
                    if (typeof done === "function") {
                        done(instances.sort(function (a, b) {
                            if (a.avgTime != null && b.avgTime != null) {
                                return a.avgTime - b.avgTime;
                            }
                            else if (a.avgTime != null) {
                                return -10000000;
                            }
                            else {
                                return 10000000;
                            }
                        }).map(function (s) { return s.serverUrl; }), instances);
                    }
                }
                else {
                    // Run next test
                    var nowTime = performance.now();
                    PMA.UI.Components.callApiMethod({
                        serverUrl: instances[instanceIndex].serverUrl,
                        method: PMA.UI.Components.ApiMethods.GetVersionInfo,
                        success: cb.bind(this, nowTime, instanceIndex, true),
                        failure: cb.bind(this, nowTime, instanceIndex, false)
                    });
                }
            };

            //Start first test
            var nowTime = performance.now();
            PMA.UI.Components.callApiMethod({
                serverUrl: instances[0].serverUrl,
                method: PMA.UI.Components.ApiMethods.GetVersionInfo,
                success: cb.bind(this, nowTime, 0, true),
                failure: cb.bind(this, nowTime, 0, false)
            });
        };

        /**
         * Gets the user information associated with a server
         * @param {string} serverUrl - The URL of the PMA.core for which to fetch user information
         * @returns {PMA.UI.Components~authenticationResponse} If no authentication has taken place for the particular server, null is returned, otherwise an object.
         */
        Context.prototype.getUserInfo = function (serverUrl) {
            for (var url in PMA.UI.Components.sessionList) {
                if (PMA.UI.Components.sessionList.hasOwnProperty(url) && url === serverUrl && PMA.UI.Components.sessionList[url]) {
                    return PMA.UI.Components.sessionList[url];
                }
            }

            return null;
        };

        /**
         * Called when a session ID was successfully obtained
         * @callback PMA.UI.Components.Context~getSessionCallback
         * @param {string} sessionID
         */

        /**
         * Finds a session ID for the requested server, either by scanning the already cached session ids or by invoking one by one the available authentication providers, until a valid session ID is found
         * @param  {string} serverUrl - The URL of the PMA.core for which to fetch a session ID
         * @param  {PMA.UI.Components.Context~getSessionCallback} success
         * @param  {function} [failure]
         */
        Context.prototype.getSession = function (serverUrl, success, failure) {
            // scan cached session IDs
            for (var url in PMA.UI.Components.sessionList) {
                if (PMA.UI.Components.sessionList.hasOwnProperty(url) && url === serverUrl && PMA.UI.Components.sessionList[url]) {
                    success(PMA.UI.Components.sessionList[url].SessionId);
                    return;
                }
            }

            // no session ID found in cached list
            // fire all providers until one succeeds or all failed
            authenticateWithProvider.call(this, serverUrl, success, failure);
        };

        Context.prototype.getImageInfo = function (serverUrl, pathOrUid, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.GetImageInfo,
                data: {
                    pathOrUid: pathOrUid
                },
                httpMethod: "GET",
                success: success,
                failure: failure
            });
        };

        Context.prototype.getFiles = function (serverUrl, path, success, failure) {
            console.warn("Context.getFiles is deprecated please use Context.getSlides instead");
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.GetFiles,
                data: {
                    path: path
                },
                httpMethod: "GET",
                success: success,
                failure: failure
            });
        };

        /**
        * Gets all slides in a specified path
        * @param {Object} options - Parameters to pass to the GetSlides request
        * @param {string} options.serverUrl - The server url to get slides from
        * @param {string} options.path - The path to get slides from
        * @param {PMA.UI.Components.GetSlidesScope} options.scope - The search scope to use
        * @param {function} [options.success] - Called upon success
        * @param {function} [options.failure] - Called upon failure
        **/
        Context.prototype.getSlides = function (options) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: options.serverUrl,
                method: PMA.UI.Components.ApiMethods.GetFiles,
                data: {
                    path: options.path,
                    scope: options.scope ? options.scope : 0
                },
                httpMethod: "GET",
                success: options.success,
                failure: options.failure
            });
        };

        Context.prototype.getDirectories = function (serverUrl, path, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.GetDirectories,
                data: {
                    path: path
                },
                httpMethod: "GET",
                success: success,
                failure: failure
            });
        };

        Context.prototype.getAnnotations = function (serverUrl, path, currentUserOnly, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.GetAnnotations,
                data: {
                    pathOrUid: path,
                    currentUserOnly: currentUserOnly
                },
                httpMethod: "GET",
                success: success,
                failure: failure
            });
        };

        Context.prototype.addAnnotation = function (serverUrl, path, classification, layerID, notes, geometry, color, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.AddAnnotation,
                data: {
                    pathOrUid: path,
                    classification: classification,
                    layerID: layerID,
                    notes: notes,
                    geometry: geometry,
                    color: color
                },
                httpMethod: "POST",
                contentType: "application/json",
                success: success,
                failure: failure
            });
        };

        Context.prototype.updateAnnotation = function (serverUrl, path, layerID, annotationID, notes, geometry, color, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.UpdateAnnotation,
                data: {
                    pathOrUid: path,
                    layerID: layerID,
                    annotationID: annotationID,
                    notes: notes,
                    geometry: geometry,
                    color: color
                },
                httpMethod: "POST",
                contentType: "application/json",
                success: success,
                failure: failure
            });
        };

        Context.prototype.saveAnnotations = function (serverUrl, path, added, updated, deleted, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.SaveAnnotations,
                data: {
                    pathOrUid: path,
                    added: added,
                    updated: updated,
                    deleted: deleted
                },
                httpMethod: "POST",
                contentType: "application/json",
                success: success,
                failure: failure
            });
        };

        Context.prototype.deleteAnnotation = function (serverUrl, path, layerID, annotationID, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.DeleteAnnotation,
                data: {
                    pathOrUid: path,
                    layerID: layerID,
                    annotationID: annotationID
                },
                httpMethod: "POST",
                contentType: "application/json",
                success: success,
                failure: failure
            });
        };

        Context.prototype.getFormDefinitions = function (serverUrl, formIDs, rootDirectoryAlias, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.GetFormDefinitions,
                data: {
                    formIDs: formIDs instanceof Array ? formIDs.join(",") : formIDs,
                    rootDirectoryAlias: rootDirectoryAlias
                },
                httpMethod: "GET",
                success: success,
                failure: failure
            });
        };

        Context.prototype.getFormSubmissions = function (serverUrl, pathOrUids, formIDs, currentUserOnly, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.GetFormSubmissions,
                data: {
                    pathOrUids: pathOrUids instanceof Array ? pathOrUids : [],
                    formIDs: formIDs instanceof Array ? formIDs : [],
                    currentUserOnly: currentUserOnly
                },
                httpMethod: "POST",
                contentType: "application/json",
                success: success,
                failure: failure
            });
        };

        Context.prototype.saveFormDefinition = function (serverUrl, definition, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.SaveFormDefinition,
                apiPath: "admin",
                contentType: "application/json",
                data: {
                    definition: definition
                },
                httpMethod: "POST",
                success: success,
                failure: failure
            });
        };

        Context.prototype.deleteFormDefinition = function (serverUrl, formID, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.DeleteFormDefinition,
                apiPath: "admin",
                data: {
                    formID: formID
                },
                httpMethod: "GET",
                success: success,
                failure: failure
            });
        };

        Context.prototype.getVersionInfo = function (serverUrl, success, failure) {
            PMA.UI.Components.callApiMethod({
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.GetVersionInfo,
                success: function (http) {
                    if (typeof success === "function") {
                        var response = PMA.UI.Components.parseJson(http.responseText);
                        success(response);
                    }
                },
                failure: function (http) {
                    if (typeof failure === "function") {
                        if (http.responseText && http.responseText.length !== 0) {
                            var response = PMA.UI.Components.parseJson(http.responseText);
                            failure(response);
                        }
                        else {
                            failure({ Message: _("Get Version Info failed") });
                        }
                    }
                }
            });
        };

        /**
        * Gets the events log from the server
        * @param {Object} options - Parameters to pass to the GetEvents request
        * @param {string} options.serverUrl - The server url to get events log
        * @param {number} options.page - The page to fetch
        * @param {number} options.pageSize - The page size to fetch
        * @param {function} [options.success] - Called upon success
        * @param {function} [options.failure] - Called upon failure
        **/
        Context.prototype.getEvents = function (options, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: options.serverUrl,
                method: PMA.UI.Components.ApiMethods.GetEvents,
                apiPath: "admin",
                data: {
                    page: options.page,
                    pageSize: options.pageSize
                },
                httpMethod: "GET",
                success: success,
                failure: failure
            });
        };

        Context.prototype.deAuthenticate = function (serverUrl, success, failure) {
            var sessionId = null;
            if (PMA.UI.Components.sessionList.hasOwnProperty(serverUrl) && PMA.UI.Components.sessionList[serverUrl]) {
                sessionId = PMA.UI.Components.sessionList[serverUrl].SessionId;
                if (!sessionId) {
                    // nothing to do, no session ID cached, call success
                    if (typeof success === "function") {
                        success();
                        return;
                    }
                }
            }
            else {
                var cbFn = function (sId) {
                    sessionId = sId;
                };

                // there is no cached sessionId for this server check for a sessionLogin provider
                for (i = 0; i < this.authenticationProviders.length; i++) {
                    if (this.authenticationProviders[i] instanceof PMA.UI.Authentication.SessionLogin) {
                        // check that this sessionLogin provider can handle the requested serverUrl
                        if (this.authenticationProviders[i].authenticate(serverUrl, cbFn, null)) {
                            break;
                        }

                        continue;
                    }
                }
            }

            if (sessionId == null) {
                // cannot handle this serverUrl just return
                success();
                return;
            }

            PMA.UI.Components.sessionList[serverUrl] = null;

            PMA.UI.Components.callApiMethod({
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.DeAuthenticate,
                data: { sessionID: sessionId },
                success: function (http) {
                    if (typeof success === "function") {
                        success();
                    }
                },
                failure: function (http) {
                    if (typeof failure === "function") {
                        if (http.responseText && http.responseText.length !== 0) {
                            var response = PMA.UI.Components.parseJson(http.responseText);
                            failure(response);
                        }
                        else {
                            failure();
                        }
                    }
                }
            });
        };

        Context.prototype.queryFilename = function (serverUrl, path, pattern, success, failure) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.QueryFilename,
                apiPath: "query",
                data: {
                    path: path,
                    pattern: pattern
                },
                httpMethod: "GET",
                success: success,
                failure: failure
            });
        };

        /**
        * Gets all distinct values for a field in aform
        * @param {Object} options - Parameters to pass to the "distinct values" request
        * @param {string} options.serverUrl - The server url to use
        * @param {string} options.formId - The Form Id to use
        * @param {string} options.fieldId - The Field Id to get distinct values for
        * @param {function} [options.success] - Called upon success
        * @param {function} [options.failure] - Called upon failure
        **/
        Context.prototype.distinctValues = function (options) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: options.serverUrl,
                method: PMA.UI.Components.ApiMethods.DistinctValues,
                apiPath: "query",
                data: {
                    formID: options.formId,
                    fieldID: options.fieldId
                },
                httpMethod: "GET",
                success: options.success,
                failure: options.failure
            });
        };

        /**
       * Gets information for all slides specified
       * @param {Object} options - Parameters to pass to the GetSlides request
       * @param {string} options.serverUrl - The server url to get slides from
       * @param {string[]} options.images - An array of image paths or uids to fetch information for
       * @param {function} [options.success] - Called upon success
       * @param {function} [options.failure] - Called upon failure
       **/
        Context.prototype.getImagesInfo = function (options) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: options.serverUrl,
                method: PMA.UI.Components.ApiMethods.GetImagesInfo,
                apiPath: "api",
                data: {
                    pathOrUids: options.images
                },
                contentType: "application/json",
                httpMethod: "POST",
                success: options.success,
                failure: options.failure
            });
        };

        // legacy alias because of typo
        Context.prototype.GetImagesInfo = Context.prototype.getImagesInfo;

        /**
       * Gets slides that satisfy the specified expressions
       * @param {Object} options - Parameters to pass to the "distinct values" request
       * @param {string} options.serverUrl - The server url to use
       * @param {Object[]} options.expressions - The Expressions to use
       * @param {Number} options.expressions.FormID - The form Id for this expression
       * @param {Number} options.expressions.FieldID - The field Id for this expression
       * @param {Number} options.expressions.Operator - The Operator for this expression ( Equals = 0, LessThan = 1, LessThanOrEquals = 2, GreaterThan = 3, GreaterThanOrEquals = 4)
       * @param {Number} options.expressions.Value - The value to compare for this expression
       * @param {function} [options.success] - Called upon success
       * @param {function} [options.failure] - Called upon failure
       **/
        Context.prototype.metadata = function (options) {
            callApiMethodWithAuthentication.call(
                this, {
                attemptCount: 0,
                serverUrl: options.serverUrl,
                method: PMA.UI.Components.ApiMethods.Metadata,
                apiPath: "query",
                data: {
                    expressions: options.expressions
                },
                httpMethod: "POST",
                contentType: "application/json",
                success: options.success,
                failure: options.failure
            });
        };

        // registers an event listener
        Context.prototype.listen = function (eventName, callback) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " is not a valid event");
            }

            this.listeners[eventName].push(callback);
        };

        Context.prototype.fireEvent = function (eventName, eventArgs) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " does not exist");
                return;
            }

            for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
                this.listeners[eventName][i](eventArgs);
            }
        };

        /** end public methods */

        return Context;
    })();
}());
window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Authentication = window.PMA.UI.Authentication || {};

// namespace
(function() {

    PMA.UI.Authentication.CustomLogin = (function() {
        /**
         * This callback should be called when the authentication to a server was successfull
         * @callback PMA.UI.Authentication.CustomLogin~authenticateSuccess
         * @param {string} sessionID - The session id acquired by authenticating to the server
         */

        /**
         * This callback should be called when the authentication to a server failed
         * @callback PMA.UI.Authentication.CustomLogin~authenticateError
         * @param {object} error - The error occured when trying to authenticate to the server
         * @param {string} error.Message - The error message to display
         */
        
        /**
         * The callback function used to authenticate to a server url
         * @callback PMA.UI.Authentication.CustomLogin~authenticateCallback
         * @param  {string} serverUrl - The URL of the PMA.core server to authenticate against
         * @param  {PMA.UI.Authentication.CustomLogin~authenticateSuccess} [success]
         * @param  {PMA.UI.Authentication.CustomLogin~authenticateError} [failure]
         * @returns {boolean} True or false depending on whether this instance has enough information to authenticate against this PMA.core server
         */

        /**
         * Authenticates against a PMA.core server with a custom callback function. The caller should implement the callback method with the required parameters
         * that authenticates to a server. The implemented function should call the success/error callbacks that are passed as parameters and return a value indicating
         * whether this provider can handle authentication to the specified server
         * @param  {PMA.UI.Components.Context} context
         * @param {PMA.UI.Authentication.CustomLogin~authenticateCallback} authenticate
         * @constructor
         * @memberof PMA.UI.Authentication
         */
        function CustomLogin(context, authenticate) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            if ((typeof authenticate !== "function")) {
                console.error("Expected authenticate callback function");
                return;
            }

            this.authenticateCb = authenticate;
            this.context = context;
            this.context.registerAuthenticationProvider(this);
        }

        /**
         * Authenticates against a PMA.core server. This method is usually invoked by the context itself and should rarely be used outside it.
         * @param  {string} serverUrl - The URL of the PMA.core server to authenticate against
         * @param  {function} [success]
         * @param  {function} [failure]
         * @returns {boolean} True or false depending on whether this instance has enough information to authenticate against this PMA.core server
         */
        CustomLogin.prototype.authenticate = function(serverUrl, success, failure) {
            return this.authenticateCb(serverUrl, success, failure);
        };

        return CustomLogin;
    })();
}());
window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Components = window.PMA.UI.Components || {};

// namespace
(function ($) {
    var _ = PMA.UI.Resources.translate;

    function arrayContains(array, value) {
        for (var i = 0; i < array.length; i++) {
            if (array[i] == value) {
                return true;
            }
        }

        return false;
    }

    PMA.UI.Components.Forms = (function () {
        /**
         * Represents a component that provides both UI and programmatic interaction with PMA.core forms and data.
         * @param  {PMA.UI.Components.Context} context
         * @param  {Object} options - Reserved for future use
         * @constructor
         * @memberof PMA.UI.Components
         * @tutorial 06-forms
         */
        function Forms(context, options) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            this.listeners = {};
            this.listeners[PMA.UI.Components.Events.FormSaved] = [];
            this.listeners[PMA.UI.Components.Events.FormEditClick] = [];
            this.context = context;
        }

        /**
         * Available form field types
         * @readonly
         * @enum {number}
         */
        Forms.FieldType = {
            /**
             * Simple text (input)
             */
            Text: 0,

            /**
             * Simple paragraph (label)
             */
            Paragraph: 1,

            /**
             * Dropdown
             */
            ListBox: 2,

            /**
             * Checkbox list
             */
            CheckBox: 3,

            /**
             * Radio button list
             */
            RadioButton: 4,

            /**
             * Integer input
             */
            Integer: 5,

            /**
             * Decimal input
             */
            Double: 6,

            /**
             * Datetime input
             */
            DateTime: 7,

            /**
             * Percentage input
             */
            Percentage: 8,

            /**
             * Section separator - label
             */
            Label: 9,

            /**
             * HyperLink
             */
            HyperLink: 10
        };

        function getCheckboxesValue(el) {
            var value = "";
            el.each(function () {
                if (value.length > 0) {
                    value += "|";
                }

                value += this.value;
            });

            return value;
        }

        function createPostData(sessionId) {
            var values = [];
            var form = this.form;

            for (var i = 0; i < this.form.FormFields.length; i++) {
                var field = form.FormFields[i];
                var fieldId = getFieldId(this.form, i);

                var value = null,
                    below = false,
                    other = false,
                    el, otherElVal;
                switch (field.FieldType) {
                    case Forms.FieldType.Text:
                    case Forms.FieldType.Paragraph:
                    case Forms.FieldType.DateTime:
                        el = $("#" + fieldId);
                        if (el.val() !== "") {
                            value = el.val();
                        }

                        break;
                    case Forms.FieldType.Integer:
                    case Forms.FieldType.Double:
                    case Forms.FieldType.Percentage:
                        el = $("#" + fieldId);
                        if (el.val() !== "") {
                            value = el.val();
                        }
                        else if (field.AllowBelowDetectableLimit === true && $("#" + fieldId + "_below:checked").length === 1) {
                            below = true;
                        }

                        break;
                    case Forms.FieldType.ListBox:
                        el = $("#" + fieldId);
                        if (el.val() !== "") {
                            value = el.val();
                        }
                        else if (field.AllowOther && $("#" + fieldId + "_other").val() !== "") {
                            value = $("#" + fieldId + "_other").val();
                            other = true;
                        }

                        break;
                    case Forms.FieldType.CheckBox:
                        otherElVal = $("#" + fieldId + "_other").val();
                        el = $("input[name='" + fieldId + "']:checked");
                        if (el.length !== 0) {
                            value = getCheckboxesValue(el);
                        }
                        else if (field.AllowOther === true && otherElVal !== "") {
                            value = otherElVal;
                            other = true;
                        }

                        break;
                    case Forms.FieldType.RadioButton:
                        otherElVal = $("#" + fieldId + "_other").val();
                        el = $("input[name='" + fieldId + "']:not([id$='_other_indicator']):checked");

                        if (el.length !== 0) {
                            value = el.val();
                        }
                        else if (field.AllowOther === true && otherElVal !== "") {
                            value = otherElVal;
                            other = true;
                        }

                        break;
                }

                values.push({
                    FieldId: field.FieldID,
                    FormValue: value,
                    IsBelowDetectableLimit: below,
                    IsOtherValue: other
                });
            }

            return {
                sessionID: sessionId,
                pathOrUid: this.path,
                formId: this.form.FormID,
                fieldValues: values
            };
        }

        function submitForm(success, failure) {
            if (!validateForm.call(this)) {
                if (typeof failure === "function") {
                    failure({ success: false, message: _("Form validation error") });
                }

                return;
            }

            var _this = this;
            _this.context.getSession(_this.serverUrl, function (sessionId) {
                var postData = createPostData.call(_this, sessionId);

                PMA.UI.Components.callApiMethod({
                    serverUrl: _this.serverUrl,
                    method: PMA.UI.Components.ApiMethods.SaveFormData,
                    httpMethod: "POST",
                    contentType: 'application/json',
                    data: postData,
                    success: function (http) {
                        _this.hasChangesProperty = false;

                        if (typeof success === "function") {
                            success({ success: true, message: '' });
                        }
                    },
                    failure: function (http) {
                        var errorMsg = _("Save failed. Check the form values and try again.");
                        if (http && http.responseText) {
                            var errorObj = JSON.parse(http.responseText);
                            if (errorObj && errorObj.Message) {
                                errorMsg = errorObj.Message;
                            }
                        }

                        $("#" + _this.form.ClientID + "_validation").html(errorMsg);
                        if (typeof failure === "function") {
                            failure({ success: false, message: errorMsg });
                        }
                    }
                });
            }, function (args) {
                if (typeof failure === "function") {
                    failure({ success: false, message: ((args && args.Message) ? args.Message : _("Authentication error")) });
                }
            });
        }

        function clearValidationErrors() {
            if (!this.form) {
                return;
            }

            $("#" + this.form.ClientID + " [id$='_validation']").html("");
            $("#" + this.form.ClientID + " *").removeClass("invalid");
        }

        function validateForm() {
            if (!this.form) {
                return false;
            }

            clearValidationErrors.call(this);

            var form = this.form;
            $("#" + form.ClientID + "_validation").html("");

            var el, otherEl, isValid;

            for (var i = 0; i < this.form.FormFields.length; i++) {
                var field = form.FormFields[i];
                var fieldId = getFieldId(this.form, i);

                var validationSpan = $("#" + fieldId + "_validation");
                validationSpan.html("");

                switch (field.FieldType) {
                    case Forms.FieldType.Text:
                    case Forms.FieldType.Paragraph:
                    case Forms.FieldType.DateTime:
                        el = $("#" + fieldId);

                        if (field.Required === true && el.val() === "") {
                            el.addClass("invalid");
                            el.focus();
                            validationSpan.html(_("Please enter a value"));

                            return false;
                        }

                        el.removeClass("invalid");
                        break;
                    case Forms.FieldType.Integer:
                    case Forms.FieldType.Double:
                    case Forms.FieldType.Percentage:

                        el = $("#" + fieldId);
                        if (field.Required === true && el.val() === "") {
                            isValid = false;
                            if (field.AllowBelowDetectableLimit === true && $("#" + fieldId + "_below:checked").length === 1) {
                                isValid = true;
                            }

                            if (!isValid) {
                                el.addClass("invalid");
                                el.focus();
                                validationSpan.html(_("Please enter a value"));

                                return false;
                            }
                        }

                        var numVal = el.val();
                        if (isNaN(numVal)) {
                            el.addClass("invalid");
                            el.focus();
                            validationSpan.html(_("Please enter a valid value"));

                            return false;
                        }

                        if (typeof numVal !== "number") {
                            numVal = parseFloat(numVal);
                        }

                        if (typeof field.LowerBound === "number" && numVal < field.LowerBound) {
                            el.addClass("invalid");
                            el.focus();
                            validationSpan.html(_("Please enter a value larger than or equal to {LowerBound}", { LowerBound: field.LowerBound }));

                            return false;
                        }

                        if (typeof field.UpperBound === "number" && numVal > field.UpperBound) {
                            el.addClass("invalid");
                            el.focus();
                            validationSpan.html(_("Please enter a value less than or equal to {UpperBound}", { UpperBound: field.UpperBound }));

                            return false;
                        }

                        el.removeClass("invalid");
                        break;
                    case Forms.FieldType.ListBox:
                        el = $("#" + fieldId);
                        if (field.Required === true && el.val() === "") {
                            isValid = false;

                            if (field.AllowOther && $("#" + fieldId + "_other").val() !== "") {
                                isValid = true;
                            }

                            if (!isValid) {
                                el.addClass("invalid");
                                el.focus();
                                validationSpan.html(_("Please select a value"));

                                return false;
                            }
                        }

                        el.removeClass("invalid");
                        break;
                    case Forms.FieldType.CheckBox:
                    case Forms.FieldType.RadioButton:
                        // if the field is required and not option is select apart from the "other value" indicator
                        if (field.Required === true && $("input[name='" + fieldId + "']:not([id$='_other_indicator']):checked").length === 0) {
                            if (field.AllowOther !== true || $("#" + fieldId + "_other").val() === "") {
                                $("input[name='" + fieldId + "']").first().focus();
                                validationSpan.html(_("Please select a value"));
                                return false;
                            }
                        }

                        break;
                    case Forms.FieldType.Label:
                    case Forms.FieldType.HyperLink:
                        break;
                    default:
                        console.error("Unknown field type " + field.FieldType);
                        return false;
                }
            }

            return true;
        }

        function bindClearFieldValueWhenBelowCheckedEvent(field, fieldId) {
            $("#" + fieldId + "_below").change(function () {
                if (this.checked) {
                    $("#" + fieldId).val("");
                }
            });
        }

        function bindUncheckBelowEvent(field, fieldId) {
            $("#" + fieldId).change(function () {
                if ($(this).val() !== "") {
                    $("#" + fieldId + "_below").prop("checked", false);
                }
            });
        }

        function bindClearFieldValueWhenOtherChangedEvent(field, fieldId) {
            $("#" + fieldId + "_other").change(function () {
                if ($(this).val() !== "") {
                    $("#" + fieldId).val("");
                    $("input[name='" + fieldId + "'").prop("checked", false);
                    $("#" + fieldId + "_other_indicator").prop("checked", true);
                }
            });
        }

        function bindClearOtherWhenValueSelectedEvent(field, fieldId) {
            $("input[name='" + fieldId + "'], #" + fieldId).change(function () {
                if (this.checked || (this.tagName === "SELECT" && this.value !== "")) {
                    $("#" + fieldId + "_other").val("");
                }
            });
        }

        function bindFormElementEvents() {
            var _this = this;
            $("#" + this.form.ClientID).submit(function (evt) {
                evt.preventDefault();
                _this.saveForm();
            }).change(function () {
                _this.hasChangesProperty = true;
            });

            $("#" + this.form.ClientID + " legend .edit-button").click(function (evt) {
                evt.preventDefault();
                _this.fireEvent(PMA.UI.Components.Events.FormEditClick);
            });

            $("#" + this.form.ClientID + " [type='reset']").click(function (evt) {
                clearValidationErrors.call(_this);
                _this.hasChangesProperty = false;
                $("#" + _this.form.ClientID + " select").val('');
                $("#" + _this.form.ClientID + " input[type='text']").val('');
                $("#" + _this.form.ClientID + " input[type='number']").val('');
                $("#" + _this.form.ClientID + " textarea").val('');
                $("#" + _this.form.ClientID + " input[type='checkbox'], #" + _this.form.ClientID + " input[type='radio']").prop('checked', false);
                evt.preventDefault();
            });

            for (var i = 0; i < this.form.FormFields.length; i++) {
                var fieldId = getFieldId(this.form, i);

                var field = this.form.FormFields[i];
                if (field.AllowBelowDetectableLimit) {
                    bindClearFieldValueWhenBelowCheckedEvent.call(_this, field, fieldId);
                    bindUncheckBelowEvent.call(_this, field, fieldId);
                }

                if (field.AllowOther) {
                    bindClearFieldValueWhenOtherChangedEvent.call(_this, field, fieldId);
                    bindClearOtherWhenValueSelectedEvent.call(_this, field, fieldId);
                    //bindUncheckBelowEvent.call(_this, field, fieldId);
                }
            }
        }

        function getFieldId(form, fieldIndex) {
            return form.ClientID + "_field_" + form.FormFields[fieldIndex].FieldID;
        }

        function getDataRecord(fieldId) {
            if (!this.originalData) {
                return;
            }

            for (var i = 0; i < this.originalData.length; i++) {
                var dataSet = this.originalData[i];
                if (!dataSet.FieldValues || !dataSet.FieldValues.length) {
                    continue;
                }

                for (var j = 0; j < dataSet.FieldValues.length; j++) {
                    var f = dataSet.FieldValues[j];
                    if (f.FieldId === fieldId) {
                        return f;
                    }
                }
            }

            return null;
        }

        function renderField(form, fieldIndex, dataOptions) {
            var field = form.FormFields[fieldIndex];
            var list, k;

            field.Tooltip = field.Tooltip || "";

            var fieldId = getFieldId(form, fieldIndex);
            var record = getDataRecord.call(this, field.FieldID);

            if (!record) {
                record = {
                    IsOtherValue: false,
                    IsBelowDetectableLimit: false,
                    FormValue: null
                };
            }

            if (typeof dataOptions.fieldCb === "function") {
                if (dataOptions.fieldCb(form, field, record) === false) {
                    return '';
                }
            }

            var fieldGroupClass = '';

            if (field.ExtraStyle) {
                fieldGroupClass += " " + field.ExtraStyle + " ";
            }

            if (field.fieldGroupClass) {
                fieldGroupClass += " " + field.fieldGroupClass + " ";
            }

            var strVal = record && record.FormValue ? record.FormValue : '';

            var html = '<div class="form-group ' + (field.Required === true ? "required" : "") + " " + fieldGroupClass + '" title="' + field.Tooltip + '">';
            if (field.FieldType !== Forms.FieldType.HyperLink) {
                html += '<label for="' + fieldId + '">' + field.Label + '</label>';
            }

            html += "<div class='field-container " + (dataOptions.fieldContainerClass ? dataOptions.fieldContainerClass : "") + " " + (field.fieldContainerClass ? field.fieldContainerClass : "") + " '>";

            var requiredStr = "";
            if (field.Required === true) {
                requiredStr = ' required="required" ';
            }

            var inputClassStr = '';
            if (dataOptions.inputClass) {
                inputClassStr += " " + dataOptions.inputClass + " ";
            }

            if (field.fieldClass) {
                inputClassStr += " " + field.fieldClass + " ";
            }

            if (field.FieldType === Forms.FieldType.DateTime) {
                console.warn("PMA.UI.Components.Forms: No proper date time field support yet.");
            }

            if (dataOptions.readOnly === true) {
                html += this.renderReadOnlyField(field, record, inputClassStr);
            }
            else {
                switch (field.FieldType) {
                    case Forms.FieldType.Text:
                        html += '<input id="' + fieldId + '" name="' + fieldId + '" value="' + strVal + '" type="text" placeholder="' + field.Tooltip + '" ' + requiredStr + ' class="' + inputClassStr + '" />';
                        break;
                    case Forms.FieldType.DateTime: // no proper date time support yet
                        html += '<input id="' + fieldId + '" name="' + fieldId + '" value="' + strVal + '" type="text" placeholder="yyyy-MM-dd HH:mm:ss" ' + requiredStr + ' class="' + inputClassStr + '" />';
                        break;
                    case Forms.FieldType.Paragraph:
                        html += '<textarea id="' + fieldId + '" name="' + fieldId + '" placeholder="' + field.Tooltip + '"' + requiredStr + ' class="' + inputClassStr + '">' + strVal + '</textarea>';
                        break;
                    case Forms.FieldType.ListBox:
                        html += '<select id="' + fieldId + '" name="' + fieldId + '" ' + ' class="' + inputClassStr + '">';

                        if (field.FormList && field.FormList.FormListValues) {
                            html += '<option value="" ' + (strVal.length === 0 ? 'selected="selected"' : "") + '>' + field.Tooltip + '</option>';

                            list = field.FormList.FormListValues;
                            for (k = 0; k < list.length; k++) {
                                html += '<option value="' + list[k].ValueID + '" ' + (strVal.length !== 0 && list[k].ValueID == strVal ? 'selected="selected"' : "") + '>' + list[k].Value + '</option>';
                            }
                        }
                        html += '</select>';

                        break;
                    case Forms.FieldType.CheckBox:
                        html += '<ul class="checkboxlist">';

                        var strValues = strVal.split("|");

                        if (field.FormList && field.FormList.FormListValues) {
                            list = field.FormList.FormListValues;
                            for (k = 0; k < list.length; k++) {
                                html += '<li>';
                                html += '<input type="checkbox" class="' + inputClassStr + '" id="' + fieldId + '_' + list[k].ValueID + '" name="' + fieldId + '" value="' + list[k].ValueID + '" ' + (arrayContains(strValues, "" + list[k].ValueID) ? 'checked="checked"' : "") + '/>';
                                html += '<label for="' + fieldId + '_' + list[k].ValueID + '">' + list[k].Value + '</label>';
                                html += "</li>";
                            }
                        }

                        html += '</ul>';
                        break;
                    case Forms.FieldType.RadioButton:
                        html += '<ul class="radiobuttonlist">';

                        if (field.FormList && field.FormList.FormListValues) {
                            list = field.FormList.FormListValues;

                            /*
                            if (field.Required === false) {
                                html += '<li>';
                                html += '<input type="radio" id="' + fieldId + '_novalue" name="' + fieldId + '" value="" />';
                                html += '<label for="' + fieldId + '_novalue">' + _("(No selection)") + '</label>';
                                html += "</li>";
                            }
                            */

                            for (k = 0; k < list.length; k++) {
                                html += '<li>';
                                html += '<input type="radio" class="' + inputClassStr + '" id="' + fieldId + '_' + list[k].ValueID + '" name="' + fieldId + '" value="' + list[k].ValueID + '" ' + (strVal !== "" && list[k].ValueID == strVal ? 'checked="checked"' : "") + ' />';
                                html += '<label for="' + fieldId + '_' + list[k].ValueID + '">' + list[k].Value + '</label>';
                                html += "</li>";
                            }

                            if (field.AllowOther === true) {
                                html += '<li>';
                                html += '<input type="radio" class="' + inputClassStr + '" id="' + fieldId + '_other_indicator" name="' + fieldId + '" ' + (record && record.IsOtherValue ? 'checked="checked"' : "") + ' value="other" />';
                                html += '<label for="' + fieldId + '_other_indicator">' + _("Other value") + '</label>';
                                html += "</li>";
                            }
                        }

                        html += '</ul>';
                        break;
                    case Forms.FieldType.Integer:
                        var step = 1;
                        if (field.Interval) {
                            step = field.Interval | 0;
                        }

                        html += '<input id="' + fieldId + '" name="' + fieldId + '"' + requiredStr + ' ' + (field.LowerBound !== null ? 'min="' + field.LowerBound + '"' : "") + ' ' + (field.UpperBound !== null ? 'max="' + field.UpperBound + '"' : "") + ' type="number" step="' + step + '" value="' + strVal + '"  placeholder="' + field.Tooltip + '" class="' + inputClassStr + '"/>';
                        break;
                    case Forms.FieldType.Double:
                        html += '<input id="' + fieldId + '" name="' + fieldId + '"' + requiredStr + ' ' + (field.LowerBound !== null ? 'min="' + field.LowerBound + '"' : "") + ' ' + (field.UpperBound !== null ? 'max="' + field.UpperBound + '"' : "") + ' type="number" step="' + (field.Interval ? field.Interval : 'any') + '" value="' + strVal + '" placeholder="' + field.Tooltip + '" class="' + inputClassStr + '" />';
                        break;
                    case Forms.FieldType.Percentage:
                        var minv = 0,
                            maxv = 100;
                        if (field.LowerBound !== null && (field.LowerBound | 0) >= 0) {
                            minv = field.LowerBound | 0;
                        }

                        if (field.UpperBound !== null && (field.UpperBound | 0) <= 0) {
                            maxv = field.UpperBound | 0;
                        }

                        html += '<input id="' + fieldId + '" name="' + fieldId + '"' + requiredStr + ' min="' + minv + '" max="' + maxv + '" type="number" step="' + (field.Interval ? field.Interval : 'any') + '" value="' + strVal + '" placeholder="' + field.Tooltip + '" class="' + inputClassStr + '" />';
                        break;
                    case Forms.FieldType.Label:
                        ////html += '<div class="labelfield" id="' + fieldId + '">' + field.Label + '</div>';
                        break;
                    case Forms.FieldType.HyperLink:
                        html += '<a class="link-field" id="' + fieldId + '" title="' + field.Tooltip + '" href="' + field.Url + '" ' + (field.NewWindow === true ? ' target="_blank" ' : '') + '>' + field.Label + '</a>';
                        break;
                }

                if (field.AllowBelowDetectableLimit === true) {
                    html += '<div class="below-container">';
                    html += '<input id="' + fieldId + '_below" name="' + fieldId + '_below" type="checkbox"  ' + (record && record.IsBelowDetectableLimit === true ? 'checked="checked"' : "") + ' value="below" />';
                    html += '<label for="' + fieldId + '_below">' + _("Below detectable limit") + '</label>';
                    html += '<div>';
                }

                if (field.AllowOther === true) {
                    html += '<input id="' + fieldId + '_other" class="other ' + (dataOptions.inputClass ? dataOptions.inputClass : "") + '" name="' + fieldId + '_other" type="text" value="' + (strVal !== "" && record && record.IsOtherValue === true ? strVal : "") + '" placeholder="' + _("Other value") + '" />';
                }
            }

            html += '<span id="' + fieldId + '_validation" class="field-validation ' + (dataOptions.fieldValidationClass ? dataOptions.fieldValidationClass : "") + '"></span>';

            if (field.additionalHtml) {
                html += field.additionalHtml;
            }

            html += "</div></div>";

            return html;
        }

        function renderForm(element, form, dataOptions, data, success, failure) {
            var _this = this;
            _this.form.ClientID = "frm" + ((Math.random() * 10000000) | 0);
            _this.hasChangesProperty = false;

            var html = "<form class='pma-ui-form " + (dataOptions.formClass ? dataOptions.formClass : "") + "' name='" + form.ClientID + "' id='" + form.ClientID + "' autocomplete='off' novalidate><fieldset>";

            if (form.FormName) {
                html += "<legend class='form-name'>" + form.FormName + (dataOptions.editButton === true && dataOptions.readOnly === true ? "<span class=\"edit-button\">" + _("[Edit]") + "</span>" : "") + "</legend>";
            }

            /*            
            if (form.Description) {
                html += "<div class='form-description'>" + form.Description + "</div>";
            }
            */

            if (form.Instructions) {
                html += "<div class='form-instructions'>" + form.Instructions + "</div>";
            }

            form.FormFields.sort(function (a, b) {
                return a.DisplayOrder - b.DisplayOrder;
            });

            for (var i = 0; i < form.FormFields.length; i++) {
                html += renderField.call(_this, form, i, dataOptions);
            }

            html += "<div id='" + form.ClientID + "_validation' class='form-validation " + (dataOptions.validationClass ? dataOptions.validationClass : "") + "'></div>";

            if (dataOptions.readOnly !== true) {
                html += "<div class='input-container " + (dataOptions.btnContainerClass ? dataOptions.btnContainerClass : "") + "'>";
                html += "<button type='submit' value='" + _("Save") + "' class='" + (dataOptions.btnSaveClass ? dataOptions.btnSaveClass : "") + "' >" + _("Save") + "</button>";
                html += "<button type='reset' value='" + _("Reset") + "' class='" + (dataOptions.btnResetClass ? dataOptions.btnResetClass : "") + "' >" + _("Reset") + "</button>";
            }

            html += "</div>";
            html += "</fieldset></form>";

            $(element).html(html);
            bindFormElementEvents.call(_this);
            if (typeof success === "function") {
                success.call(_this, _this.form, null);
            }
        }

        function loadFormData(serverUrl, sessionId, form, path, currentUserOnly, dataFilter, success, failure) {
            var _this = this;
            PMA.UI.Components.callApiMethod({
                serverUrl: serverUrl,
                method: PMA.UI.Components.ApiMethods.GetFormData,
                data: { sessionID: sessionId, pathOrUid: path, formId: form.FormID, currentUserOnly: currentUserOnly },
                success: function (http) {
                    var response = PMA.UI.Components.parseJson(http.responseText);
                    if (typeof success === "function") {
                        var data = null;
                        if (typeof dataFilter === "string") {
                            for (var i = 0; i < response.length; i++) {
                                if (response[i].Login == dataFilter) {
                                    data = [response[i]];
                                    break;
                                }
                            }
                        }
                        else if (typeof dataFilter === "function") {
                            data = dataFilter.call(_this, response);
                        }
                        else {
                            data = response;
                        }

                        success.call(_this, data);
                    }
                },
                failure: failure
            });
        }

        /**
         * Renders a form field in read-only form
         * @param  {Object} field - A PMA.core form field structure
         * @param  {Object} record - A PMA.core form field value structure
         * @param  {string} cssClass - Extra CSS class to add to the container element
         * @returns {string} The HTML output
         */
        Forms.prototype.renderReadOnlyField = function (field, record, cssClass) {
            var strVal = (record && record.FormValue ? record.FormValue : '').replace(/"/g, "&quot;");

            if (strVal.length !== 0 && record && field.FieldType != Forms.FieldType.CheckBox && field.FormList && field.FormList.FormListValues && !record.IsOtherValue && !record.IsBelowDetectableLimit) {
                strVal = field.FormList.FormListValues[strVal].Value;
            }

            if (record && record.IsBelowDetectableLimit) {
                strVal = _("Below detectable limit");
            }

            var html = "";

            switch (field.FieldType) {
                case Forms.FieldType.Paragraph:
                    html += '<p class="' + cssClass + '">' + strVal + '</p>';
                    break;
                case Forms.FieldType.CheckBox:

                    if (field.FormList && field.FormList.FormListValues && record && !record.IsOtherValue && strVal.length !== 0 && !record.IsBelowDetectableLimit) {
                        var chkValues = strVal.split("|");
                        html += '<ul class="' + cssClass + '">';

                        for (k = 0; k < chkValues.length; k++) {
                            html += "<li>" + field.FormList.FormListValues[chkValues[k]].Value + "</li>";
                        }

                        html += '</ul>';
                    }
                    else {
                        html += '<div class="' + cssClass + '">' + strVal + '</div>';
                    }

                    break;
                case Forms.FieldType.Percentage:
                    html += '<div class="' + cssClass + '">' + strVal + '%</div>';
                    break;
                case Forms.FieldType.HyperLink:
                    html += '<a class="link-field" title="' + field.Tooltip + '" href="' + field.Url + '" ' + (field.NewWindow === true ? ' target="_blank" ' : '') + '>' + field.Label + '</a>';
                    break;
                case Forms.FieldType.Label:
                    break;
                default:
                    html += '<div class="' + cssClass + '">' + strVal + '</div>';
                    break;
            }

            return html;
        };

        /**
         * Passes a form definition and optionally form data back to the caller
         * @callback PMA.UI.Components.Forms~getFormCallback
         * @param {Object} form - The requested form definition
         * @param {Object} [data] - The submitted form data
         */

        /**
         * A callback to filter the data retrieved from the server
         * @callback PMA.UI.Components.Forms~filterDataCallback
         * @param {Object} [data] - The retrieved form data
         * @returns {Object} The filtered data
         */

        /**
         * Loads a form definition and optionally the available submitted data
         * @param  {String} serverUrl
         * @param  {Number} formId
         * @param  {Object} [dataOptions] - Used when it is desired to load submitted data as well
         * @param  {string} dataOptions.path - The path to load form submitted data for
         * @param  {boolean} [dataOptions.currentUserOnly=false] - True to load only the current user's data. The user is determined by the server's URL and the authentication providers available in the context.
         * @param  {string | PMA.UI.Components.Forms~filterDataCallback} [dataOptions.dataFilter=""] - Optional parameter to filter data results with. If the parameter is a string it is considered a username and data from this user are kept. This is useful when a particular user's data is desired, but not the currently logged on one's. If a callback function is provided it should return the filtered data
         * @param  {PMA.UI.Components.Forms~getFormCallback} [success]
         * @param  {function} [failure]
         */
        Forms.prototype.getForm = function (serverUrl, formId, dataOptions, success, failure) {
            var _this = this;

            _this.context.getSession(serverUrl, function (sessionId) {
                PMA.UI.Components.callApiMethod({
                    serverUrl: serverUrl,
                    method: PMA.UI.Components.ApiMethods.GetForm,
                    data: { sessionID: sessionId, id: formId },
                    success: function (http) {
                        var response = PMA.UI.Components.parseJson(http.responseText);
                        if (response) {
                            if (dataOptions !== null && dataOptions !== undefined && typeof dataOptions === "object") {
                                loadFormData.call(_this, serverUrl, sessionId, response, dataOptions.path, dataOptions.currentUserOnly === true, dataOptions.dataFilter,
                                    function (data) {
                                        response.FormData = data;
                                        if (typeof success === "function") {
                                            success.call(_this, response, data);
                                        }
                                    }, failure);
                            }
                            else if (typeof success === "function") {
                                success.call(_this, response, null);
                            }
                        }
                        else if (typeof failure === "function") {
                            failure();
                        }
                    },
                    failure: failure
                });
            }, failure);
        };

        /**
         * Passes form definitions back to the caller
         * @callback PMA.UI.Components.Forms~getFormsCallback
         * @param {Object[]} forms - Form definition objects fetched from PMA.core
         */

        /**
         * Loads all the available form definitions from a PMA.core server
         * @param  {String} serverUrl - The PMA.core server URL
         * @param  {PMA.UI.Components.Forms~getFormsCallback} success - Called upon successful completion of the data request.
         * @param  {function} [failure]
         */
        Forms.prototype.getForms = function (serverUrl, success, failure) {
            var _this = this;
            _this.context.getSession(serverUrl, function (sessionId) {
                PMA.UI.Components.callApiMethod({
                    serverUrl: serverUrl,
                    method: PMA.UI.Components.ApiMethods.GetForms,
                    data: { sessionID: sessionId },
                    success: function (http) {
                        var response = PMA.UI.Components.parseJson(http.responseText);
                        if (response && typeof success === "function") {
                            success(response);
                        }
                        else if (typeof failure === "function") {
                            failure(response);
                        }
                    },
                    failure: failure
                });
            }, failure);
        };

        /**
         * Called while rendering each field. This function gives the opportunity to the caller to make modifications per element, or return false to prevent rendering of specific fields.
         * @callback PMA.UI.Components.Forms~renderFieldCallback
         * @param {Object} form - The definition of the form that is being rendered
         * @param {Object} field - The definition of the field that is being rendered
         * @param {Object} field.fieldGroupClass - The CSS class to assign to the fields group element
         * @param {Object} field.fieldContainerClass - The CSS class to assign to the field's container
         * @param {Object} [record] - The data that will be displayed by the field. Can be null if no data is available or loaded.
         * @returns {boolean} True to render this field, otherwise false
         */

        /**
         * Renders a form and optionally loads the available user submitted data
         * @param  {string} serverUrl
         * @param  {Number} formId
         * @param {string|HTMLElement} element - The element that hosts the form. It can be either a valid CSS selector or an HTMLElement instance.
         * @param  {Object} [dataOptions] - Supplied only when it is desired to load form submitted data as well
         * @param  {string} dataOptions.path - The path to load form submitted data for
         * @param  {boolean} [dataOptions.currentUserOnly=false] - True to load only the current user's data. The user is determined by the server's URL and the authentication providers available in the context.
         * @param  {string | PMA.UI.Components.Forms~filterDataCallback} [dataOptions.dataFilter=""] - Optional parameter to filter data results with. If the parameter is a string it is considered a username and data from this user are kept. This is useful when a particular user's data is desired, but not the currently logged on one's. If a callback function is provided it should return the filtered data
         * @param  {string} [dataOptions.btnContainerClass] - CSS class to assign the to element that contains the save and reset buttons
         * @param  {string} [dataOptions.btnResetClass] - CSS class to assign the reset button
         * @param  {string} [dataOptions.btnSaveClass] - CSS class to assign the save button
         * @param  {PMA.UI.Components.Forms~renderFieldCallback} [dataOptions.fieldCb] - Called when rendering each field.
         * @param  {string} [dataOptions.fieldContainerClass] - CSS class to assign the to element that contains field controls
         * @param  {string} [dataOptions.fieldValidationClass] - CSS class to assign to error labels that are displayed upon validating the form
         * @param  {string} [dataOptions.formClass] - CSS class to assign to the rendered form element
         * @param  {string} [dataOptions.inputClass] - CSS class to assign the to input elements
         * @param  {boolean} [dataOptions.readOnly=false] - True to render the form in read only mode
         * @param  {boolean} [dataOptions.editButton=false] - True to render an edit button next to the form's name. Only applicable in read only mode
         * @param  {string} [dataOptions.validationClass] - CSS class to assign to generic validation messages (e.g. "The form could not be saved")
         * @param  {function} [success]
         * @param  {function} [failure]
         */
        Forms.prototype.displayForm = function (serverUrl, formId, element, dataOptions, success, failure) {
            var _this = this;

            this.getForm(serverUrl, formId, dataOptions,
                function (form, data) {
                    _this.serverUrl = serverUrl;

                    if (form) {
                        _this.form = form;
                        _this.form.enabled = true;

                        if (form.ReadOnly === true) {
                            dataOptions.readOnly = true;
                        }
                    }

                    _this.path = dataOptions.path;
                    _this.originalData = data;
                    renderForm.call(_this, element, form, dataOptions, data, success, failure);
                },
                failure);
        };

        /**
         * Renders a form and optionally loads the available user submitted data without loading from the server
         * @param {string|HTMLElement} element - The element that hosts the form. It can be either a valid CSS selector or an HTMLElement instance.
         * @param {Object} form - The form definition object
         * @param  {Object} [dataOptions] - Supplied only when it is desired to load form submitted data as well
         * @param  {string} dataOptions.path - The path to load form submitted data for
         * @param  {boolean} [dataOptions.currentUserOnly=false] - True to load only the current user's data. The user is determined by the server's URL and the authentication providers available in the context.
         * @param  {string | PMA.UI.Components.Forms~filterDataCallback} [dataOptions.dataFilter=""] - Optional parameter to filter data results with. If the parameter is a string it is considered a username and data from this user are kept. This is useful when a particular user's data is desired, but not the currently logged on one's. If a callback function is provided it should return the filtered data
         * @param  {string} [dataOptions.btnContainerClass] - CSS class to assign the to element that contains the save and reset buttons
         * @param  {string} [dataOptions.btnResetClass] - CSS class to assign the reset button
         * @param  {string} [dataOptions.btnSaveClass] - CSS class to assign the save button
         * @param  {PMA.UI.Components.Forms~renderFieldCallback} [dataOptions.fieldCb] - Called when rendering each field.
         * @param  {string} [dataOptions.fieldContainerClass] - CSS class to assign the to element that contains field controls
         * @param  {string} [dataOptions.fieldValidationClass] - CSS class to assign to error labels that are displayed upon validating the form
         * @param  {string} [dataOptions.formClass] - CSS class to assign to the rendered form element
         * @param  {string} [dataOptions.inputClass] - CSS class to assign the to input elements
         * @param  {boolean} [dataOptions.readOnly=false] - True to render the form in read only mode
         * @param  {boolean} [dataOptions.editButton=false] - True to render an edit button next to the form's name. Only applicable in read only mode
         * @param  {string} [dataOptions.validationClass] - CSS class to assign to generic validation messages (e.g. "The form could not be saved")
         * @param {Object} [data] - The data to load to the form displayed
         * @param  {function} [success]
         * @param  {function} [failure]
         */
        Forms.prototype.renderForm = function (element, form, dataOptions, data, success, failure) {
            if (form) {
                this.form = form;
                this.form.enabled = true;

                if (form.ReadOnly === true) {
                    dataOptions.readOnly = true;
                }
            }

            this.path = dataOptions.path;
            this.originalData = data;

            renderForm.call(this, element, form, dataOptions, data, success, failure);
        };

        /**
         * Saves the currently rendered form to PMA.core
         * @param  {function} [success]
         * @param  {function} [failure]
         * @fires PMA.UI.Components.Events#FormSaved
         */
        Forms.prototype.saveForm = function (success, failure) {
            var _this = this;
            _this.setEnabled(false);
            $("#" + _this.form.ClientID + " button[type='submit']").html(_("Saving..."));
            submitForm.call(this, function (args) {
                $("#" + _this.form.ClientID + " button[type='submit']").html(_("Save"));
                _this.setEnabled(true);
                _this.fireEvent(PMA.UI.Components.Events.FormSaved, args);
                if (typeof success === "function") {
                    success(args);
                }
            },
                function (args) {
                    $("#" + _this.form.ClientID + " button[type='submit']").html(_("Save"));
                    _this.setEnabled(true);
                    _this.fireEvent(PMA.UI.Components.Events.FormSaved, args);
                    if (typeof failure === "function") {
                        failure(args);
                    }
                });
        };

        /**
         * Enables or disables the currently rendered form
         * @param  {boolean} enabled
         */
        Forms.prototype.setEnabled = function (enabled) {
            if (this.form) {
                this.form.enabled = !!enabled;
                $("#" + this.form.ClientID + " > fieldset").prop("disabled", !this.form.enabled);
            }
            else {
                console.error("No form");
            }
        };

        /**
         * Gets the state of the currently rendered form
         * @return {boolean}
         */
        Forms.prototype.getEnabled = function () {
            if (this.form) {
                return this.form.enabled;
            }
            else {
                console.error("No form");
            }
        };

        /**
         * Attaches an event listener
         * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
         * @param {function} callback - The function to call when the event occurs
         */
        Forms.prototype.listen = function (eventName, callback) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " is not a valid event");
            }

            this.listeners[eventName].push(callback);
        };

        // fires an event
        Forms.prototype.fireEvent = function (eventName, eventArgs) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " does not exist");
                return;
            }

            for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
                this.listeners[eventName][i].call(this, eventArgs);
            }
        };

        /**
         * Checks if the currently loaded form has any changes that have not been saved yet.
         * @return {boolean}
         */
        Forms.prototype.hasChanges = function () {
            if (!this.form) {
                return false;
            }

            return this.hasChangesProperty;
        };

        /**
         * Resets the currently loaded form to it's initial state
         */
        Forms.prototype.reset = function () {
            if (!this.form) {
                return false;
            }

            $("#" + this.form.ClientID + " [type='reset']").click();
        };

        return Forms;
    })();
}(window.jQuery));
window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Components = window.PMA.UI.Components || {};

// namespace
(function (Ps, $) {
    var _ = PMA.UI.Resources.translate;

    // gallery class
    PMA.UI.Components.Gallery = (function () {
        /**
         * Available components to render
         * @readonly
         * @enum {string}
         * @memberof PMA.UI.Components
         */
        PMA.UI.Components.GalleryRenderOptions = {
            /** Render both thumbnail and barcode*/
            All: "all",
            /** Render thumbnail only */
            Thumbnail: "thumbnail",
            /** Render barcode only */
            Barcode: "barcode",
        };

        // find the currently visible images for horizontal scrolling and loads them
        function loadVisibleX() {
            var rail = $(this.element).find(".ps-scrollbar-x-rail");
            var left = -Infinity;
            var right = Infinity;

            if (rail && rail.position()) {
                left = rail.position().left;
                right = left + rail.width();
            }

            if (left === 0 && right === 0) {
                left = -Infinity;
                right = Infinity;
            }

            var self = this;

            $(this.element).find("li.lazy").each(function () {
                var el = $(this);
                var elLeft = el.position().left;
                var elRight = elLeft + el.width();
                if (!(elLeft > right || elRight < left)) {
                    loadImage.call(self, el);
                }
            });
        }

        // find the currently visible images for vertical scrolling and loads them
        function loadVisibleY() {
            var rail = $(this.element).find(".ps-scrollbar-y-rail");
            var top = -Infinity;
            var bottom = Infinity;

            if (rail && rail.position()) {
                top = rail.position().top;
                bottom = top + rail.height();
            }

            if (top === bottom) {
                top = -Infinity;
                bottom = Infinity;
            }

            var self = this;

            $(this.element).find("li.lazy").each(function () {
                var el = $(this);
                var elTop = el.position().top;
                var elBottom = elTop + el.height();

                if (!(elTop > bottom || elBottom < top)) {
                    loadImage.call(self, el);
                }
            });
        }

        // lazy loading background images
        function loadImage(li) {
            var self = this;
            li.removeClass("lazy");
            var div = li.find("div[data-img]");
            var src = div.data("img");
            var rot = div.data("rotation");

            $("<img />").bind("load", function () {
                li.removeClass("loading");
                div.css("background-image", "url(\"" + src + "\")");

                if (self.mode === "vertical") {
                    div.css("width", "100%");
                }
                else {
                    div.css("width", self.thumbnailWidth + "px");
                }

                div.css("height", self.thumbnailHeight + "px");
                div.css("margin", "0 auto");
                if (rot) {
                    var w = this.width;
                    var h = this.height;
                    var rad = rot / (180 / Math.PI);
                    var factorWidth = w / (w * Math.abs(Math.cos(rad)) + h * Math.abs(Math.sin(rad)));
                    var factorHeight = h / (w * Math.abs(Math.sin(rad)) + h * Math.abs(Math.cos(rad)));
                    div.css("transform", "rotate(" + rot + "deg) scale(" + Math.min(factorWidth, factorHeight) + ")");
                }
            }).bind("error", function () {
                li.removeClass("loading");
                div.addClass("no-image");
                div.html(_("Failed to load image"));

                if (self.mode === "vertical") {
                    div.css("width", "100%");
                }
                else {
                    div.css("width", self.thumbnailWidth + "px");
                }

                div.css("height", self.thumbnailHeight + "px");
                div.css("margin", "0 auto");
                div.css("padding-top", (self.thumbnailHeight / 3) + "px");

                var a = li.find("a");
                var serverUrl = a.data("server");
                var path = a.data("path");
                self.fireEvent(PMA.UI.Components.Events.SlideInfoError, { serverUrl: serverUrl, path: path });
            }).attr("src", src);

            // load barcode if it exists
            var barcodeImage = li.find("img.barcode");
            if (barcodeImage.length !== 0) {
                barcodeImage.bind("error", function () {
                    barcodeImage.hide();
                });

                barcodeImage.attr("src", barcodeImage.data("src"));
                barcodeImage.css("width", Math.round(self.thumbnailWidth * 0.4) + "px");
                barcodeImage.css("display", "block");
            }
        }

        function imageClick(element, fromUserInteraction) {
            var $el = $(element);

            var alreadySelected = $el.parent().hasClass("selected");
            if (alreadySelected) {
                $el.parent().removeClass("selected");
                this.fireEvent(PMA.UI.Components.Events.SlideDeSelected, { serverUrl: $el.data("server"), path: $el.data("path"), index: $el.parent().index(), userInteraction: fromUserInteraction === true });
            }

            if (!this.multiSelect) {
                $(this.element).find("ul li").removeClass("selected");
            }

            if (!alreadySelected) {
                $el.parent().addClass("selected");
                this.fireEvent(PMA.UI.Components.Events.SlideSelected, { serverUrl: $el.data("server"), path: $el.data("path"), index: $el.parent().index(), userInteraction: fromUserInteraction === true });
            }
        }

        function printMessage(message) {
            var style = " style=' ";
            if (this.thumbnailHeight > 0) {
                style += "height: " + this.thumbnailHeight + "px!important; ";
            }

            style += "' ";

            this.element.innerHTML = "<ul><li class='empty-message' " + style + "><span>" + message + "</span></li></ul>";
        }

        function renderImages(serverUrl, sessionId, images, doneCb, append) {
            var _this = this;
            if (images.length === 0) {
                printMessage.call(_this, _("No images found"));
                if (typeof doneCb === "function") {
                    doneCb();
                }

                return;
            }

            var showBarcodeNormal = this.renderOptions == PMA.UI.Components.GalleryRenderOptions.All;
            var showBarcodeOnly = this.renderOptions == PMA.UI.Components.GalleryRenderOptions.Barcode;

            var imagesObj = [];
            for (var i = 0; i < images.length; i++) {
                var img = { path: images[i], hasBarcode: true, rotation: 0 };

                if (typeof images[i] === 'object' && images[i].hasOwnProperty('path')) {
                    img.path = images[i].path;
                }

                if (typeof images[i] === 'object' && images[i].hasOwnProperty('rotation')) {
                    img.rotation = images[i].rotation;
                }

                if (typeof images[i] === 'object' && images[i].hasOwnProperty('snapshotParameters')) {
                    img.snapshotParameters = images[i].snapshotParameters;
                }

                img.hasBarcode = true;
                imagesObj.push(img);
            }

            if (showBarcodeNormal || showBarcodeOnly) {
                this.context.getImagesInfo({
                    serverUrl: serverUrl, images: imagesObj.map(function (r) { return r.path; }), success: function (sessionId, imagesInfo) {
                        for (var i = 0; i < imagesInfo.length; i++) {
                            var hasBarcode = imagesInfo[i].AssociatedImageTypes.indexOf("Barcode") > -1;
                            for (var j = 0; j < imagesObj.length; j++) {
                                if (imagesObj[j].path == imagesInfo[i].Filename) {
                                    imagesObj[j].hasBarcode = hasBarcode;
                                    break;
                                }
                            }
                        }

                        continueRenderImages.call(_this, serverUrl, sessionId, imagesObj, doneCb, append);
                    },
                    failure: function () {
                        continueRenderImages.call(_this, serverUrl, sessionId, imagesObj, doneCb, append);
                    }
                });
            }
            else {
                continueRenderImages.call(_this, serverUrl, sessionId, imagesObj, doneCb, append);
            }
        }

        function continueRenderImages(serverUrl, sessionId, imagesObj, doneCb, append) {
            var _this = this;

            var showBarcodeNormal = this.renderOptions == PMA.UI.Components.GalleryRenderOptions.All;
            var showBarcodeOnly = this.renderOptions == PMA.UI.Components.GalleryRenderOptions.Barcode;

            var loadingImgMarginX = Math.floor(_this.thumbnailWidth / 3);
            var loadingImgMarginY = Math.floor(_this.thumbnailHeight / 3);
            var imageStyle = "margin: " + loadingImgMarginY + "px " + (_this.mode === "horizontal" || _this.mode === "grid" ? loadingImgMarginX + "px" : "auto") + "; width: " + loadingImgMarginX + "px; height: " + loadingImgMarginY + "px;";
            var aStyle = "";
            var listStyle = "";
            var liStyle = "";
            var tw = _this.thumbnailWidth,
                th = _this.thumbnailHeight;

            if (_this.mode === "horizontal") {
                listStyle = " style='width: " + (imagesObj.length * _this.thumbnailWidth) + "px; height: " + _this.thumbnailHeight + "px; overflow: hidden;' ";
                aStyle = " style='max-width: " + _this.thumbnailWidth + "px;' ";
                tw = 0;
            }
            else if (_this.mode === "vertical") {
                th = 0;
            }
            else if (_this.mode === "grid") {
                // aStyle = " style='width: " + (_this.thumbnailWidth + 32) + "px;height: " + (_this.thumbnailHeight + 32) + "px;' ";
                aStyle = " style='width: auto;height: " + (_this.thumbnailHeight + 32) + "px;' ";
                liStyle = " style='width: " + (_this.thumbnailWidth + 32) + "px;' ";
                tw = 0;
            }

            var html = "<ul" + listStyle + ">";

            $(_this.element).find("ul li.emptyli").remove();
            if (append === true) {
                html = "";
            }

            for (var i = 0; i < imagesObj.length; i++) {
                var path = imagesObj[i].path;
                var extrastyle = '';
                var extradata = '';

                var thumbUrl = PMA.UI.Components.getThumbnailUrl(serverUrl, sessionId, path, 0, tw, th);

                if (imagesObj[i].hasOwnProperty('snapshotParameters')) {
                    thumbUrl = PMA.UI.Components.getSnapshotUrl(serverUrl, sessionId, path, imagesObj[i].snapshotParameters, tw, th, "jpg");
                }
                else if (imagesObj[i].hasOwnProperty('rotation')) {
                    var rInt = parseInt(imagesObj[i].rotation);
                    if (!isNaN(rInt) && rInt != 0) {
                        extrastyle = 'transform:rotate(' + rInt + 'deg);';
                        extradata = 'data-rotation="' + rInt + '"';
                    }
                }

                var barcodeUrl = PMA.UI.Components.getBarcodeUrl(serverUrl, sessionId, path, _this.barcodeRotation ? _this.barcodeRotation : 0);

                html +=
                    "<li draggable='true' class='lazy loading'" + liStyle + "><a " + aStyle + " data-server='" + serverUrl + "' data-path='" + path + "' href='#'>";
                if (_this.showFileName) {
                    if (typeof _this.filenameCallback === "function") {
                        html += "<span>" + _this.filenameCallback(serverUrl, path) + "</span>";
                    }
                    else {
                        html += "<span>" + path.split('/').pop() + "</span>";
                    }
                }

                html += "<div " + extradata + " data-img='" + (showBarcodeOnly ? barcodeUrl : thumbUrl) + "' style=' " + imageStyle + extrastyle + "'></div>";

                if (showBarcodeNormal && imagesObj[i].hasBarcode) {
                    html += '<img class="barcode ' + (showBarcodeNormal ? "" : "hidden") + ' " data-src="' + barcodeUrl + '" />';
                }

                html += '</a>';

                if (typeof _this.additionalHtmlCallback === "function") {
                    html += _this.additionalHtmlCallback(imagesObj[i]);
                }

                html += '</li>';
            }

            if (this.mode === "grid") {
                for (var p = 0; p < 10; p++) {
                    html += "<li class='emptyli' " + liStyle + " ></li>";
                }
            }

            if (append !== true) {
                html += "</ul>";
            }

            if (append === true) {
                $(_this.element).find("ul").append(html);
            }
            else {
                _this.element.innerHTML = html;
            }

            $(_this.element).find("ul li:not(.emptyli)").each(function (index, element) {
                element.addEventListener("dragstart", dragstart.bind(this, element), false);
            });

            if (_this.mode === "horizontal") {
                // fix width once loaded
                var horUl = $(_this.element).find("ul");
                var horWidth = horUl.outerWidth(true) - horUl.width();

                $(_this.element).find("ul li").each(function () {
                    horWidth += $(this).outerWidth(true) + 2;
                });

                horUl.css("width", horWidth + "px");
                horUl.css("height", "");
                horUl.css("overflow", "");
            }

            $(_this.element).find("ul li a").click(function (ev) {
                ev.preventDefault();
                imageClick.call(_this, this, true);
            });

            if (append !== true) {
                Ps.initialize(_this.element, { useBothWheelAxes: true, wheelPropagation: true, swipePropagation: true });
            }
            else {
                Ps.update(_this.element);
            }

            // bind scroll events for lazy loading
            if (_this.mode === "horizontal") {
                $(document).on('ps-scroll-x', function () {
                    clearTimeout(_this.lazyLoadTimeOut);
                    _this.lazyLoadTimeOut = setTimeout(loadVisibleX.bind(_this), 500);
                });

                loadVisibleX.call(_this);
            }
            else {
                $(document).on('ps-scroll-y', function () {
                    clearTimeout(_this.lazyLoadTimeOut);
                    _this.lazyLoadTimeOut = setTimeout(loadVisibleY.bind(_this), 500);
                });

                loadVisibleY.call(_this);
            }

            if (typeof doneCb === "function") {
                doneCb();
            }
        }
        /**
         * A private function which load slides from one server only
         * @param  {string} serverUrl - The URL of the PMA.core server to get images from
         * @param  {string[]|Object[]} images - An array of strings that contains the paths of the images to load or an array of objects that contains the path and rotation of the images as desribed below
         * @param  {string} images.path - The path of the image to load
         * @param  {string} images.rotation - The rotation of the image in degrees
         * @param  {PMA.UI.Components~snapshotParameters} images.snapshotParameters - Optional snapshot parameters to show 
         * @param  {function} [doneCb] - Called when image loading is complete
         */
        function loadSlides(serverUrl, images, doneCb) {
            Ps.destroy(this.element);
            var _this = this;
            _this.context.getSession(serverUrl, function (sessionId) {
                renderImages.call(_this, serverUrl, sessionId, images, doneCb);
            });
        }

        function refresh() {
            if (this.lastLoadedImages.length > 0) {
                for (var i = 0; i < this.lastLoadedImages.length; i++) {
                    renderImages.call(this, this.lastLoadedImages[i].serverUrl, this.lastLoadedImages[i].sessionId, this.lastLoadedImages[i].images, null);
                }
            }
        }

        function ondragover(ev) {
            ev.preventDefault();
            var types = ev.dataTransfer.types;
            if (types) {
                if (types.indexOf) {
                    hasData = types.indexOf("application/x-fancytree-node") > -1 || types.indexOf(PMA.UI.Components.DragDropMimeType) > -1;
                }
                else if (types.contains) {
                    // in IE and EDGE types is DOMStringList
                    hasData = types.contains("application/x-fancytree-node") || types.contains(PMA.UI.Components.DragDropMimeType);
                }

                // var nodeData = PMA.UI.Components.parseDragData(ev.dataTransfer);
                if (hasData) {
                    if (ev.altKey) {
                        ev.dataTransfer.dropEffect = "move";
                    }
                    else {
                        ev.dataTransfer.dropEffect = "copy";
                    }

                    return;
                }

                ev.dataTransfer.dropEffect = "none";
            }
        }

        function ondrop(ev) {
            ev.preventDefault();
            var self = this;
            var nodeData = PMA.UI.Components.parseDragData(ev.dataTransfer);
            var append = ev.altKey == false;
            if (nodeData && nodeData.path && nodeData.serverUrl && nodeData.source !== "gallery") {
                if (nodeData.isFolder) {
                    this.context.getSlides({
                        serverUrl: nodeData.serverUrl,
                        path: nodeData.path,
                        success: function (sessionId, files) {
                            if (files == null || files.length == 0) {
                                self.fireEvent(PMA.UI.Components.Events.Dropped, { serverUrl: nodeData.serverUrl, path: nodeData.path, isFolder: true, append: append && self.lastLoadedImages.length > 1 });
                                return;
                            }
                           
                            self.lastLoadedImages.push([{ serverUrl: nodeData.serverUrl, sessionId: sessionId, images: files }]);
                            renderImages.call(self, nodeData.serverUrl, sessionId, files, function () {
                                self.fireEvent(PMA.UI.Components.Events.Dropped, { serverUrl: nodeData.serverUrl, path: nodeData.path, isFolder: true, append: append && self.lastLoadedImages.length > 1 });
                            }, append && self.lastLoadedImages.length > 1);
                        },
                        failure: function (error) {
                            console.error("Error loading slides from directory");
                        }
                    });
                }
                else {
                    var imageArray = [{ serverUrl: nodeData.serverUrl, path: nodeData.path }];

                    self.context.getSession(nodeData.serverUrl, function (sessionId) {
                        self.lastLoadedImages.push({ serverUrl: nodeData.serverUrl, sessionId: sessionId, images: imageArray });
                        renderImages.call(self, nodeData.serverUrl, sessionId, imageArray, function () {
                            self.fireEvent(PMA.UI.Components.Events.Dropped, { serverUrl: nodeData.serverUrl, path: nodeData.path, isFolder: false, append: append && self.lastLoadedImages.length > 1 });
                        }, append && self.lastLoadedImages.length > 1);
                    });

                    return;
                }
            }
        }

        function initializeDropZone() {
            if (this.element) {
                this.element.addEventListener("drop", ondrop.bind(this), false);
                this.element.addEventListener("dragover", ondragover.bind(this), false);
            }
        }

        function dragstart(element, ev) {
            var link = $(element).find("a");
            if (link) {
                var d = JSON.stringify({
                    serverUrl: link.data("server"),
                    path: link.data("path"),
                    isFolder: false,
                    source: "gallery"
                });

                ev.dataTransfer.setData("text", d);
                ev.dataTransfer.setData(PMA.UI.Components.DragDropMimeType, d);
            }
        }

        /**
         * Function that returns a string to be displayed on top of a thumbnail
         * @callback PMA.UI.Components.Gallery~callback
         * @param {string} serverUrl - The serverUrl of the image
         * @param {string} filename - The virtual path of the image
         * @returns {string}
         */

        /**
         * Function that returns a string to be displayed on below a thumbnail
         * @callback PMA.UI.Components.Gallery~additionalHtmlCallback
         * @param {object} image - The image object used to render this thumbnail
         * @returns {string}
         */

        /**
         * Represents a UI component that shows image thumbnails. Provides events to handle click and multiple selection, as well as built-in lazy loading functionality.
         * @param  {PMA.UI.Components.Context} context
         * @param  {object} options - Configuration options
         * @param {string|HTMLElement} options.element - The element that hosts the gallery. It can be either a valid CSS selector or an HTMLElement instance.
         * @param {Number} options.thumbnailWidth - The desired width of the displayed thumbnails.
         * @param {Number} options.thumbnailHeight - The desired height of the displayed thumbnails.
         * @param {string} [options.mode="horizontal"] - "horizontal", "vertical" or "grid"
         * @param {PMA.UI.Components.Gallery~callback} [options.filenameCallback] - Callback to override the displayed name of each image.
         * @param {PMA.UI.Components.Gallery~additionalHtmlCallback} [options.additionalHtmlCallback] - Callback to provide additional HTML to render on top of the thumbnail.
         * @param {boolean} [options.showFileName=false] - Whether or not to print each image's name
         * @param {PMA.UI.Components.GalleryRenderOptions} [options.renderOptions = PMA.UI.Components.GalleryRenderOptions.All] - Whether to render thumbnail only, barcode only or both
         * @param {Number} [options.barcodeRotation=0] - Rotation of the barcode in steps of 90 degrees
         * @param {boolean} [options.multiSelect=false] - Whether or not to allow multiple files to be selected
         * @fires PMA.UI.Components.Events#SlideInfoError
         * @fires PMA.UI.Components.Events#SlideSelected
         * @fires PMA.UI.Components.Events#SlideDeSelected
         * @fires PMA.UI.Components.Events#Dropped
         * @constructor
         * @memberof PMA.UI.Components
         * @tutorial 03-gallery
         * @tutorial 04-tree
         */
        function Gallery(context, options) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            if (options.element instanceof HTMLElement) {
                this.element = options.element;
            }
            else if (typeof options.element == "string") {
                var el = document.querySelector(options.element);
                if (!el) {
                    console.error("Invalid selector for element");
                }
                else {
                    this.element = el;
                }
            }
            else {
                console.error("Invalid element");
                return;
            }

            if (isNaN(options.thumbnailHeight) || isNaN(options.thumbnailWidth) || options.thumbnailHeight <= 0 || options.thumbnailWidth <= 0) {
                console.error("thumbnailWidth & thumbnailHeight must be positive integers");
                return;
            }

            this.listeners = {};
            this.listeners[PMA.UI.Components.Events.SlideDeSelected] = [];
            this.listeners[PMA.UI.Components.Events.SlideSelected] = [];
            this.listeners[PMA.UI.Components.Events.SlideInfoError] = [];
            this.listeners[PMA.UI.Components.Events.Dropped] = [];

            this.filenameCallback = options.filenameCallback;
            this.additionalHtmlCallback = options.additionalHtmlCallback;
            this.multiSelect = options.multiSelect === true;
            this.lazyLoadTimeOut = 0;
            this.showFileName = options.showFileName === true;
            this.barcodeRotation = options.barcodeRotation;
            this.context = context;
            this.thumbnailWidth = options.thumbnailWidth;
            this.thumbnailHeight = options.thumbnailHeight;

            // a helper array that holds the last loaded images as objects  { serverUrl, sessionId, imageArray }
            this.lastLoadedImages = [];

            if (options.mode !== "horizontal" && options.mode !== "vertical" && options.mode !== "grid") {
                options.mode = "horizontal";
            }

            this.renderOptions = options.renderOptions ? options.renderOptions : PMA.UI.Components.GalleryRenderOptions.All;

            // for backwards compatibility keep the showBarcode option
            if (options.showBarcode === false) {
                this.renderOptions = PMA.UI.Components.GalleryRenderOptions.Thumbnail;
            }
            this.mode = options.mode;

            $(this.element).addClass("pma-ui-gallery");
            if (this.mode === "vertical") {
                $(this.element).addClass("vertical");
            }
            else if (this.mode === "grid") {
                $(this.element).addClass("grid");
            }

            printMessage.call(this, _("No images found"));

            initializeDropZone.call(this);
        }

        /**
         * Attaches an event listener
         * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
         * @param {function} callback - The function to call when the event occurs
         */
        Gallery.prototype.listen = function (eventName, callback) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " is not a valid event");
            }

            this.listeners[eventName].push(callback);
        };

        // fires an event
        Gallery.prototype.fireEvent = function (eventName, eventArgs) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " does not exist");
                return;
            }

            for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
                this.listeners[eventName][i].call(this, eventArgs);
            }
        };

        /**
         * Loads the thumbnails of all the images found directly under a given directory
         * @param  {string} serverUrl - The URL of the PMA.core server to get images from
         * @param  {string} directory - The path of a directory to load images from
         * @param  {function} [doneCb] - Called when image loading is complete
         */
        Gallery.prototype.loadDirectory = function (serverUrl, directory, doneCb) {
            Ps.destroy(this.element);
            var _this = this;
            this.lastLoadedImages = [];

            this.context.getSlides({
                serverUrl: serverUrl,
                path: directory,
                success: function (sessionId, files) {
                    _this.lastLoadedImages = [{ serverUrl: serverUrl, sessionId: sessionId, images: files }];
                    renderImages.call(_this, serverUrl, sessionId, files, doneCb);
                },
                failure: function (error) {
                    _this.element.innerHTML = error.Message;
                }
            });
        };

        /**
         * Loads the thumbnails of all the provided images
         * @param {Object[]} images - An array of image objects containing the path, rotation and server url for each image to load
         * @param {string} images.serverUrl - The URL of the PMA.core server to load this image from
         * @param {string} images.path - The path of the image to load
         * @param {string} images.rotation - The rotation of the image in degrees
         * @param {function} doneCb - Called when image loading is complete
         */
        Gallery.prototype.loadSlides = function (images, doneCb) {
            if (typeof images === "string") {
                // the old deprecated function is used if the first parameter is of type string, corresponding to the server url
                // loadSlides = function(serverUrl, images, doneCb)
                loadSlides.call(this, arguments[0], arguments[1], arguments[2]);
                return;
            }

            Ps.destroy(this.element);
            var servers = {};
            var serverUrls = [];
            var _this = this;

            if (!images || images.length == 0) {
                // call with empty parameters to clear all 
                renderImages.call(this, "", "", [], doneCb);
                return;
            }

            for (var i = 0; i < images.length; i++) {
                if (images[i].serverUrl) {
                    if (!servers.hasOwnProperty(images[i].serverUrl)) {
                        servers[images[i].serverUrl] = [];
                        serverUrls.push(images[i].serverUrl);
                    }

                    servers[images[i].serverUrl].push({ path: images[i].path, rotation: images[i].rotation });
                }
            }

            _this.lastLoadedImages = [];
            var c = 0;
            var getSessionFunc = (function (serverUrl, imageArray, gallery, cb, count, done) {
                gallery.context.getSession(serverUrl, function (sessionId) {
                    _this.lastLoadedImages.push({ serverUrl: serverUrl, sessionId: sessionId, images: imageArray });
                    renderImages.call(gallery, serverUrl, sessionId, imageArray, function () {
                        if (done && typeof cb === "function") {
                            cb();
                        }
                    }, count != 0);
                });
            });

            for (i = 0; i < serverUrls.length; i++) {
                getSessionFunc(serverUrls[i], servers[serverUrls[i]], _this, doneCb, c, ++c >= serverUrls.length);
            }
        };

        /**
         * Selects or deselects a slide
         * @param  {Number} index - The index of the slide to select
         * @fires PMA.UI.Components.Events#SlideSelected
         * @fires PMA.UI.Components.Events#SlideDeSelected
         */
        Gallery.prototype.selectSlide = function (index) {
            if (index === undefined || index === null) {
                $(this.element).find("ul li").removeClass("selected");
                return;
            }

            var el = $(this.element).find("ul li:nth-child(" + (index + 1) + ") a")[0];
            imageClick.call(this, el, false);
        };

        /**
         * Highlights or unhighlights a slide
         * @param  {Number} index - The index of the slide to highlight
         * @param  {boolean} highlight - True to highlight, otherwise false
         */
        Gallery.prototype.highlightSlide = function (index, highlight) {
            if (index === undefined || index === null) {
                $(this.element).find("ul li").removeClass("selected");
                return;
            }

            var el = $(this.element).find("ul li:nth-child(" + (index + 1) + ") a");
            if (highlight === true) {
                el.parent().addClass("selected");
            }
            else {
                el.parent().removeClass("selected");
            }
        };

        /**
         * Slide information
         * @typedef {Object} PMA.UI.Components.Gallery~slide
         * @property {string} server - The URL of the PMA.core server this slide has been loaded from
         * @property {string} path - The path of the slide
         * @property {Number} index - The index of the slide in the gallery
         */

        /**
         * Returns the first of the currently selected slides, or null
         * @return {PMA.UI.Components.Gallery~slide}
         */
        Gallery.prototype.getSelectedSlide = function () {
            var el = $(this.element).find("ul li.selected a:first-child");
            if (el.length === 0) {
                return null;
            }
            else {
                return { server: el.data("server"), path: el.data("path"), index: el.parent().index() };
            }
        };

        /**
         * Returns the currently selected slides, or null
         * @return {PMA.UI.Components.Gallery~slide[]}
         */
        Gallery.prototype.getSelectedSlides = function () {
            var el = $(this.element).find("ul li.selected a:first-child");
            if (el.length === 0) {
                return null;
            }
            else {
                var result = [];
                el.each(function () {
                    result.push({ server: $(this).data("server"), path: $(this).data("path"), index: $(this).parent().index() });
                });

                return result;
            }
        };

        /**
         * Returns all the currently loaded slides
         * @return {PMA.UI.Components.Gallery~slide[]}
         */
        Gallery.prototype.getSlides = function () {
            var el = $(this.element).find("ul li a:first-child");
            if (el.length === 0) {
                return null;
            }
            else {
                var result = [];
                el.each(function () {
                    result.push({ server: $(this).data("server"), path: $(this).data("path") });
                });

                return result;
            }
        };

        /**
         * Sets the render options
         * @param {PMA.UI.Components.GalleryRenderOptions} option - The render option to set
         */
        Gallery.prototype.setRenderOptions = function (option) {
            if (option && this.renderOptions != option) {
                this.renderOptions = option;
                refresh.call(this);
            }
        };

        /**
         * Toggles the mode of the gallery
         * @param {String} mode - The mode to set, one of  "horizontal", "vertical" or "grid"
         */
        Gallery.prototype.setMode = function (mode) {
            if (this.mode !== mode) {
                this.mode = mode;
                $(this.element).removeClass("vertical grid");
                if (this.mode === "vertical") {
                    $(this.element).addClass("vertical");
                }
                else if (this.mode === "grid") {
                    $(this.element).addClass("grid");
                }
                refresh.call(this);
            }
        };

        return Gallery;
    })();
}(window.PerfectScrollbar, window.jQuery));
window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Components = window.PMA.UI.Components || {};

// namespace
(function () {
    var _ = PMA.UI.Resources.translate;

    PMA.UI.Components.MetadataSearch = (function () {
        /**
         * Represents a component that searches in form meta data for slides. It has a visual representation as a text field and also provides programmatic methods to search.
         * @param {PMA.UI.Components.Context} context
         * @param {object} [options=null] - Configuration options. If not provided, the component has no visual representation.
         * @param {string|HTMLElement} options.element - The container in which an input field will be added to function as a search box. It can be either a valid CSS selector or an HTMLElement instance. 
         * @fires PMA.UI.Components.Events#SearchStarted
         * @fires PMA.UI.Components.Events#SearchFinished
         * @constructor
         * @memberof PMA.UI.Components
         * @tutorial 08-meta-search
         */
        function MetadataSearch(context, options) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            if (!options) {
                options = {};
            }

            this.context = context;
            this.servers = {};
            this.currentFocus = null;
            this.searchTimeout = 0;

            if (options.element instanceof HTMLElement) {
                this.element = options.element;
            }
            else if (typeof options.element == "string") {
                var el = document.querySelector(options.element);
                if (!el) {
                    console.error("Invalid selector for element");
                }
                else {
                    this.element = el;
                }
            }
            else {
                this.element = null;
            }

            this.listeners = {};
            this.listeners[PMA.UI.Components.Events.SearchStarted] = [];
            this.listeners[PMA.UI.Components.Events.SearchFinished] = [];
            this.options = options;
            this.query = "";
            this.position = 0;

            if (this.element) {
                initializeTextBox.call(this);
            }
        }

        /**
         * Attaches an event listener
         * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
         * @param {function} callback - The function to call when the event occurs
         */
        MetadataSearch.prototype.listen = function (eventName, callback) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " is not a valid event");
            }

            this.listeners[eventName].push(callback);
        };

        // fires an event
        MetadataSearch.prototype.fireEvent = function (eventName, eventArgs) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " does not exist");
                return;
            }

            for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
                this.listeners[eventName][i].call(this, eventArgs);
            }
        };

        function initServer(serverUrl) {
            var self = this;
            if (self.inputBox) {
                self.inputBox.readonly = true;
            }

            return new Promise(function (resolve, reject) {
                self.context.getVersionInfo(serverUrl, function (version) {
                    if (!version || version.startsWith("1.")) {
                        reject();
                        return;
                    }

                    self.context.getSession(serverUrl, function (sessionId) {
                        self.servers[serverUrl] = self.servers[serverUrl] || {};
                        self.servers[serverUrl].sessionId = sessionId;

                        loadForms.call(self, serverUrl, function () {
                            if (self.inputBox) {
                                self.inputBox.readonly = false;
                            }

                            resolve(sessionId);
                        }, reject);

                    }, reject);
                }, reject);
            });
        }

        function loadForms(serverUrl, resolve, reject) {
            if (this.servers[serverUrl].forms) {
                resolve();
                return;
            }

            var self = this;
            this.context.getFormDefinitions(serverUrl, [], "", function (sessionId, definitions) {
                self.servers[serverUrl].forms = definitions; // .filter(function (x) { return !x.ReadOnly; });
                self.servers[serverUrl].forms.push({ FormID: -1, FormName: "Slide", FormFields: [{ FieldID: 0, Label: "Path" }, { FieldID: 1, Label: "Barcode" }/*, { FieldID: 0, Label: "Extension" }*/] });
                resolve();
            }, reject);
        }

        /*************** Parsing & searching *************/

        /**
         * Searches for slides in meta data entries
         * @param {string} serverUrl - The url of the PMA.core search to search against
         * @param {string} queryString - The query to search with
         * @returns {Promise}
         */
        MetadataSearch.prototype.search = function (serverUrl, queryString) {
            var self = this;

            var searchToken = (new Date()).getTime();
            self.searchToken = searchToken;
            return initServer.call(this, serverUrl).
                then(function () {
                    return parse.call(self, queryString);
                }).
                then(function (query) {
                    if (query.expressions.length === 0) {
                        return getFormSuggestions.call(self, serverUrl, query);
                    }

                    return getSuggestions.call(self, serverUrl, query);
                }).
                then(function (processedQuery) {
                    if (!processedQuery || self.searchToken !== searchToken) {
                        return null;
                    }

                    var searchTerms = {
                        "sessionID": self.servers[serverUrl].sessionId,
                        "expressions": []
                    };

                    for (var i = 0; i < processedQuery.expressions.length; i++) {
                        var expr = processedQuery.expressions[i];
                        if (!isExpressionValid.call(self, expr)) {
                            continue;
                        }

                        var form = findForm.call(self, serverUrl, expr[0].value);
                        if (!form) {
                            continue;
                        }

                        var field = findField.call(self, form, expr[1].value);

                        if (!field) {
                            continue;
                        }

                        searchTerms.expressions.push({
                            "FormID": form.FormID,
                            "FieldID": field.FieldID,
                            "Operator": 0,
                            "Value": field.FormList ? listLabelToID(field.FormList.FormListValues, expr[3].value) : expr[3].value
                        });
                    }

                    if (self.searchToken === searchToken) {
                        if (searchTerms.expressions.length === 0) {
                            return { serverUrl: serverUrl, query: processedQuery, results: [] };
                        }

                        var searchTermsJson = JSON.stringify(searchTerms);

                        if (self.lastSearchTerms == searchTermsJson) {
                            return { serverUrl: serverUrl, query: processedQuery, results: self.lastSearchResults.slice() };
                        }

                        self.lastSearchTerms = "";
                        self.lastSearchResults = [];
                        return searchWithDelay.call(self, serverUrl, processedQuery, searchTerms, searchToken);
                    }
                });
        };

        function searchWithDelay(serverUrl, processedQuery, searchTerms, searchToken) {
            var self = this;
            clearTimeout(self.searchTimeout);

            var searchTermsJson = JSON.stringify(searchTerms);

            var slideSearchExpressions = searchTerms.expressions.filter(function (item) {
                return item.FormID === -1 && item.FieldID === 0;
            });

            var searchFilesOnly = slideSearchExpressions.length > 0 && slideSearchExpressions.length === searchTerms.expressions.length;

            var metaSearchQuery = searchTerms;

            return new Promise(function (resolve) {
                self.searchTimeout = setTimeout(function () {
                    self.fireEvent(PMA.UI.Components.Events.SearchStarted, { serverUrl: serverUrl, queryString: processedQuery.getText() });

                    if (searchFilesOnly) {
                        searchPaths.call(self, serverUrl, slideSearchExpressions).
                            then(function (pathResults) {
                                var results = [];
                                if (pathResults instanceof Array) {
                                    results = pathResults;
                                }

                                if (self.searchToken !== searchToken) {
                                    return;
                                }

                                self.lastSearchTerms = searchTermsJson;
                                self.lastSearchResults = results;
                                self.fireEvent(PMA.UI.Components.Events.SearchFinished, results);
                                resolve({ serverUrl: serverUrl, query: processedQuery, results: results });
                            });
                    }
                    else {
                        fetch(serverUrl + "query/json/Metadata", {
                            method: "POST",
                            mode: "cors",
                            credentials: "same-origin",
                            headers: { "Content-Type": "application/json; charset=utf-8" },
                            body: JSON.stringify(metaSearchQuery)
                        }).
                            then(function (response) {
                                if (response.ok) {
                                    return response.json();
                                }
                                else {
                                    return [];
                                }
                            }).
                            then(function (metaResults) {
                                if (self.searchToken !== searchToken) {
                                    return;
                                }

                                self.lastSearchTerms = searchTermsJson;
                                self.lastSearchResults = metaResults;

                                self.fireEvent(PMA.UI.Components.Events.SearchFinished, metaResults);
                                resolve({ serverUrl: serverUrl, query: processedQuery, results: metaResults });
                            });
                    }
                }, 1000);
            });
        }

        function searchPaths(serverUrl, expressions) {
            var self = this;

            if (!expressions || expressions.length === 0) {
                return Promise.resolve(null);
            }

            return new Promise(function (resolve, reject) {
                var q = expressions.map(function (item) { return item.Value; }).join("|");

                self.context.queryFilename(serverUrl, "", q, function (sessionId, results) { resolve(results); }, reject);
            });
        }

        function intersect(a, b) {
            var t;
            if (b.length > a.length) {
                // indexOf to loop over shorter
                t = b;
                b = a;
                a = t;
            }

            return a.filter(function (e) {
                return b.indexOf(e) > -1;
            }).
                filter(function (e, i, c) { // extra step to remove duplicates
                    return c.indexOf(e) === i;
                });
        }

        function parse(queryString) {
            if (typeof (queryString) !== "string") {
                throw "Query must be a string";
            }

            this.query = queryString;
            this.position = 0;

            var tokens = [];

            while (canRead.call(this)) {
                skipWhitespace.call(this);

                var t = parseToken.call(this);
                if (t) {
                    tokens.push(t);
                }
            }

            var query = {
                getText: function () {
                    if (!this.expressions || this.expressions.length === 0) {
                        return "";
                    }

                    var result = "";

                    for (var i = 0; i < this.expressions.length; i++) {
                        if (i > 0) {
                            result += ",";
                        }

                        var expr = this.expressions[i];

                        for (var t = 0; t < expr.length; t++) {
                            if (result.length > 0) {
                                result += " ";
                            }

                            if (expr[t].quoted) {
                                result += '"';
                            }

                            result += expr[t].value;

                            if (expr[t].quoted) {
                                result += '"';
                            }
                        }
                    }

                    return result;
                },
                getLastToken: function () {
                    if (!this.expressions || this.expressions.length === 0) {
                        return null;
                    }

                    var lastExpr = this.expressions[this.expressions.length - 1];
                    if (lastExpr.length === 0) {
                        return null;
                    }

                    return lastExpr[lastExpr.length - 1];
                },
                expressions: [],
                suggestions: []
            };

            var i = 0;
            while (i < tokens.length) {
                if (i <= tokens.length - 4 &&
                    tokens[i + 0].type === tokenTypeEnum.literal &&
                    tokens[i + 1].type === tokenTypeEnum.literal &&
                    tokens[i + 2].operator &&
                    tokens[i + 3].type === tokenTypeEnum.literal) {

                    query.expressions.push([
                        tokens[i + 0],
                        tokens[i + 1],
                        tokens[i + 2],
                        tokens[i + 3]
                    ]);

                    i += 4;
                }
                else if (i === tokens.length - 3 &&
                    tokens[i + 0].type === tokenTypeEnum.literal &&
                    tokens[i + 1].type === tokenTypeEnum.literal &&
                    tokens[i + 2].operator) {

                    query.expressions.push([
                        tokens[i + 0],
                        tokens[i + 1],
                        tokens[i + 2]
                    ]);

                    i += 3;
                }
                else if (i === tokens.length - 2 &&
                    tokens[i + 0].type === tokenTypeEnum.literal &&
                    tokens[i + 1].type === tokenTypeEnum.literal) {

                    query.expressions.push([
                        tokens[i + 0],
                        tokens[i + 1]
                    ]);

                    i += 2;
                }
                else if (tokens[i].type === tokenTypeEnum.literal) {
                    query.expressions.push([tokens[i]]);

                    i++;
                }
                else {
                    i++;
                }
            }

            return new Promise(function (resolve, reject) { resolve(query); });
        }

        function canRead() {
            return this.position < this.query.length;
        }

        function readChar() {
            if (canRead.call(this)) {
                return this.query[this.position++];
            }
            else {
                return "";
            }
        }

        function peekChar() {
            if (canRead.call(this)) {
                return this.query[this.position];
            }
            else {
                return "";
            }
        }

        function isExpressionValid(expression) {
            return expression &&
                expression.length === 4 &&
                expression[0].type === tokenTypeEnum.literal &&
                expression[1].type === tokenTypeEnum.literal &&
                expression[2].operator &&
                expression[3].type === tokenTypeEnum.literal;
        }

        function isTokenComplete(token) {
            if (!token || (!token.quoted && isWhitespace.call(this, this.query[this.query.length - 1]))) {
                return true;
            }
            else if (token.quoted) {
                var trimmed = this.query.trimRight();
                if (trimmed.length >= 2 && trimmed[trimmed.length - 1] === '"' && trimmed[trimmed.length - 1] === '\\') {
                    return false;
                }
                else if (trimmed[trimmed.length - 1] === '"') {
                    return true;
                }
            }

            return false;
        }

        function getSuggestions(serverUrl, processedQuery) {
            var lastExpression = processedQuery.expressions[processedQuery.expressions.length - 1];

            if (lastExpression.length === 1) {
                // either suggest forms or fields
                if (isTokenComplete.call(this, lastExpression[0])) {
                    return getFieldSuggestions.call(this, serverUrl, processedQuery, lastExpression[0]);
                }
                else {
                    return getFormSuggestions.call(this, serverUrl, processedQuery);
                }
            }
            else if (lastExpression.length === 2) {
                // either suggest fields or operators
                if (isTokenComplete.call(this, lastExpression[1])) {
                    return getOperatorSuggestions.call(this, processedQuery);
                }
                else {
                    return getFieldSuggestions.call(this, serverUrl, processedQuery, lastExpression[0]);
                }
            }
            else if (lastExpression.length === 3) {
                // either suggest operators or values
                if (isTokenComplete.call(this, lastExpression[2])) {
                    return getValueSuggestions.call(this, serverUrl, processedQuery, lastExpression[0], lastExpression[1]);
                }
                else {
                    return getOperatorSuggestions.call(this, processedQuery);
                }
            }
            else {
                // either suggest forms or fields
                if (isTokenComplete.call(this, lastExpression[3])) {
                    return getFormSuggestions.call(this, serverUrl, processedQuery);
                }
                else {
                    return getValueSuggestions.call(this, serverUrl, processedQuery, lastExpression[0], lastExpression[1]);
                }
            }
        }

        function findForm(serverUrl, name) {
            if (!this.servers[serverUrl] || !this.servers[serverUrl].forms) {
                return null;
            }

            return this.servers[serverUrl].forms.find(function (x) { return x.FormName.localeCompare(name, {}, { sensitivity: "base" }) === 0; });
        }

        function findField(form, name) {
            if (!form) {
                return null;
            }

            return form.FormFields.find(function (x) { return x.Label.localeCompare(name, {}, { sensitivity: "base" }) === 0; });
        }

        function getFormSuggestions(serverUrl, processedQuery) {
            processedQuery.suggestions = this.servers[serverUrl].forms.map(function (x) { return x.FormName; });

            return new Promise(function (resolve, reject) {
                resolve(processedQuery);
            });
        }

        function getFieldSuggestions(serverUrl, processedQuery, form) {
            var formObj = findForm.call(this, serverUrl, form.value);
            if (formObj) {
                processedQuery.suggestions = formObj.FormFields.map(function (x) { return x.Label; });
            }

            return new Promise(function (resolve, reject) {
                resolve(processedQuery);
            });
        }

        function getOperatorSuggestions(processedQuery) {
            var self = this;
            return new Promise(function (resolve, reject) {
                processedQuery.suggestions = getOperators.call(self);
                resolve(processedQuery);
            });
        }

        function listLabelToID(list, label) {
            var result = [];
            for (var j = 0; j < list.length; j++) {
                if (list[j].Value == label) {
                    return list[j].ValueID;
                }
            }

            return "";
        }

        function valuesToListLabels(list, values) {
            var result = [];
            var i, j;
            for (i = 0; i < values.length; i++) {
                for (j = 0; j < list.length; j++) {
                    if (list[j].ValueID == values[i]) {
                        result.push(list[j].Value);
                        break;
                    }
                }
            }

            return result;
        }

        function getValueSuggestions(serverUrl, processedQuery, form, field) {
            var self = this;

            var formObj = findForm.call(this, serverUrl, form.value);
            if (!formObj) {
                return new Promise(function (resolve, reject) {
                    resolve(processedQuery);
                });
            }

            var fieldObject = formObj.FormFields.find(function (x) { return x.Label.localeCompare(field.value, {}, { sensitivity: "base" }) === 0; });
            if (!fieldObject) {
                return new Promise(function (resolve, reject) {
                    resolve(processedQuery);
                });
            }

            if (fieldObject.values) {
                processedQuery.suggestions = fieldObject.values;

                return new Promise(function (resolve, reject) {
                    resolve(processedQuery);
                });
            }

            return fetch(serverUrl + "query/json/DistinctValues?formID=" + formObj.FormID + "&fieldID=" + fieldObject.FieldID + "&sessionID=" + self.servers[serverUrl].sessionId, {
                method: "GET",
                mode: "cors",
                credentials: "same-origin",
                headers: {
                    "Content-Type": "application/json; charset=utf-8"
                }
            }).
                then(function (response) {
                    if (response.ok) {
                        return response.json();
                    }
                    else {
                        return [];
                    }
                }).
                then(function (values) {
                    if (fieldObject.FormList) {
                        fieldObject.values = valuesToListLabels(fieldObject.FormList.FormListValues, values);
                    }
                    else {
                        fieldObject.values = values.map(function (x) { return x + ""; });
                    }

                    processedQuery.suggestions = fieldObject.values;
                    return processedQuery;
                });
        }

        function getOperators() {
            return ["="];
            ////return ["=", ">", "<>", "<", ">=", "<=", "~="];
        }

        function isWhitespace(char) {
            return [' ', ','].indexOf(char) !== -1;
        }

        function skipWhitespace() {
            while (isWhitespace.call(this, peekChar.call(this))) {
                readChar.call(this);
            }
        }

        function isOperatorChar(char) {
            return ['=', '>', '<', '~'].indexOf(char) !== -1;
        }

        function isTokenChar(char) {
            return !isWhitespace.call(this, char) && !isOperatorChar.call(this, char);
        }

        function parseToken() {
            if (!canRead.call(this)) {
                return null;
            }

            var tokenChars = [];

            var isQuoted = false;
            if (peekChar.call(this) === '"') {
                readChar.call(this);
                isQuoted = true;
            }
            else if (isOperatorChar.call(this, peekChar.call(this))) {
                return parseOperator.call(this);
            }

            while (canRead.call(this)) {
                var c = readChar.call(this);

                if (isQuoted) {
                    if (c === "\\" && peekChar.call(this) === '"') {
                        tokenChars.push(readChar.call(this));
                        continue;
                    }
                    else if (c === '"') {
                        break;
                    }
                }
                else if (!isTokenChar.call(this, c)) {
                    this.position--;
                    break;
                }

                tokenChars.push(c);
            }

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

            return {
                value: tokenChars.join(""),
                type: tokenTypeEnum.literal,
                operator: false,
                quoted: isQuoted
            };
        }

        function parseOperator() {
            var tokenChars = [];
            while (canRead.call(this)) {
                if (isOperatorChar.call(this, peekChar.call(this))) {
                    tokenChars.push(readChar.call(this));
                }
                else {
                    break;
                }
            }

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

            var tokenString = tokenChars.join("");
            var tokenType = tokenTypeEnum.literal;
            switch (tokenString) {
                case "=":
                    tokenType = tokenTypeEnum.equalOperator;
                    break;
                case "<>":
                    tokenType = tokenTypeEnum.differentOperator;
                    break;
                case ">":
                    tokenType = tokenTypeEnum.greaterThanOperator;
                    break;
                case ">=":
                    tokenType = tokenTypeEnum.greaterThanOrEqualToOperator;
                    break;
                case "<":
                    tokenType = tokenTypeEnum.lessThanOperator;
                    break;
                case "<=":
                    tokenType = tokenTypeEnum.lessThanOrEqualToOperator;
                    break;
                case "~=":
                    tokenType = tokenTypeEnum.similarOperator;
                    break;
                default:
                    throw "Unknown operator";
            }

            return {
                value: tokenString,
                type: tokenType,
                operator: true,
                quoted: false
            };
        }

        tokenTypeEnum = {
            literal: "literal",
            equalOperator: "equal to",
            greaterThanOperator: "greater than",
            greaterThanOrEqualToOperator: "greater than or equal to",
            lessThanOperator: "less than",
            lessThanOrEqualToOperator: "less than or equal to",
            differentOperator: "different than",
            similarOperator: "similar to"
        };

        /*************** End of parsing & searching *************/

        /*************** Autocomplete UI *************/
        function initializeTextBox() {
            this.element.classList.add("pma-ui-meta-data-search");
            this.inputBox = document.createElement("input", { type: "text", autocomplete: "off" });
            this.element.appendChild(this.inputBox);
            this.inputBox.addEventListener("input", inputInput.bind(this));
            this.inputBox.addEventListener("keydown", inputKeyDown.bind(this));
            this.inputBox.addEventListener("focus", inputInput.bind(this));

            var self = this;
            document.addEventListener("click", function (e) {
                closeAllLists.call(self, e.target);
            });

            var context = new PMA.UI.Components.Context({ caller: "" });
            context.getVersionInfo(
                this.options.serverUrl,
                function (version) {
                    if (version != null && version.startsWith("2.0")) {
                        //self.inputBox.disabled = false;
                    }
                    else {
                        console.warn("Metadata search only works with PMA.core version 2.x. Version reported: " + version);
                    }
                },
                function () {
                    console.warn("Cannot reach PMA.core server. Metadata search only works with PMA.core version 2.x.");
                });
        }

        function inputInput(e) {
            var self = this;
            var val = self.inputBox.value;

            closeAllLists.call(self);

            self.search(self.options.serverUrl, val).
                then(function (result) {
                    showSuggestions.call(self, result.query);
                },
                    function () {
                    });
        }

        function showSuggestions(query) {
            var a, b, i;
            a = document.createElement("DIV");
            a.setAttribute("class", "autocomplete-items");
            var self = this;

            this.element.appendChild(a);

            var arr = query.suggestions;
            var lastToken = query.getLastToken();
            var val = "";
            if (lastToken && !isTokenComplete.call(self, lastToken)) {
                val = lastToken.value;
            }

            function itemSelected() {
                var itemValue = this.getElementsByTagName("input")[0].value;
                if (lastToken && !isTokenComplete.call(self, lastToken)) {
                    lastToken.value = itemValue;
                    lastToken.quoted = lastToken.value.indexOf(' ') !== -1;
                    self.inputBox.value = query.getText();
                }
                else {
                    self.inputBox.value = query.getText() + " " + (itemValue.indexOf(' ') !== -1 ? '"' + itemValue + '"' : itemValue);
                }

                self.search(self.options.serverUrl, self.inputBox.value);
                closeAllLists.call(self);
            }

            var io;
            for (i = 0; i < arr.length; i++) {
                if (val.length === 0) {
                    b = document.createElement("DIV");
                    b.innerHTML = arr[i];
                }
                else {
                    io = arr[i].toUpperCase().indexOf(val.toUpperCase());
                    if (io === -1) {
                        continue;
                    }

                    b = document.createElement("DIV");

                    // make the matching letters bold
                    b.innerHTML = arr[i].substr(0, io);
                    b.innerHTML += "<strong>" + arr[i].substr(io, val.length) + "</strong>";
                    b.innerHTML += arr[i].substr(io + val.length);
                }

                // insert a input field that will hold the current array item's value
                b.innerHTML += "<input type='hidden' value='" + arr[i] + "'>";

                b.addEventListener("click", itemSelected);

                a.appendChild(b);
            }
        }

        function inputKeyDown(e) {
            var x = this.element.querySelectorAll(".autocomplete-items div");
            if (x.length === 0) {
                return;
            }

            var curActive = this.element.querySelector(".autocomplete-active");
            var index = -1;
            if (curActive) {
                curActive.classList.remove("autocomplete-active");
                for (var count = 0; count < x.length; count++) {
                    if (x[count] === curActive) {
                        index = count;
                        break;
                    }
                }
            }

            if (e.keyCode == 40) {
                index++;
                index = index >= x.length ? 0 : index;
                x[index].classList.add("autocomplete-active");
            }
            else if (e.keyCode == 38) {
                index--;
                index = index < 0 ? x.length - 1 : index;
                x[index].classList.add("autocomplete-active");
            }
            else if (e.keyCode == 13) {
                e.preventDefault();

                if (index !== -1) {
                    x[index].click();
                }

                closeAllLists.call(this);
            }
            else if (e.keyCode == 27) {
                closeAllLists.call(this);
            }
        }

        function closeAllLists(element) {
            var x = document.getElementsByClassName("autocomplete-items");
            for (var i = 0; i < x.length; i++) {
                if (element != x[i] && element != this.inputBox) {
                    x[i].parentNode.removeChild(x[i]);
                }
            }
        }

        /*************** End of Autocomplete UI *************/

        return MetadataSearch;
    })();
}());

window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Components = window.PMA.UI.Components || {};

// namespace
(function ($) {
    var _ = PMA.UI.Resources.translate;

    // meta data tree view class
    PMA.UI.Components.MetadataTree = (function () {
        var nodeTypes = {
            ServerNode: "servernode",
            FormNode: "formnode",
            FieldNode: "fieldnode",
            ValueNode: "valuenode",
            SlideNode: "slidenode",
        };

        function loadServerNode(forms, serverIndex) {
            var result = [];
            for (var i = 0; i < forms.length; i++) {
                var form = forms[i];
                var formNode = {
                    title: form.FormName,
                    type: nodeTypes.FormNode,
                    lazy: false,
                    serverIndex: serverIndex,
                    folder: true,
                    formId: form.FormID,
                    children: [],
                };

                for (var j = 0; j < form.FormFields.length; j++) {
                    var field = form.FormFields[j];
                    if (this.showAllFields || field.FormList != null) {
                        formNode.children.push({
                            title: field.Label,
                            type: nodeTypes.FieldNode,
                            fieldId: field.FieldID,
                            formId: form.FormID,
                            formListValues: field.FormList ? field.FormList.FormListValues : null,
                            lazy: true,
                            serverIndex: serverIndex,
                            folder: true,
                        });
                    }
                }

                if (formNode.children.length > 0) {
                    result.push(formNode);
                }

            }

            return result;
        }

        function loadNode(event, data) {
            var dfd = new $.Deferred();
            data.result = dfd.promise();

            var node = data.node;
            var _this = this;
            var serverIndex = node.data.serverIndex;

            if (node.data.type == nodeTypes.ServerNode) {
                this.context.getFormDefinitions(_this.servers[serverIndex].url, [], "", function (sessionId, forms) {
                    var result = loadServerNode.call(_this, forms, serverIndex);
                    dfd.resolve(result);
                },
                    function (error) {
                        dfd.reject(error.Message ? error.Message : _("Error loading forms"));
                    });
            }
            else if (node.data.type == nodeTypes.FieldNode) {
                this.context.distinctValues({
                    serverUrl: _this.servers[serverIndex].url,
                    formId: node.data.formId,
                    fieldId: node.data.fieldId,
                    success: function (sessionId, values) {
                        var result = [];
                        for (var i = 0; i < values.length; i++) {
                            var label = values[i];
                            if (node.data.formListValues) {
                                for (var v = 0; v < node.data.formListValues.length; v++) {
                                    if (node.data.formListValues[v].ValueID == label) {
                                        label = node.data.formListValues[v].Value;
                                        break;
                                    }
                                }
                            }

                            result.push({
                                title: label,
                                type: nodeTypes.ValueNode,
                                formId: node.data.formId,
                                fieldId: node.data.fieldId,
                                value: values[i],
                                lazy: true,
                                serverIndex: serverIndex,
                                folder: true,
                            });
                        }

                        dfd.resolve(result);
                    },
                    failure: function (error) {
                        dfd.reject(error.Message ? error.Message : _("Error loading field"));
                    }
                });
            }
            else if (node.data.type == nodeTypes.ValueNode) {
                this.context.metadata({
                    serverUrl: _this.servers[serverIndex].url,
                    expressions: [{
                        FormID: node.data.formId,
                        FieldID: node.data.fieldId,
                        Operator: 0,
                        Value: node.data.value
                    }],
                    success: function (sessionId, slides) {
                        var result = [];
                        for (var i = 0; i < slides.length; i++) {
                            var slide = slides[i];
                            result.push({
                                title: slide,
                                type: nodeTypes.SlideNode,
                                formId: node.data.formId,
                                fieldId: node.data.fieldId,
                                path: slide,
                                lazy: false,
                                serverIndex: serverIndex,
                                folder: false,
                            });
                        }

                        dfd.resolve(result);
                    },
                    failure: function (error) {
                        dfd.reject(error.Message ? error.Message : _("Error loading field"));
                    }
                });
            }
        }

        function serverVersionResult(server, version) {
            server.version = version;
        }

        /**
       * Represents a UI component that shows a tree view that allows browsing through the forms and their values submitted from multiple PMA.core servers. This component uses {@link https://github.com/mar10/fancytree|fancytree} under the hood.
       * @param  {PMA.UI.Components.Context} context
       * @param  {object} options - Configuration options
       * @param  {PMA.UI.Components.Tree~server[]} options.servers An array of servers to show files from
       * @param {string|HTMLElement} options.element - The element that hosts the tree view. It can be either a valid CSS selector or an HTMLElement instance.
       * @param  {function} [options.renderNode] - Allows tweaking after node state was rendered
       * @param  {PMA.UI.Components.Tree~rootDirSortCb} [options.rootDirSortCb] - Function that sorts an array of directories
       * @param  {boolean} [options.autoExpandNodes=false] - Whether the tree should expand nodes on single click
       * @param  {boolean} [options.showAllFields=false] - Whether to show all fields or list fields only
       * @constructor
       * @memberof PMA.UI.Components
       * @fires PMA.UI.Components.Events#ValueExpanded
       * @fires PMA.UI.Components.Events#SlideSelected
       */
        function MetadataTree(context, options) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            if (options.element instanceof HTMLElement) {
                this.element = element;
            }
            else if (typeof options.element == "string") {
                var el = document.querySelector(options.element);
                if (!el) {
                    console.error("Invalid selector for element");
                }
                else {
                    this.element = el;
                }
            }

            this.context = context;
            this.servers = options.servers || [];
            var _this = this;

            this.autoExpand = options.autoExpandNodes === true;
            this.lastSearchResults = {};
            this.showAllFields = options.showAllFields === true ? true : false;
            this.listeners = {};
            this.listeners[PMA.UI.Components.Events.ValueExpanded] = [];
            this.listeners[PMA.UI.Components.Events.SlideSelected] = [];

            if (typeof options.rootDirSortCb === "function") {
                this.rootDirSortCb = options.rootDirSortCb;
            }

            var sourceData = [];
            for (var i = 0; i < this.servers.length; i++) {
                sourceData.push({
                    title: this.servers[i].name,
                    type: nodeTypes.ServerNode,
                    serverNode: true,
                    key: this.servers[i].url,
                    serverIndex: i,
                    extraClasses: "server",
                    dirPath: "/",
                    lazy: true,
                    unselectableStatus: false,
                    unselectable: true,
                    selected: false,
                });

                // try get version for each server
                this.context.getVersionInfo(this.servers[i].url, serverVersionResult.bind(this, this.servers[i]));
            }

            var tree = $(this.element).fancytree({
                keyPathSeparator: "?",
                extensions: [],
                selectMode: 3,
                toggleEffect: { effect: "drop", options: { direction: "left" }, duration: 400 },
                wide: {
                    iconWidth: "1em", // Adjust this if @fancy-icon-width != "16px"
                    iconSpacing: "0.5em", // Adjust this if @fancy-icon-spacing != "3px"
                    levelOfs: "1.5em" // Adjust this if ul padding != "16px"
                },
                icon: function (event, data) {
                    switch (data.node.data.type) {
                        case nodeTypes.ServerNode:
                            return "server";
                        case nodeTypes.FormNode:
                            return "fa fa-table";
                        case nodeTypes.SlideNode:
                            return "image";
                        case nodeTypes.FieldNode:
                            return "fa fa-tag";
                        case nodeTypes.ValueNode:
                            return "fa fa-code";
                        default:
                            return "fa fa-table";
                    }
                },
                renderNode: (typeof options.renderNode === "function" ? options.renderNode : null),
                source: sourceData,
                lazyLoad: loadNode.bind(this),
                activate: function (event, data) {
                    // A node was activated:
                    var node = data.node;
                    if (_this.autoExpand === true) {
                        node.setExpanded(true);
                    }

                    if (node.data.type == nodeTypes.SlideNode) {
                        _this.fireEvent(PMA.UI.Components.Events.SlideSelected, { serverUrl: _this.servers[node.data.serverIndex].url, path: node.data.path });
                    }
                },
                select: function (event, data) {
                    var n = data.tree.getSelectedNodes();
                    var selectionArray = [];
                },
                expand: function (event, data) {
                    var node = data.node;

                    if (node.data.type == nodeTypes.ValueNode) {
                        var slides = [];
                        if (node.children && node.children.length > 0) {
                            for (var i = 0; i < node.children.length; i++) {
                                slides.push(node.children[i].data.path);
                            }
                        }

                        _this.fireEvent(PMA.UI.Components.Events.ValueExpanded, { serverUrl: _this.servers[node.data.serverIndex].url, slides: slides });
                    }

                },
                dblclick: function (event, data) {
                    var node = data.node;
                }
            });
        }

        /**
        * Adds a new server to the tree
        * @param  {PMA.UI.Components.Tree~server} server A server object
        */
        MetadataTree.prototype.addServer = function (server) {
            if (server) {
                this.servers.push(server);

                var serverInfo = {
                    title: this.servers[this.servers.length - 1].name,
                    serverNode: true,
                    key: this.servers[this.servers.length - 1].url,
                    serverIndex: this.servers.length - 1,
                    extraClasses: "server",
                    dirPath: "/",
                    lazy: true,
                    unselectableStatus: false,
                    unselectable: true,
                    selected: false,
                };

                // try get version for server
                this.context.getVersionInfo(server.url, serverVersionResult.bind(this, server));
                $(this.element).fancytree("getRootNode").addChildren(serverInfo);
            }
        };

        /**
         * Removes a server from the tree
         * @param {number} index The index of the server to remove
         */
        MetadataTree.prototype.removeServer = function (index) {
            var children = $(this.element).fancytree("getRootNode").getChildren();
            if (children && children.length && index >= 0 && index < children.length) {
                children[index].remove();
            }
            else {
                console.error("No children found or index out of range");
            }
        };

        /**
       * Attaches an event listener
       * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
       * @param {function} callback - The function to call when the event occurs
       */
        MetadataTree.prototype.listen = function (eventName, callback) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " is not a valid event");
            }

            this.listeners[eventName].push(callback);
        };

        // fires an event
        MetadataTree.prototype.fireEvent = function (eventName, eventArgs) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " does not exist");
                return;
            }

            for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
                this.listeners[eventName][i].call(this, eventArgs);
            }
        };

        return MetadataTree;
    })();
}(window.jQuery));
/**
 * PMA.UI.Authentication contains UI and utility components that authenticate against PMA.core
 * @namespace PMA.UI.Authentication
 */

window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Authentication = window.PMA.UI.Authentication || {};

// namespace
(function ($) {
    var _ = PMA.UI.Resources.translate;

    // prompt login class
    PMA.UI.Authentication.PromptLogin = (function () {

        function loginClick(serverUrl, attempts) {
            var self = this;

            PMA.UI.Components.login(
                serverUrl,
                $("div.pma-ui-loginprompt input[type=text]").val(),
                $("div.pma-ui-loginprompt input[type=password]").val(),
                this.context.getCaller(),
                function (sessionId) {
                    for (var i = 0; i < self.successCallbacks.length; i++) {
                        self.successCallbacks[i](sessionId);
                    }

                    self.active = false;
                    $("div.pma-ui-loginprompt").remove();
                },
                function (error) {
                    if (!error.Message && error.Reason) {
                        error.Message = error.Reason;
                    }

                    if (attempts < 2) {
                        $("div.pma-ui-loginprompt .error").html(error.Message);
                    }
                    else {
                        self.active = false;
                        for (var i = 0; i < self.failureCallbacks.length; i++) {
                            self.failureCallbacks[i](error);
                        }

                        $("div.pma-ui-loginprompt").remove();
                    }
                });
        }
        /**
        * The callback function used to authenticate to a server url
        * @callback PMA.UI.Authentication.PromptLogin~serverNameCallback
        * @param  {string} serverUrl - The URL of the PMA.core server to find a friendly name for
        * @returns {string} A friendly name for the specified server url if found or null
        */

        /**
         * Authenticates against a PMA.core server by prompting the user to enter credentials. Upon creation, the instance will register itself automatically as an authentication provider for the given context.
         * @param  {PMA.UI.Components.Context} context
         * @param {PMA.UI.Authentication.PromptLogin~serverNameCallback} serverNameCallback - A callback that takes a serverUrl as parameter and returns a frienly name for that server
         * @constructor
         * @memberof PMA.UI.Authentication
         * @tutorial 04-tree
         */
        function PromptLogin(context, serverNameCallback) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            this.context = context;
            this.context.registerAuthenticationProvider(this);
            this.active = false;
            this.serverNameCallback = serverNameCallback;
        }

        /**
         * Authenticates against a PMA.core server. This method is usually invoked by the context itself and should rarely be used outside it.
         * @param  {string} serverUrl - The URL of the PMA.core server to authenticate against
         * @param  {function} [success]
         * @param  {function} [failure]
         * @returns {boolean} - True or false depending on whether this instance has enough information to authenticate against this PMA.core server
         */
        PromptLogin.prototype.authenticate = function (serverUrl, success, failure) {
            if (!this.active) {
                this.successCallbacks = [];
                this.failureCallbacks = [];

                if (typeof success === "function") {
                    this.successCallbacks.push(success);
                }

                if (typeof failure === "function") {
                    this.failureCallbacks.push(failure);
                }

                this.active = true;
                var self = this;
                var attempts = 0;

                var name = serverUrl;
                if (typeof this.serverNameCallback === "function") {
                    var r = this.serverNameCallback(serverUrl);
                    if (r && typeof r === "string") {
                        name = r;
                    }
                }

                $("body").append("<div class='pma-ui-loginprompt'>" +
                    "<div class='login-panel'>" +
                    "<div>" +
                    "<label>" + name + "</label>" +
                    "</div>" +
                    "<div>" +
                    "<input type='text' placeholder='" + _("Username") + "' />" +
                    "</div>" +
                    "<div>" +
                    "<input type='password' placeholder='" + _("Password") + "' />" +
                    "</div>" +
                    "<div class='error'></div>" +
                    "<div class='controls'>" +
                    "<input type='button' class='accept' value='" + _("OK") + "' />" +
                    "<input type='button' class='cancel' value='" + _("Cancel") + "' />" +
                    "</div>" +
                    "</div>" +
                    "</div>");

                $("div.pma-ui-loginprompt input[type=text], div.pma-ui-loginprompt input[type=password]").keypress(function (e) {
                    if (e.which == 13) {
                        e.preventDefault();
                        loginClick.call(self, serverUrl, attempts++);
                    }
                });

                $("div.pma-ui-loginprompt input.accept").click(function () {
                    loginClick.call(self, serverUrl, attempts++);
                });

                $("div.pma-ui-loginprompt input.cancel").click(function () {
                    self.active = false;
                    for (var i = 0; i < self.failureCallbacks.length; i++) {
                        self.failureCallbacks[i]({ Message: _("Login cancelled") });
                    }

                    $("div.pma-ui-loginprompt").remove();
                });
            }
            else {
                if (typeof success === "function") {
                    this.successCallbacks.push(success);
                }

                if (typeof failure === "function") {
                    this.failureCallbacks.push(failure);
                }
            }

            return true;
        };

        return PromptLogin;
    })();
}(window.jQuery));
window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Authentication = window.PMA.UI.Authentication || {};

// namespace
(function () {

    PMA.UI.Authentication.SessionLogin = (function () {
        /**
         * Holds a session ID per PMA.core server URL. Used to skip authentication, if a session ID is already available.
         * @typedef {Object} PMA.UI.Authentication.SessionLogin~serverSessions
         * @property {string} serverUrl
         * @property {string} sessionId
         */

        /**
         * Authenticates against a PMA.core server without user interaction. Upon creation, the instance will register itself automatically as an authentication provider for the given context.
         * @param  {PMA.UI.Components.Context} context
         * @param  {PMA.UI.Authentication.SessionLogin~serverSessions[]} serverSessions - Array of PMA.core server sessions.
         * @constructor
         * @memberof PMA.UI.Authentication
         */
        function SessionLogin(context, serverSessions) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            if (!(serverSessions instanceof Array)) {
                console.error("Expected array of server credentials");
                return;
            }

            this.serverSessions = serverSessions;
            this.context = context;
            this.context.registerAuthenticationProvider(this);
        }

        /**
         * Authenticates against a PMA.core server. This method is usually invoked by the context itself and should rarely be used outside it.
         * @param  {string} serverUrl - The URL of the PMA.core server to authenticate against
         * @param  {function} [success]
         * @param  {function} [failure]
         * @returns {boolean} True or false depending on whether this instance has enough information to authenticate against this PMA.core server
         */
        SessionLogin.prototype.authenticate = function (serverUrl, success, failure) {
            for (var i = 0, max = this.serverSessions.length; i < max; i++) {
                if (this.serverSessions[i].serverUrl === serverUrl) {
                    success(this.serverSessions[i].sessionId);
                    return true;
                }
            }

            // Requested server is not handled by this provider
            return false;
        };

        /** 
         * Updates the session id of a specified server
         * @param {string} serverUrl - The URL of the PMA.core server to authenticate against
         * @param {string} sessionId - The sessionId to update
        */
        SessionLogin.prototype.updateSessionId = function (serverUrl, sessionId) {
            for (var i = 0, max = this.serverSessions.length; i < max; i++) {
                if (this.serverSessions[i].serverUrl === serverUrl) {
                    this.serverSessions[i].sessionId = sessionId;
                    break;
                }
            }
        };

        return SessionLogin;
    })();
}());
window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Components = window.PMA.UI.Components || {};

// namespace
(function () {
    function clone(obj) {
        if (null === obj || "object" != typeof obj) {
            return obj;
        }

        var copy = obj.constructor();
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) {
                copy[attr] = obj[attr];
            }
        }

        return copy;
    }

    function checkReloadImage(serverUrl, path, doneCb, dropped) {
        if (!this.lastLoadImageRequest) {
            return;
        }

        if (this.lastLoadImageRequest.serverUrl !== serverUrl || this.lastLoadImageRequest.path !== path || this.lastLoadImageRequest.doneCb !== doneCb) {
            var ref = this.lastLoadImageRequest;
            this.lastLoadImageRequest = null;

            this.load(ref.serverUrl, ref.path, ref.doneCb, ref.dropped);
        }
    }

    function onDragOver(ev) {
        ev.preventDefault();
        var types = ev.dataTransfer.types;
        if (types) {
            var hasData = false;
            if (types.indexOf) {
                hasData = types.indexOf("application/x-fancytree-node") > -1 || types.indexOf(PMA.UI.Components.DragDropMimeType) > -1;
            }
            else if (types.contains) {
                // in IE and EDGE types is DOMStringList
                hasData = types.contains("application/x-fancytree-node") || types.contains(PMA.UI.Components.DragDropMimeType);
            }

            if (hasData) {
                ev.dataTransfer.dropEffect = "link";
                return;
            }
        }

        ev.dataTransfer.dropEffect = "none";
    }

    function onDrop(ev) {
        ev.preventDefault();
        var nodeData = PMA.UI.Components.parseDragData(ev.dataTransfer);
        if (nodeData && !nodeData.isFolder && nodeData.path && nodeData.serverUrl) {
            var cancels = this.fireEvent(PMA.UI.Components.Events.BeforeDrop, { serverUrl: nodeData.serverUrl, path: nodeData.path, node: nodeData });
            if (cancels.filter(function (c) { return c == false; }).length == 0) {
                this.load(nodeData.serverUrl, nodeData.path, null, true);
            }
        }
    }

    function initializeDropZone() {
        if (this.element) {
            this.element.addEventListener("drop", onDrop.bind(this), false);
            this.element.addEventListener("dragover", onDragOver.bind(this), false);
        }
    }

    PMA.UI.Components.SlideLoader = (function () {
        /**
         * Helper class that wraps around the {@link PMA.UI.View.Viewport} class. It's purpose is mainly to automatically handle slide reloading and authentication, via the provided {@link PMA.UI.Components.Context} instance.
         * @param {PMA.UI.Components.Context} context
         * @param {Object} slideLoaderOptions - Initialization options passed to each {@link PMA.UI.View.Viewport} that is created during a {@link PMA.UI.Components.SlideLoader#load} call. This is the same struct as the one accepted by the {@link PMA.UI.View.Viewport} constructor, omitting server URLs, credentials and specific slide paths. The omitted information is either available via the {@link PMA.UI.Components.Context} instance, or supplied during the {@link PMA.UI.Components.SlideLoader#load} call.
         * @param {string|HTMLElement} slideLoaderOptions.element - The element that hosts the viewer. It can be either a valid CSS selector or an HTMLElement instance
         * @param {string} slideLoaderOptions.image - The path or UID of the image to load
         * @param {Number} [slideLoaderOptions.keyboardPanFactor=0.5] - A factor to calculate pan delta when pressing a keyboard arrow. The actual pan in pixels is calculated as keyboardPanFactor * viewportWidth.
         * @param {PMA.UI.View.Themes} [slideLoaderOptions.theme="default"] - The theme to use
         * @param {boolean|Object} [slideLoaderOptions.overview=true] - Whether or not to display an overview map
         * @param {boolean} [slideLoaderOptions.overview.collapsed] - Whether or not to start the overview in collapsed state
         * @param {boolean|Object} [slideLoaderOptions.dimensions=true] - Whether or not to display the dimensions selector for images that have more than one channel, z-stack or timeframe
         * @param {boolean} [slideLoaderOptions.dimensions.collapsed] - Whether or not to start the dimensions selector in collapsed state
         * @param {boolean|Object} [slideLoaderOptions.barcode=false] - Whether or not to display the image's barcode if it exists
         * @param {boolean} [slideLoaderOptions.barcode.collapsed=undefined] - Whether or not to start the barcode in collapsed state
         * @param {Number} [slideLoaderOptions.barcode.rotation=undefined] - Rotation in steps of 90 degrees
         * @param {boolean|Object} [slideLoaderOptions.loadingBar=true] - Whether or not to display a loading bar while the image is loading
         * @param {PMA.UI.View.Viewport~position} [slideLoaderOptions.position] - The initial position of the viewport within the image
         * @param {boolean} [slideLoaderOptions.snapshot=false] - Whether or not to display a button that generates a snapshot image
         * @param {PMA.UI.View.Viewport~annotationOptions} [slideLoaderOptions.annotations] - Annotation options
         * @param {Number} [slideLoaderOptions.digitalZoomLevels=0] - The number of digital zoom levels to add
         * @param {boolean} [slideLoaderOptions.scaleLine=true] - Whether or not to display a scale line when resolution information is available
         * @param {boolean} [slideLoaderOptions.colorAdjustments=false] - Whether or not to add a control that allows color adjustments
         * @param {string|PMA.UI.View.Viewport~filenameCallback} [slideLoaderOptions.filename] - A string to display as the file name or a callback function. If no value is supplied, no file name is displayed.
         * @param {boolean|PMA.UI.View.Viewport~attributionOptions}[slideLoaderOptions.attributions=undefined] - Whether or not to display Pathomation attribution in the viewer
         * @param {Array<PMA.UI.View.Viewport~customButton>} [slideLoaderOptions.customButtons] - An array of one or more custom buttons to add to the viewer
         * @param {Object|boolean} [options.magnifier=false] - Whether or not to show the magnifier control 
         * @param {Object|boolean} [options.magnifier.collapsed=undefined] - Whether or not to show the magnifier control in collapsed state
         * @param {Object} [options.grid] - Options for measurement grid
         * @param {Array<number>} [options.grid.size] - Grid cell width and height in micrometers
         * @fires PMA.UI.Components.Events#SlideInfoError
         * @fires PMA.UI.Components.Events#BeforeSlideLoad
         * @fires PMA.UI.Components.Events#SlideLoaded
         * @fires PMA.UI.Components.Events#BeforeDrop
         * @constructor
         * @memberof PMA.UI.Components
         * @tutorial 03-gallery
         * @tutorial 04-tree
         * @tutorial 05-annotations
         */
        function SlideLoader(context, slideLoaderOptions) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            this.loadingImage = false;
            this.lastLoadImageRequest = null;
            this.context = context;
            this.slideLoaderOptions = slideLoaderOptions || {};

            this.listeners = {};
            this.listeners[PMA.UI.Components.Events.SlideInfoError] = [];
            this.listeners[PMA.UI.Components.Events.BeforeSlideLoad] = [];
            this.listeners[PMA.UI.Components.Events.SlideLoaded] = [];
            this.listeners[PMA.UI.Components.Events.BeforeDrop] = [];

            /**
             * The currently loaded {@link PMA.UI.View.Viewport} instance, or null
             * @public
             */
            this.mainViewport = null;

            // try to get the element
            if (this.slideLoaderOptions.element) {
                if (this.slideLoaderOptions.element instanceof HTMLElement) {
                    this.element = this.slideLoaderOptions.element;
                }
                else if (typeof this.slideLoaderOptions.element == "string") {
                    var el = document.querySelector(this.slideLoaderOptions.element);
                    if (!el) {
                        throw "Invalid selector for element";
                    }
                    else {
                        this.element = el;
                    }
                }
            }

            initializeDropZone.call(this);
        }

        /**
         * Sets or overrides the value of an option. Useful when it is required to modify a viewer option before loading as slide.
         * @param  {string} option
         * @param  {any} value
         */
        SlideLoader.prototype.setOption = function (option, value) {
            this.slideLoaderOptions[option] = value;
        };

        /**
         * Gets the value of a viewer option.
         * @param  {string} option
         * @param  {any} value
         * @return {any} The value of the option or undefined
         */
        SlideLoader.prototype.getOption = function (option) {
            return this.slideLoaderOptions[option];
        };

        /**
         * Creates a {@link PMA.UI.View.Viewport} instance that loads the requested slide
         * @param  {string} serverUrl - PMA.core server URL
         * @param  {string} path - Path or UID of the slide load
         * @param  {function} [doneCb] - Called when the slide has finished loading
         * @param {boolean} [dropped] - Whether this slide was loaded by a drag and drop operation
         * @fires PMA.UI.Components.Events#BeforeSlideLoad
         * @fires PMA.UI.Components.Events#SlideLoaded
         * @fires PMA.UI.Components.Events#SlideInfoError
         */
        SlideLoader.prototype.load = function (serverUrl, path, doneCb, dropped) {
            if (this.loadingImage === true) {
                //console.error("SlideLoader.loadImage: Last load image call hasn't finished yet");
                this.lastLoadImageRequest = {
                    serverUrl: serverUrl,
                    path: path,
                    doneCb: doneCb,
                    dropped: dropped === true ? true : false
                };

                return;
            }

            if (dropped !== true) {
                dropped = false;
            }

            this.loadingImage = true;

            if (this.mainViewport && this.mainViewport.map) {
                while (this.mainViewport.map.getInteractions().getLength() > 0) {
                    this.mainViewport.map.removeInteraction(this.mainViewport.map.getInteractions().item(0));
                }

                while (this.mainViewport.map.getLayers().getLength() > 0) {
                    this.mainViewport.map.removeLayer(this.mainViewport.map.getLayers().item(0));
                }

                while (this.mainViewport.map.getControls().getLength() > 0) {
                    this.mainViewport.map.removeControl(this.mainViewport.map.getControls().item(0));
                }
            }

            var beforeLoadEa = { serverUrl: serverUrl, path: path, cancel: false };
            this.fireEvent(PMA.UI.Components.Events.BeforeSlideLoad, beforeLoadEa);

            if (beforeLoadEa.cancel) {
                this.loadingImage = false;
                return;
            }

            if (!serverUrl || !path) {
                if (this.mainViewport) {
                    this.mainViewport.element.innerHTML = "";
                    this.mainViewport = null;
                }

                this.loadingImage = false;
                this.fireEvent(PMA.UI.Components.Events.SlideLoaded, { serverUrl: serverUrl, path: path, dropped: dropped });
                if (typeof doneCb === "function") {
                    doneCb();
                }

                checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
                return;
            }

            var _this = this;
            this.context.getSession(serverUrl, function (sessionId) {
                var opts = clone(_this.slideLoaderOptions);

                opts.serverUrls = [serverUrl];
                opts.image = path;
                opts.sessionID = sessionId;
                opts.caller = _this.context.getCaller();

                _this.mainViewport = new PMA.UI.View.Viewport(opts, function () {
                    _this.loadingImage = false;
                    _this.fireEvent(PMA.UI.Components.Events.SlideLoaded, { serverUrl: serverUrl, path: path, dropped: dropped });

                    if (typeof doneCb === "function") {
                        doneCb();
                    }

                    checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
                }, function () {
                    _this.loadingImage = false;
                    console.error("Error loading slide");
                });

                var count = 0;
                _this.mainViewport.listen("tileserror", function () {
                    if (count === 0) {
                        count++;
                        PMA.UI.Components.sessionList[serverUrl] = null;

                        _this.context.getSession(serverUrl, function (newSessionId) {
                            count = -1;
                            _this.mainViewport.sessionID = newSessionId;
                            _this.mainViewport.redraw();
                        }, function () {
                            _this.fireEvent(PMA.UI.Components.Events.SlideInfoError, {});
                        });
                    }
                    else if (count === -1) {
                        count = 0;
                        _this.fireEvent(PMA.UI.Components.Events.SlideInfoError, {});
                    }
                });

                _this.mainViewport.listen("SlideLoadError", function () {
                    _this.loadingImage = false;
                    _this.fireEvent(PMA.UI.Components.Events.SlideInfoError, {});
                    checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
                });
            },
                function () {
                    _this.loadingImage = false;
                    _this.fireEvent(PMA.UI.Components.Events.SlideInfoError, {});
                    checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
                });
        };

        /**
         * Gets the image info of the currently loaded image
         * @return {object|null}
         */
        SlideLoader.prototype.getLoadedImageInfo = function () {
            if (this.mainViewport && this.mainViewport.map && this.mainViewport.imageInfo) {
                return this.mainViewport.imageInfo;
            }

            return null;
        };

        /**
         * Reloads annotations from the server
         * @param {function} [readyCallback] - Called when the annotations have finished loading
         */
        SlideLoader.prototype.reloadAnnotations = function (readyCallback) {
            if (this.mainViewport && this.mainViewport.map && this.mainViewport.imageInfo) {
                this.mainViewport.reloadAnnotations(readyCallback);
                return;
            }

            if (typeof readyCallback === "function") {
                readyCallback.call(this);
            }
        };

        /**
         * Attaches an event listener
         * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
         * @param {function} callback - The function to call when the event occurs
         */
        SlideLoader.prototype.listen = function (eventName, callback) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " is not a valid event");
            }

            this.listeners[eventName].push(callback);
        };

        // fires an event
        SlideLoader.prototype.fireEvent = function (eventName, eventArgs) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " does not exist");
                return;
            }

            var returns = [];
            for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
                returns.push(this.listeners[eventName][i].call(this, eventArgs));
            }

            return returns;
        };

        return SlideLoader;
    })();
}());
window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Components = window.PMA.UI.Components || {};

// namespace
(function () {
    PMA.UI.Components.SyncView = (function () {
        /**
         * Automatically handles syncronization of views between slides
         * @param  {PMA.UI.Components.SlideLoader[]} slideLoaders - Array of PMA.UI.Components.SlideLoaders.
         * @constructor
         * @memberof PMA.UI.Components
         */
        function SyncView(slideLoaders) {
            if (!slideLoaders.length) {
                console.error("Expected array of PMA.UI.Components.SlideLoader");
                return;
            }

            this.slideLoaders = slideLoaders;

            // for syncing viewers
            this.eventsKeys = [];
            this.posSync = [];
            this.overridePosition = false;

            this.listeners = {};
            this.listeners[PMA.UI.Components.Events.SyncChanged] = [];

            for (var i = 0; i < this.slideLoaders.length; i++) {
                this.slideLoaders[i].listen(PMA.UI.Components.Events.BeforeSlideLoad, this.disableSync.bind(this));
            }
        }

        /**
         * Enables synchronization on the slides
         * @fires PMA.UI.Components.Events#SyncChanged
         */
        SyncView.prototype.enableSync = function () {
            this.posSync = [];

            for (var i = 0; i < this.slideLoaders.length; i++) {
                var slideLoader = this.slideLoaders[i];

                if (slideLoader.mainViewport && slideLoader.mainViewport.map) {
                    this.posSync.push(slideLoader.mainViewport.getPosition());

                    var parameter = { index: i, self: this, slideLoaders: this.slideLoaders };
                    this.eventsKeys.push({ viewport: slideLoader.mainViewport, callback: viewChanged.bind(this, parameter) });
                    slideLoader.mainViewport.listen(PMA.UI.View.Events.ViewChanged, this.eventsKeys[this.eventsKeys.length - 1].callback);
                }
            }

            this.fireEvent(PMA.UI.Components.Events.SyncChanged, true);
        };

        /**
         * Disables synchronization on the slides
         * @fires PMA.UI.Components.Events#SyncChanged
         */
        SyncView.prototype.disableSync = function () {
            if (this.eventsKeys) {
                while (this.eventsKeys.length > 0) {
                    var ek = this.eventsKeys.pop();
                    var result = ek.viewport.unlisten(PMA.UI.View.Events.ViewChanged, ek.callback);
                }
            }

            this.posSync = [];

            this.fireEvent(PMA.UI.Components.Events.SyncChanged, false);
        };

        /**
         * Returns a value indicating whether slides are synchronized
         * @fires PMA.UI.Components.Events#SyncChanged
         */
        SyncView.prototype.getStatus = function () {
            return this.eventsKeys && this.eventsKeys.length > 0;
        };

        /**
         * Attaches an event listener
         * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
         * @param {function} callback - The function to call when the event occurs
         */
        SyncView.prototype.listen = function (eventName, callback) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " is not a valid event");
            }

            this.listeners[eventName].push(callback);
        };

        // fires an event
        SyncView.prototype.fireEvent = function (eventName, eventArgs) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " does not exist");
                return;
            }

            for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
                this.listeners[eventName][i].call(this, eventArgs);
            }
        };

        var overridePosition = false;
        function viewChanged(parameters) {
            if (parameters && !overridePosition) {
                var self = parameters.self;
                var slideLoader = parameters.slideLoaders[parameters.index];
                var pos = slideLoader.mainViewport.getPosition();
                var oldPos = self.posSync[parameters.index];

                if (isNaN(pos.zoom)) {
                    return;
                }

                if (oldPos) {
                    var diff = {
                        zoom: pos.zoom - oldPos.zoom,
                        rotation: pos.rotation - oldPos.rotation,
                        center: [pos.center[0] - oldPos.center[0], pos.center[1] - oldPos.center[1]]
                    };

                    self.posSync[parameters.index] = pos;
                    oldPos = self.posSync[parameters.index];

                    overridePosition = true;
                    for (var i = 0; i < parameters.slideLoaders.length; i++) {
                        if (i == parameters.index) {
                            continue;
                        }

                        var p = self.posSync[i];
                        if (p) {
                            p.zoom += diff.zoom;
                            p.rotation += diff.rotation;
                            if (p.rotation != oldPos.rotation) {
                                // different rotation 
                                var d = ol.coordinate.rotate([diff.center[0], diff.center[1]], p.rotation - oldPos.rotation);
                                p.center[0] += d[0];
                                p.center[1] += d[1];
                            }
                            else {
                                p.center[0] += diff.center[0];
                                p.center[1] += diff.center[1];
                            }

                            parameters.slideLoaders[i].mainViewport.setPosition(p, true);
                            parameters.slideLoaders[i].mainViewport.map.getView().dispatchEvent("change:center");
                        }

                        self.posSync[i] = p;
                    }

                    overridePosition = false;
                }
            }
        }

        return SyncView;
    })();
}());
window.PMA = window.PMA || {};
window.PMA.UI = window.PMA.UI || {};
window.PMA.UI.Components = window.PMA.UI.Components || {};

// namespace
(function ($) {
    var _ = PMA.UI.Resources.translate;

    // tree view class
    PMA.UI.Components.Tree = (function () {
        var PmaStartUrl = "http://127.0.0.1:54001/";

        function loadNode(event, data) {
            var dfd = new $.Deferred();
            data.result = dfd.promise();

            var node = data.node;
            var path = node.data.dirPath === "/" ? "" : node.data.dirPath;
            var _this = this;

            // if the directories to be loaded are root directories or sub directories
            var isRootDir = node.data.dirPath === "/";

            this.context.getDirectories(_this.servers[node.data.serverIndex].url, path,
                function (sessionId, directories) {
                    if (_this.rootDirSortCb && path === "") {
                        directories = _this.rootDirSortCb(directories);
                    }

                    var result = [];
                    for (var i = 0; i < directories.length; i++) {
                        result.push({
                            title: directories[i].split('/').pop(),
                            lazy: true,
                            serverIndex: node.data.serverIndex,
                            serverUrl: _this.servers[node.data.serverIndex].url,
                            dirPath: directories[i],
                            key: directories[i],
                            folder: true,
                            rootDir: isRootDir,
                            extraClasses: isRootDir ? "rootdir" : "subdir",
                            // unselectableStatus: true,
                            checkbox: _this.checkboxes
                        });
                    }

                    if (_this.servers[node.data.serverIndex].showFiles !== false && path && path !== "") {
                        _this.context.getSlides(
                            {
                                serverUrl: _this.servers[node.data.serverIndex].url,
                                path: path,
                                success: function (sessionId, files) {
                                    for (var i = 0; i < files.length; i++) {
                                        result.push({
                                            title: files[i].split('/').pop(),
                                            lazy: false,
                                            serverIndex: node.data.serverIndex,
                                            dirPath: files[i],
                                            serverUrl: _this.servers[node.data.serverIndex].url,
                                            key: files[i],
                                            folder: false,
                                            extraClasses: "slide"
                                        });
                                    }

                                    dfd.resolve(result);
                                },
                                failure: function (error) {
                                    dfd.reject(error.Message ? error.Message : _("Error loading files"));
                                }
                            });
                    }
                    else {
                        dfd.resolve(result);
                    }
                },
                function (error) {
                    dfd.reject(error.Message ? error.Message : _("Error loading directories"));
                });
        }

        function searchResultsSuccess(searchNode, serverIndex, searchHash, sessionId, results) {
            if (this.lastSearchHash !== searchHash) {
                return;
            }

            this.lastSearchResults[this.servers[serverIndex].name] = results;

            var searchServerNode = searchNode.addChildren({
                title: this.servers[serverIndex].name,
                serverNode: true,
                key: "_searchServer_" + this.servers[serverIndex].url,
                serverIndex: serverIndex,
                dirPath: (this.servers[serverIndex].path ? this.servers[serverIndex].path : "/"),
                lazy: false,
                unselectableStatus: false,
                unselectable: true,
                selected: false,
                checkbox: false,
                resultCount: results.length
            });

            var tree = $(this.element).fancytree("getTree");
            for (var r = 0; r < results.length; r++) {
                var parts = results[r].split('/');

                var thispart = "";
                var currentLevel = searchServerNode;
                for (var i = 0; i < parts.length; i++) {
                    thispart += i > 0 ? "/" + parts[i] : parts[i];

                    var n = tree.getNodeByKey("_searchResult_" + thispart, currentLevel);
                    if (n == null) {
                        currentLevel = currentLevel.addChildren({
                            title: thispart.split("/").pop(),
                            lazy: false,
                            serverIndex: serverIndex,
                            dirPath: thispart,
                            key: "_searchResult_" + thispart,
                            folder: i < parts.length - 1,
                            rootDir: i == 0,
                            extraClasses: i == 0 ? "rootdir" : "subdir",
                            checkbox: this.checkboxes,
                            resultCount: 1
                        });
                    }
                    else {
                        currentLevel = n;
                        n.data.resultCount += 1;
                    }
                }
            }

            searchNode.data.resultCount += results.length;

            searchNode.visit(function (n) {
                if (n == searchNode) {
                    n.setTitle(PMA.UI.Resources.translate("Search results for \"{pattern}\" ({count})", { pattern: searchNode.data.pattern, count: n.data.resultCount }));
                }
                else if ((n.isFolder() || n.data.serverNode) && n == searchServerNode) {
                    n.setTitle(n.title + " (" + n.data.resultCount + ")");
                }

            }, true);

            searchNode.data.serverDone++;
            if (searchNode.data.serverDone >= searchNode.data.serversSearched) {
                searchNode.data.searching = false;
                searchNode.renderTitle();
            }

            searchNode.makeVisible();
            searchNode.setExpanded(true);

            this.fireEvent(PMA.UI.Components.Events.SearchFinished, this.lastSearchResults);
        }

        function searchResultError(searchNode, serverIndex, searchHash) {
            if (this.lastSearchHash !== searchHash) {
                return;
            }

            searchNode.data.serverDone++;
            if (searchNode.data.serverDone >= searchNode.data.serversSearched) {
                searchNode.data.searching = false;
                searchNode.renderTitle();
            }

            this.fireEvent(PMA.UI.Components.Events.SearchFailed, this.servers[serverIndex]);
        }

        function serverVersionResult(server, version) {
            server.version = version;
        }

        function startSearch(pattern) {
            this.lastSearchResults = {};
            var searchResultsNode = $(this.element).fancytree("getTree").getNodeByKey("_searchResults");
            if (searchResultsNode == null) {
                var searchResultsNodeInfo = {
                    title: PMA.UI.Resources.translate("Search results for \"{pattern}\"", { pattern: pattern }),
                    key: "_searchResults",
                    lazy: false,
                    selected: false,
                    checkbox: false,
                    resultCount: 0,
                    searching: true,
                    serverDone: 0,
                    serversSearched: 0,
                    pattern: pattern
                };

                searchResultsNode = $(this.element).fancytree("getRootNode").addChildren(searchResultsNodeInfo);
            }
            else {
                searchResultsNode.resultCount = 0;
                searchResultsNode.removeChildren();
                searchResultsNode.data.searching = true;
                searchResultsNode.data.serverDone = 0;
                searchResultsNode.data.serversSearched = 0;
                searchResultsNode.data.pattern = pattern;
                searchResultsNode.renderTitle();
            }

            searchResultsNode.makeVisible();
            searchResultsNode.setExpanded(true);

            var self = this;
            this.lastSearchHash = Math.random();
            for (var i = 0; i < this.servers.length; i++) {
                if (!this.servers[i].version || this.servers[i].version.substring(0, "1.".length) === "1.") {
                    continue;
                }

                searchResultsNode.data.serversSearched++;
                this.context.queryFilename(
                    this.servers[i].url,
                    "",
                    pattern,
                    searchResultsSuccess.bind(self, searchResultsNode, i, this.lastSearchHash),
                    searchResultError.bind(self, searchResultsNode, i, this.lastSearchHash)
                );
            }
        }

        /**
         * Converts a virtual path to a key used internaly by the tree component to locate any node
         * @param {string} path - The virtual path to convert. The server part should be the NAME of the server (not the url)
         * @param {Object} tree - The instance of the fancytree
         * @returns {string} The key to the node
         */
        function virtualPathToTreePath(path, tree) {
            var parts = path.split("/").filter(function(e) { return e != null && e != ""; });
            var initialPath = "";

            if (parts.length > 0) {
                // find server url as first part of path
                for (var i = 0; i < this.servers.length; i++) {
                    if (this.servers[i].name.toLowerCase() == parts[0].toLowerCase()) {
                        parts[0] = this.servers[i].url;
                        initialPath = this.servers[i].path ? this.servers[i].path : "";
                        break;
                    }
                }

                if (initialPath) {
                    parts[1] = initialPath + "/" + parts[1];
                }

                for (i = 2; i < parts.length; i++) {
                    parts[i] = parts[i - 1] + "/" + parts[i];
                }
            }

            parts = parts.join(tree.options.keyPathSeparator);

            return parts;
        }

        function disablePreview(tree) {
            if (this.previewEnabled) {
                tree.off("mouseenter", "span.fancytree-title");
                tree.off("mouseleave", "span.fancytree-title");
                tree.off("mousemove", "span.fancytree-title");

                $("#fancytree-preview").remove();
                this.previewEnabled = false;
            }
        }

        function enablePreview(tree) {
            if (!this.previewEnabled) {
                var xOffset = 100, yOffset = -30;
                var hoverTimeout = null;
                var _this = this;

                tree.on("mouseenter", "span.fancytree-title", function (event) {
                    // Add a hover handler to all node titles (using event delegation)
                    var node = $.ui.fancytree.getNode(event);
                    if (!(node === null || node.data.serverNode === true || node.data.rootDir === true || node.isFolder()) && node.data.serverIndex !== undefined) {
                        var serverUrl = _this.servers[node.data.serverIndex].url;

                        var f = function () {
                            _this.context.getSession(serverUrl, function (sessionId) {
                                var tUrl = PMA.UI.Components.getThumbnailUrl(serverUrl, sessionId, node.data.dirPath, 0, 150, 0);
                                var el = $("#fancytree-preview");
                                if (el.length > 0) {
                                    el.remove();
                                }

                                el = $("<p id='fancytree-preview' class='fancytree-preview'><i class='fa fa-spinner fa-spin'></i><img/></p>").appendTo("body");
                                el.css("position", "absolute")
                                    .css("top", (event.pageY + yOffset) + "px")
                                    .css("left", (event.pageX + xOffset) + "px")
                                    .fadeIn("fast");

                                el.find("img").bind("load", function () {
                                    el.find("i").remove();
                                }).attr("src", tUrl);

                            });
                        };

                        hoverTimeout = setTimeout(f, 250);
                    }
                });

                tree.on("mouseleave", "span.fancytree-title", function (event) {
                    $("#fancytree-preview").remove();
                    if (hoverTimeout) {
                        clearTimeout(hoverTimeout);
                        hoverTimeout = null;
                    }
                });

                tree.on("mousemove", "span.fancytree-title", function (event) {
                    var el = $("#fancytree-preview");
                    if (el.length > 0) {
                        el.css("top", (event.pageY + yOffset) + "px")
                            .css("left", (event.pageX + xOffset) + "px");
                    }
                });

                this.previewEnabled = true;
            }
        }

        /**
         * Holds information about a PMA.core server
         * @typedef {Object} PMA.UI.Components.Tree~server
         * @property {string} name - The display name of the server
         * @property {string} url - The url of the server
         * @property {string} [path] - An optional path of a folder in the server to show
         * @property {boolean} [showFiles=true] - Whether or not to display slides along with directories
         */

        /**
         * Function that sorts an array of directories
         * @callback PMA.UI.Components.Tree~rootDirSortCb
         * @param {String[]} directories
         * @returns {String[]}
         */

        /**
        * Function that gets called when an attempt to add a server completes.
        * @callback PMA.UI.Components.Tree~addServerCallback
        * @param {boolean} success - Whether the server was successfully added or not
        */

        /**
         * Represents a UI component that shows a tree view that allows browsing through the directories and slides from multiple PMA.core servers. This component uses {@link https://github.com/mar10/fancytree|fancytree} under the hood.
         * @param  {PMA.UI.Components.Context} context
         * @param  {object} options - Configuration options
         * @param  {PMA.UI.Components.Tree~server[]} options.servers An array of servers to show files from
         * @param {string|HTMLElement} options.element - The element that hosts the tree view. It can be either a valid CSS selector or an HTMLElement instance.
         * @param  {function} [options.renderNode] - Allows tweaking after node state was rendered
         * @param  {PMA.UI.Components.Tree~rootDirSortCb} [options.rootDirSortCb] - Function that sorts an array of directories
         * @param  {boolean} [options.checkboxes=false] - Allows multi selection of files with checkboxes
         * @param  {boolean} [options.autoDetectPmaStart=false] - Whether the tree should try to connect to a PMA.core lite server
         * @param  {boolean} [options.autoExpandNodes=false] - Whether the tree should expand nodes on single click
         * @param  {boolean} [options.preview=false] - Whether the tree should show a popover preview of slides
         * @param  {boolean} [options.search=false] - Whether the tree should show a textbox for enabling search
         * @constructor
         * @memberof PMA.UI.Components
         * @fires PMA.UI.Components.Events#SlideSelected
         * @fires PMA.UI.Components.Events#DirectorySelected
         * @fires PMA.UI.Components.Events#ServerSelected
         * @fires PMA.UI.Components.Events#MultiSelectionChanged
         * @fires PMA.UI.Components.Events#TreeNodeDoubleClicked
         * @fires PMA.UI.Components.Events#ServerExpanded
         * @fires PMA.UI.Components.Events#DirectoryExpanded
         * @fires PMA.UI.Components.Events#SearchFinished
         * @fires PMA.UI.Components.Events#SearchFailed
         * @tutorial 04-tree
         */
        function Tree(context, options) {
            if (!PMA.UI.Components.checkBrowserCompatibility()) {
                return;
            }

            if (options.element instanceof HTMLElement) {
                this.element = element;
            }
            else if (typeof options.element == "string") {
                var el = document.querySelector(options.element);
                if (!el) {
                    console.error("Invalid selector for element");
                }
                else {
                    this.element = el;
                }
            }

            this.context = context;
            this.servers = options.servers || [];
            var _this = this;

            this.navigating = false;
            this.lastNavigatePathRequest = null;

            this.autoExpand = options.autoExpandNodes === true;
            this.checkboxes = options.checkboxes === true;
            this.lastSearchResults = {};
            this.listeners = {};
            this.listeners[PMA.UI.Components.Events.DirectorySelected] = [];
            this.listeners[PMA.UI.Components.Events.SlideSelected] = [];
            this.listeners[PMA.UI.Components.Events.ServerSelected] = [];
            this.listeners[PMA.UI.Components.Events.MultiSelectionChanged] = [];
            this.listeners[PMA.UI.Components.Events.TreeNodeDoubleClicked] = [];
            this.listeners[PMA.UI.Components.Events.ServerExpanded] = [];
            this.listeners[PMA.UI.Components.Events.DirectoryExpanded] = [];
            this.listeners[PMA.UI.Components.Events.SearchFinished] = [];
            this.listeners[PMA.UI.Components.Events.SearchFailed] = [];

            this.lastSearchHash = 0;
            this.previewEnabled = false;

            if (typeof options.rootDirSortCb === "function") {
                this.rootDirSortCb = options.rootDirSortCb;
            }

            var sourceData = [];
            for (var i = 0; i < this.servers.length; i++) {
                sourceData.push({
                    title: this.servers[i].name,
                    serverNode: true,
                    key: this.servers[i].url,
                    serverIndex: i,
                    extraClasses: "server",
                    dirPath: (this.servers[i].path ? this.servers[i].path : "/"),
                    lazy: true,
                    unselectableStatus: false,
                    unselectable: true,
                    selected: false,
                    checkbox: false
                });

                // try get version for each server
                this.context.getVersionInfo(this.servers[i].url, serverVersionResult.bind(this, this.servers[i]));
            }

            var searchBox = null;
            if (options.search === true) {
                var cls = options.searchClass ? options.searchClass : 'pma-ui-tree-search-box';
                searchBox = $("<input type='text' class='" + cls + "' placeholder='" + PMA.UI.Resources.translate("Search") + "'/><hr />").appendTo(this.element);
            }

            var tree = $(this.element).fancytree({
                keyPathSeparator: "?",
                checkbox: options.checkboxes === true,
                extensions: ["dnd5", /*"glyph", "wide"*/],
                dnd5: {
                    dragStart: function (node, data) {
                        // Called when user starts dragging `node`.
                        // This method MUST be defined to enable dragging for tree nodes.
                        //
                        // We can
                        //   Add or modify the drag data using `data.dataTransfer.setData()`
                        //   Return false to cancel dragging of `node`.

                        // For example:
                        //    if( data.originalEvent.shiftKey ) ...          
                        //    if( node.isFolder() ) { return false; }
                        if (node.data.dirPath && !node.data.serverNode) {
                            node.data.dragging = true;
                            if (node.isActive) {
                                node.setActive(false);
                            }

                            data.dataTransfer.setData("text", JSON.stringify({
                                serverUrl: node.data.serverUrl,
                                path: node.data.dirPath,
                                isFolder: node.isFolder()
                            }));

                            return true;
                        }

                        return false;
                    },
                    dragEnd: function (node, data) {
                        node.data.dragging = false;
                    }
                },
                //glyph: glyph_opts,
                selectMode: 3,
                toggleEffect: { effect: "drop", options: { direction: "left" }, duration: 400 },
                // toggleEffect: false,
                wide: {
                    iconWidth: "1em", // Adjust this if @fancy-icon-width != "16px"
                    iconSpacing: "0.5em", // Adjust this if @fancy-icon-spacing != "3px"
                    levelOfs: "1.5em" // Adjust this if ul padding != "16px"
                },
                icon: function (event, data) {
                    if (data.node.key === "_searchResults") {
                        if (data.node.data.searching) {
                            return "fa fa-spinner fa-spin";
                        }
                        else {
                            return "fa fa-search";
                        }
                    }
                    else if (data.node.data.serverNode === true) {
                        return "server";
                    }
                    else if (data.node.data.rootDir === true) {
                        return "rootdir";
                    }
                    else if (!data.node.isFolder()) {
                        return "image";
                    }
                },
                renderNode: (typeof options.renderNode === "function" ? options.renderNode : null),
                source: sourceData,
                lazyLoad: loadNode.bind(this),
                activate: function (event, data) {
                    // A node was activated:
                    var node = data.node;
                    if (node.key == "_searchResults" && _this.autoExpand === true) {
                        node.setExpanded(true);
                    }

                    setTimeout(function () {
                        if (node.data && node.data.dragging === true) {
                            event.preventDefault();
                            return;
                        }

                        if (node.data.serverNode === true) {
                            _this.fireEvent(PMA.UI.Components.Events.ServerSelected, { serverUrl: _this.servers[node.data.serverIndex].url });
                            return;
                        }

                        if (node.data.dirPath !== "/") {
                            if (node.isFolder()) {
                                if (_this.autoExpand === true) {
                                    node.setExpanded(true);
                                }

                                _this.fireEvent(PMA.UI.Components.Events.DirectorySelected, { serverUrl: _this.servers[node.data.serverIndex].url, path: node.data.dirPath });
                            }
                            else {
                                if (_this.servers[node.data.serverIndex]) {
                                    _this.fireEvent(PMA.UI.Components.Events.SlideSelected, { serverUrl: _this.servers[node.data.serverIndex].url, path: node.data.dirPath });
                                }
                            }
                        }
                        else if (_this.autoExpand === true) {
                            node.setExpanded(true);
                        }
                    }, 300);
                },
                select: function (event, data) {
                    var n = data.tree.getSelectedNodes();
                    var selectionArray = [];

                    if (n && n.length > 0) {
                        for (var i = 0; i < n.length; i++) {
                            if (!(n[i] === null || n[i].data.serverNode === true || n[i].data.rootDir === true || n[i].isFolder())) {
                                selectionArray.push({ serverUrl: _this.servers[n[i].data.serverIndex].url, path: n[i].data.dirPath });
                            }
                        }
                    }

                    _this.fireEvent(PMA.UI.Components.Events.MultiSelectionChanged, selectionArray);
                },
                expand: function (event, data) {
                    var node = data.node;
                    setTimeout(function () {
                        if (node.data.serverNode === true) {
                            _this.fireEvent(PMA.UI.Components.Events.ServerExpanded, { serverUrl: _this.servers[node.data.serverIndex].url });
                            return;
                        }
                        else if (node.data.dirPath !== "/") {
                            if (node.isFolder()) {
                                _this.fireEvent(PMA.UI.Components.Events.DirectoryExpanded, { serverUrl: _this.servers[node.data.serverIndex].url, path: node.data.dirPath });
                                return;
                            }
                        }
                    }, 300);
                },
                dblclick: function (event, data) {
                    var node = data.node;
                    _this.fireEvent(PMA.UI.Components.Events.TreeNodeDoubleClicked,
                        {
                            serverUrl: _this.servers[node.data.serverIndex].url,
                            path: node.data.dirPath,
                            isSlide: !(node.data.serverNode === true || node.data.rootDir === true || node.isFolder())
                        });
                }
            });

            if (options.preview === true) {
                enablePreview.call(this, tree);
            }

            if (options.autoDetectPmaStart === true) {
                this.addPmaStartServer();
            }

            if (options.search === true) {
                $(this.element).find(".ui-fancytree").addClass("ui-fancytree-search");
            }

            this.fancytree = $.ui.fancytree.getTree(this.element);

            var self = this;
            var searchTimeout = null;
            var t = this.fancytree;
            if (searchBox) {
                searchBox.on('input propertychange paste', function () {
                    var val = $(this).val();
                    if (val && val.length > 3) {
                        clearTimeout(searchTimeout);
                        searchTimeout = setTimeout(function () {
                            startSearch.call(self, val);
                        }, 500);
                    }
                    else {
                        var n = t.getNodeByKey("_searchResults");
                        if (n) {
                            n.remove();
                        }
                    }
                });
            }
        }

        /**
         * Toggle the live preview on/off
         * @param {boolean} enable 
         */
        Tree.prototype.togglePreview = function (enable) {
            if (enable && !this.previewEnabled) {
                enablePreview.call(this, $(this.element));
            }
            else if (!enable && this.previewEnabled) {
                disablePreview.call(this, $(this.element));
            }
        };

        /**
         * Adds a new server to the tree
         * @param  {PMA.UI.Components.Tree~server} server A server object
         */
        Tree.prototype.addServer = function (server) {
            if (server) {
                this.servers.push(server);

                var serverInfo = {
                    title: this.servers[this.servers.length - 1].name,
                    serverNode: true,
                    key: this.servers[this.servers.length - 1].url,
                    serverIndex: this.servers.length - 1,
                    extraClasses: "server",
                    dirPath: (this.servers[this.servers.length - 1].path ? this.servers[this.servers.length - 1].path : "/"),
                    lazy: true,
                    unselectableStatus: false,
                    unselectable: true,
                    selected: false,
                    checkbox: false
                };

                // try get version for server
                this.context.getVersionInfo(server.url, serverVersionResult.bind(this, server));
                $(this.element).fancytree("getRootNode").addChildren(serverInfo);
            }
        };

        /**
         * Removes a server from the tree
         * @param {number} index The index of the server to remove
         */
        Tree.prototype.removeServer = function (index) {
            var children = this.fancytree.getRootNode().getChildren();
            if (children && children.length && index >= 0 && index < children.length) {
                if (children[index].data) {
                    this.servers.splice(children[index].data.serverIndex, 1);
                }

                children[index].remove();
            }
            else {
                console.error("No children found or index out of range");
            }
        };

        /**
         * Removes a Pma.start server if it exists
         */
        Tree.prototype.removePmaStartServer = function () {
            var children = this.fancytree.getRootNode().getChildren();
            for (var i = 0; i < children.length; i++) {
                if (children[i].key == PmaStartUrl) {
                    this.removeServer(children[i].data.serverIndex);
                }
            }
        };

        /**
         * Add a pma.start server if available
         * @param {PMA.UI.Components.Tree~addServerCallback} callback - The function to call when the attempt to add server completes
         */
        Tree.prototype.addPmaStartServer = function (callback) {
            // This function adds a SessionLogin provider to the context as many times as it is called. 
            // This needs fixing
            var _this = this;
            var children = this.fancytree.getRootNode().getChildren();
            for (var i = 0; i < children.length; i++) {
                if (children[i].key == PmaStartUrl) {
                    return;
                }
            }

            PMA.UI.Components.callApiMethod({
                method: PMA.UI.Components.ApiMethods.GetVersionInfo,
                httpMethod: "GET",
                data: { rnd: Math.random() },
                serverUrl: PmaStartUrl,
                success: function () {
                    var sl = new PMA.UI.Authentication.SessionLogin(_this.context, [{ serverUrl: PmaStartUrl, sessionId: "pma.core.lite" }]);
                    _this.addServer({ name: PMA.UI.Resources.translate("Computer"), url: PmaStartUrl });
                    if (typeof callback === "function") {
                        callback.call(this, true);
                    }
                },
                failure: function () {
                    if (typeof callback === "function") {
                        callback.call(this, false);
                    }
                }
            });
        };

        /**
         * Returns the list of servers currently under the tree view
         * @returns {PMA.UI.Components.Tree~server[]}
         */
        Tree.prototype.getServers = function () {
            return this.servers;
        };

        /**
         * Gets the currently selected slide or null
         * @returns {PMA.UI.Components.Tree~server}
         */
        Tree.prototype.getSelectedSlide = function () {
            var n = this.fancytree.getActiveNode();
            if (n === null || n.data.serverNode === true || n.data.rootDir === true || n.isFolder()) {
                return null;
            }

            return { server: this.servers[n.data.serverIndex].url, path: n.data.dirPath };
        };

        /**
         * Gets the currently selected directory or null
         * @returns {PMA.UI.Components.Tree~server}
         */
        Tree.prototype.getSelectedDirectory = function () {
            var n = this.fancytree.getActiveNode();
            if (n !== null && (n.data.rootDir === true || n.isFolder())) {
                return { server: this.servers[n.data.serverIndex].url, path: n.data.dirPath };
            }

            return null;
        };

        /**
         * Gets an array with the checked slides or an empty array
         * @returns {PMA.UI.Components.Tree~server[]}
         */
        Tree.prototype.getMultiSelection = function () {
            var n = this.fancytree.getSelectedNodes();
            var selectionArray = [];
            if (n && n.length > 0) {
                for (var i = 0; i < n.length; i++) {
                    if (!(n[i] === null || n[i].data.serverNode === true || n[i].data.rootDir === true || n[i].isFolder())) {
                        selectionArray.push({ serverUrl: this.servers[n[i].data.serverIndex].url, path: n[i].data.dirPath });
                    }
                }
            }

            return selectionArray;
        };

        /**
         * Clears the selected nodes in the tree view
         */
        Tree.prototype.clearMultiSelection = function () {
            this.fancytree.selectAll(false);
        };

        /**
         * Navigates to a path in the tree
         * @param {string} path - The virtual path to navigate to. The server part of the path should be the server NAME(not the server url)
         */
        Tree.prototype.navigateTo = function (path) {
            // fancy tree needs a path separated with options.keyPathSeparator 
            //  ex. ?key1?key2?key3
            var self = this;
            if (self.navigating) {
                self.lastNavigatePathRequest = path;
                return;
            }

            self.navigating = true;

            var tree = this.fancytree;
            var key = virtualPathToTreePath.call(this, path, tree);

            return tree.loadKeyPath(key, function (node, status) {
                if (status === "ok") {
                    node.setActive();
                    node.scrollIntoView();
                    self.navigating = false;
                }
            }).done(function () {
                self.navigating = false;

                if (self.lastNavigatePathRequest) {
                    var p = self.lastNavigatePathRequest;
                    self.lastNavigatePathRequest = null;
                    self.navigateTo(p);
                }
            });
        };

        /**
         * Refreshes a node in the tree specified by the path (server or directory)
         * @param {string} path - The virtual path to refresh. The server part of the path should be the server NAME(not the server url)
         */
        Tree.prototype.refresh = function (path) {
            var tree = this.fancytree;
            var key = virtualPathToTreePath.call(this, path, tree);

            return tree.loadKeyPath(key, function (node, status) {
                if (status === "ok") {
                    node.resetLazy();
                }
            });
        };

        /**
         * Returns the last search results, grouped by the server name
         * @return {Object.<string, string[]>} The object containing the result for each server available
         */
        Tree.prototype.getSearchResults = function () {
            return this.lastSearchResults;
        };

        /**  
         * Clears any search results for this tree
        */
        Tree.prototype.clearSearchResults = function () {
            var tree = this.fancytree;
            var n = tree.getNodeByKey("_searchResults");
            if (n) {
                n.remove();
            }
        };

        /**
         * Searches for files matching the specified value, and appends them to the tree. Use {@link PMA.UI.Components.Tree#getSearchResults} to get the search results
         * @param {string} value - The value to search for
         * @fires PMA.UI.Components.Events#SearchFinished
         */
        Tree.prototype.search = function (value) {
            this.clearSearchResults();
            return startSearch.call(this, value);
        };

        /**
         * Attaches an event listener
         * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
         * @param {function} callback - The function to call when the event occurs
         */
        Tree.prototype.listen = function (eventName, callback) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " is not a valid event");
            }

            this.listeners[eventName].push(callback);
        };

        // fires an event
        Tree.prototype.fireEvent = function (eventName, eventArgs) {
            if (!this.listeners.hasOwnProperty(eventName)) {
                console.error(eventName + " does not exist");
                return;
            }

            for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
                this.listeners[eventName][i].call(this, eventArgs);
            }
        };

        /**
         * Gets a value indicating whether files are shown for a specified server
         * @param {string} serverUrl - A server url to show/hide files for
         */
        Tree.prototype.getFilesVisibility = function (serverUrl) {
            var children = this.fancytree.getRootNode().getChildren();
            for (var i = 0; i < children.length; i++) {
                if (children[i].key == serverUrl) {
                    for (var j = 0; j < this.servers.length; j++) {
                        if (this.servers[j].url == children[i].key) {
                            return this.servers[j].showFiles;
                        }
                    }
                }
            }
        };

        /**
         * Shows or hides the files of a specified server
         * @param {string} serverUrl - A server url to show/hide files for
         * @param {boolean} visible - Whether to show or hide files for the specific server
         */
        Tree.prototype.setFilesVisibility = function (serverUrl, visible) {
            var children = this.fancytree.getRootNode().getChildren();
            for (var i = 0; i < children.length; i++) {
                if (children[i].key == serverUrl) {
                    for (var j = 0; j < this.servers.length; j++) {
                        if (this.servers[j].url == children[i].key) {
                            this.servers[j].showFiles = visible;
                        }
                    }

                    children[i].resetLazy();
                }
            }
        };

        /**
         * Resets the state of all servers, collapses all visible folder, and resets the lazy load state
         */
        Tree.prototype.collapseAll = function () {
            var children = this.fancytree.getRootNode().getChildren();
            for (var i = 0; i < children.length; i++) {
                // children[i].setExpanded(false);
                children[i].resetLazy();
            }
        };

        /**
         * Resets the state of a specified server, collapses all visible folder, resets the lazy load state and invalidates the session 
         */
        Tree.prototype.signOut = function (serverUrl) {
            var children = this.fancytree.getRootNode().getChildren();
            for (var i = 0; i < children.length; i++) {
                if (children[i].key == serverUrl) {
                    children[i].resetLazy();
                    this.context.deAuthenticate(serverUrl);
                }
            }
        };

        return Tree;
    })();
}(window.jQuery));