(function ($, Drupal) {
  'use strict';

  /**
   * @file
   * Defines Imce File Manager.
   */

  /**
   * Global container.
   */
  var imce = window.imce = {
    // Configuration options
    conf: {},
    // Locally stored data
    local: {},
    // Events
    events: {},
    // Shortcuts
    shortcuts: {fm: {}, tree: {}, content: {}},
    // Toolbar buttons
    toolbarButtons: {},
    // Folder tree
    tree: {},
    // Currently selected items
    selection: [],
    // Message queue
    messageQueue: [],
    // Sort handlers
    sorters: {}
  };

  /**
   * Initiate imce on document ready.
   */
  $(function () {

    var settings = window.drupalSettings;
    var conf = settings && settings.imce;
    var body = document.body;
    if (conf && !imce.activeFolder && $(body).hasClass('imce-page')) {
      if (!conf.ajax_url) {
        conf.ajax_url = Drupal.url(settings.path.currentPath);
      }
      imce.init(conf, body);
    }
  });

  /**
   * Initialize imce.
   */
  imce.init = function (conf, parentEl) {
    // Set conf
    conf = $.extend(imce.conf, conf);
    if (!conf.ajax_url || !conf.folders || !conf.root_url) {
      return;
    }
    imce.parentEl = parentEl;
    // Get stored data
    try {
      $.extend(imce.local, JSON.parse(localStorage.getItem('imce.local')));
    }
    catch (err) {
      imce.delayError(err);
    }
    // Init
    imce.trigger('preinit');
    imce.checkIntegration();
    imce.initUI();
    imce.initTree();
    // Add shortcuts
    imce.addShortcut('ENTER', imce.eTreeEnter, 'tree');
    imce.addShortcut('UP', imce.eTreeUp, 'tree');
    imce.addShortcut('DOWN', imce.eTreeDown, 'tree');
    imce.addShortcut('LEFT', imce.eTreeLR, 'tree');
    imce.addShortcut('RIGHT', imce.eTreeLR, 'tree');
    imce.addShortcut('ENTER', imce.eContentEnter, 'content');
    imce.addShortcut('UP', imce.eContentArrow, 'content');
    imce.addShortcut('DOWN', imce.eContentArrow, 'content');
    imce.addShortcut('LEFT', imce.eContentArrow, 'content');
    imce.addShortcut('RIGHT', imce.eContentArrow, 'content');
    imce.addShortcut('Ctrl+A', imce.eContentCtrlA, 'content');
    // Add refresh button
    imce.addTbb('refresh', {
      title: Drupal.t('Refresh'),
      permission: 'browse_files|browse_subfolders',
      handler: imce.eFmRefresh,
      shortcut: 'F5',
      icon: 'refresh'
    });
    // Register default activeFolder handler
    imce.bind('activateFolder', imce.defaultActivateFolder);
    // Trigger init handlers
    imce.trigger('init');
    // Create sendto toolbar button if needed.
    imce.createSendtoTbb();
    // Add the file manager to the page
    parentEl.appendChild(imce.fmEl);
    // Set window events
    $(window).on('beforeunload', imce.eWinBeforeunload).on('resize', imce.eWinResize);
    imce.eWinResize();
    // Content focus
    imce.contentEl.focus();
    // Set initial messages
    imce.ajaxProcessMessages(conf);
    // Open active path
    var path = conf.active_path;
    var Folder = path && imce.addFolder(path);
    if (!Folder) {
      for (path in conf.folders) {
        if (Folder = imce.getFolder(path)) {
          break;
        }
      }
    }
    if (Folder) {
      Folder.open();
    }
    // Trigger postinit
    imce.trigger('postinit');
  };

  /**
   * Init UI elements.
   */
  imce.initUI = function () {
    var el = imce.fmEl;
    var createEl = imce.createEl;
    if (el) {
      return el;
    }
    el = imce.fmEl = createEl('<div id="imce-fm"></div>');
    el.onkeydown = imce.eFmKeydown;
    el.tabIndex = 0;
    // Toolbar
    el.appendChild(imce.toolbarEl = createEl('<div id="imce-toolbar" aria-label="Operations" role="toolbar"></div>'));
    // Body
    el.appendChild(imce.bodyEl = createEl('<div id="imce-body"></div>'));
    // Tree
    el = imce.treeEl = createEl('<div id="imce-tree"></div>');
    el.onkeydown = imce.eTreeKeydown;
    el.onmousedown = imce.eTreeMousedown;
    el.ontouchstart = imce.eTreeTouchstart;
    el.tabIndex = 0;
    imce.bodyEl.appendChild(el);
    // Tree resizer
    el = imce.treeResizerEl = createEl('<div id="imce-tree-resizer"></div>');
    el.onmousedown = imce.eTreeResizerMousedown;
    el.ontouchstart = imce.eTreeResizerTouchstart;
    imce.bodyEl.appendChild(el);
    // Content
    el = imce.contentEl = createEl('<div id="imce-content"></div>');
    if (imce.conf.thumbnail_grid_style) {
      el.className = 'thumbnail-grid';
    }
    el.onmousedown = imce.eContentMousedown;
    el.ontouchstart = imce.eContentTouchstart;
    el.onkeydown = imce.eContentKeydown;
    el.onscroll = imce.eContentScroll;
    el.tabIndex = 0;
    imce.bodyEl.appendChild(el);
    // Content header
    el = imce.contentHeaderEl = imce.createEl('<div class="imce-content-header"><div class="imce-item"><div class="imce-item-date" data-sort="date">' + Drupal.t('Date') + '</div><div class="imce-item-height" data-sort="height">' + Drupal.t('Height') + '</div><div class="imce-item-width" data-sort="width">' + Drupal.t('Width') + '</div><div class="imce-item-size" data-sort="size">' + Drupal.t('Size') + '</div><div class="imce-item-icon imce-ficon" data-sort="ext"></div><div class="imce-item-name" data-sort="name">' + Drupal.t('Name') + '</div></div></div>');
    el.onclick = imce.eContentHeaderClick;
    imce.contentEl.appendChild(el);
    // Content status
    el = imce.contentStatusEl = imce.createEl('<div class="imce-content-status"></div>');
    imce.contentEl.appendChild(el);
    // Body resizer
    el = imce.bodyResizerEl = createEl('<div id="imce-body-resizer"></div>');
    el.onmousedown = imce.eBodyResizerMousedown;
    el.ontouchstart = imce.eBodyResizerTouchstart;
    imce.fmEl.appendChild(el);
    // Preview
    imce.fmEl.appendChild(imce.previewEl = createEl('<div id="imce-preview"></div>'));
    return el;
  };

  /**
   * Init folder tree.
   */
  imce.initTree = function () {
    var path;
    var folders = imce.getConf('folders');
    // Create root
    var scheme = imce.getConf('scheme');
    var root = new imce.Folder(scheme ? scheme + '://' : '<' + Drupal.t('root') + '>');
    root.setPath('.');
    root.branchEl.className += ' root';
    imce.treeEl.appendChild(root.branchEl);
    // Create predefined folders in alphabetical order.
    var paths = [];
    for (path in folders) {
      if (imce.owns(folders, path)) {
        paths.push(path);
      }
    }
    paths.sort();
    for (var i = 0; path = paths[i]; i++) {
      imce.addFolder(path, folders[path]);
    }
  };

  /**
   * Returns a folder by path.
   */
  imce.getFolder = function (path) {
    if (imce.owns(imce.tree, path)) {
      return imce.tree[path];
    }
  };

  /**
   * Returns an item by path.
   */
  imce.getItem = function (path) {
    var Folder;
    var parts = imce.splitPath(path);
    if (parts) {
      if (Folder = imce.getFolder(parts[0])) {
        return Folder.getItem(parts[1]);
      }
    }
  };

  /**
   * Adds a folder to the tree.
   */
  imce.addFolder = function (path, conf) {
    var parts;
    var parent;
    var Folder = imce.getFolder(path);
    // Existing
    if (Folder) {
      if (conf) {
        Folder.setConf(conf);
      }
      return Folder;
    }
    // New. Append to the parent.
    if (parts = imce.splitPath(path)) {
      if (parent = imce.addFolder(parts[0])) {
        Folder = new imce.Folder(parts[1], conf);
        parent.appendItem(Folder);
        return Folder;
      }
    }
  };

  /**
   * Add a toolbar button.
   */
  imce.addTbb = function (id, opt) {
    return imce.getTbb(id) || new imce.Tbb(id, opt);
  };

  /**
   * Returns a toolbar button.
   */
  imce.getTbb = function (id) {
    return imce.toolbarButtons[id];
  };

  /**
   * Returns a configuration option.
   */
  imce.getConf = function (name, defaultValue) {
    var value;
    var conf = imce.conf;
    if (!name) {
      return conf;
    }
    value = conf[name];
    return value == null ? defaultValue : value;
  };


  /**
   * Returns a copy of selected items.
   */
  imce.getSelection = function () {
    return imce.selection.slice(0);
  };

  /**
   * Counts selected items.
   */
  imce.countSelection = function () {
    return this.selection.length;
  };

  /**
   * Returns the selected items grouped by parent folder and type.
   */
  imce.groupSelection = function () {
    return imce.groupItems(imce.selection);
  };

  /**
   * Select all items in the active folder.
   */
  imce.selectAll = function () {
    var Folder = imce.activeFolder;
    if (Folder) {
      Folder.selectAll();
    }
  };

  /**
   * Deselects all items.
   */
  imce.deselectAll = function () {
    var i;
    var arr = imce.getSelection();
    for (i in arr) {
      if (imce.owns(arr, i)) {
        arr[i].deselect();
      }
    }
  };

  /**
   * Returns last selected item.
   */
  imce.getLastSelected = function () {
    var arr = imce.selection;
    var len = arr.length;
    if (len) {
      return arr[len - 1];
    }
  };

  /**
   * Adds an item to the selection.
   */
  imce.selectItem = function (Item) {
    if (!Item.selected) {
      var arr = imce.selection;
      var oldlen = arr.length;
      arr.push(Item);
      Item.setState('selected');
      if (oldlen < 2) {
        imce.updatePreview();
      }
    }
  };

  /**
   * Removes an item from the selection.
   */
  imce.deselectItem = function (Item) {
    if (Item.selected) {
      var arr = imce.selection;
      var i = $.inArray(Item, arr);
      Item.unsetState('selected');
      if (i !== -1) {
        arr.splice(i, 1);
        if (arr.length < 2) {
          imce.updatePreview();
        }
      }
    }
  };

  /**
   * Loads item uuids by ajax.
   */
  imce.loadItemUuids = function (items, callback) {
    var i;
    var Item;
    var missing = [];
    var loaded = [];
    for (i in items) {
      Item = items[i];
      if (Item && Item.isFile) {
        if (Item.uuid) {
          loaded.push(Item);
        }
        else {
          missing.push(Item);
        }
      }
    }
    // All loaded
    if (!missing.length) {
      if (callback) {
        callback(loaded);
      }
      return loaded;
    }
    // Load missing uuids
    return imce.ajaxItems('uuid', missing, {
      customComplete: function(xhr, status) {
        var path;
        var Item;
        var response = this.response;
        if (response && response.uuids) {
          for (path in response.uuids) {
            if (Item = imce.getItem(path)) {
              Item.uuid = response.uuids[path];
              loaded.push(Item);
            }
          }
        }
        if (callback) {
          callback(loaded);
        }
      }
    });
  };

  /**
   * Generate link/image html for the given items.
   */
   imce.itemsHtml = function (items, type, innerHtml, separator) {
    const lines = [];
    const isImg = type === 'image';
    for (const File of items) {
      // Image.
      if (isImg && File.isImageSource()) {
        lines.push(
          '<img src="' +
            File.getUrl() +
            '"' +
            (File.width ? ' width="' + File.width + '"' : '') +
            (File.height ? ' height="' + File.height + '"' : '') +
            ' data-entity-type="file" data-entity-uuid="' +
            (File.uuid || '') +
            '" alt="" />'
        );
      }
      // Link.
      else {
        // Use the innerHtml for the first link.
        const text = (!lines.length && innerHtml) || File.formatName();
        lines.push('<a href="' + File.getUrl() + '">' + text + '</a>');
      }
    }
    return lines.join(separator == null ? '<br />' : separator);
  };

  /**
   * Checks external application integration by URL parameters.
   *
   * Ex-1: http://example.com/imce?sendto=HANDLER
   * Creates a sendto operation that calls HANDLER(File, imceWin) of the parent window.
   * Ex-2: http://example.com/imce?urlField=FIELD-ID
   * Creates a sendto operation that fills the field in parent window with the selected file's URL.
   * Ex-3: http://example.com/imce?oninit=HANDLER
   * Calls HANDLER() with imce context when the UI is ready.
   */
  imce.checkIntegration = function () {
    var query = imce.getQuery();
    var handler;
    var urlField;
    var parentWin = window.opener || window.parent;
    if (imce.parentWin = parentWin) {
      // Check sendto handler
      if (handler = imce.resolveHandler(query.sendto, parentWin)) {
        imce.sendtoHandler = handler;
      }
      // Check url field
      else if (urlField = query.urlField) {
        if (urlField = parentWin.document.getElementById(urlField)) {
          imce.sendtoHandler = function (Item, win) {
            try {
              imce.parentWin.focus();
              (imce.parentWin.jQuery||$)(urlField).val(Item.getUrl()).trigger('blur').trigger('change').trigger('focus');
            }
            catch (err) {
              imce.delayError(err);
            }
            win.close();
          };
        }
      }
      // Check init handler
      if (handler = imce.resolveHandler(query.oninit, parentWin)) {
        imce.bind('init', handler);
      }
      // Store sendto type
      if (imce.sendtoHandler && query.type) {
        imce.sendtoType = query.type;
      }
    }
  };

  /**
   * Creates the sendto toolbar button.
   */
  imce.createSendtoTbb = function (title, desc) {
    if (imce.sendtoHandler && !imce.getTbb('sendto')) {
      return imce.addTbb('sendto', {
        title: title || Drupal.t('Select'),
        tooltip: desc || Drupal.t('Use the selected file.'),
        handler: function () {
          imce.runSendtoHandler();
        },
        icon: 'check'
      });
    }
  };

  /**
   * Runs custom sendto handler on the first selected item.
   */
  imce.runSendtoHandler = function (items) {
    var handler = imce.sendtoHandler;
    if (handler) {
      var Item;
      var imgType = imce.sendtoType === 'image';
      items = items || imce.getSelection();
      for (var i in items) {
        if (imce.owns(items, i)) {
          Item = items[i];
          if (imgType ? Item.isImageSource() : Item.isFile) {
            return handler(Item, window);
          }
        }
      }
    }
  };

  /**
   * Default handler for activateFolder event.
   */
  imce.defaultActivateFolder = function (Folder, oldFolder) {
    // Enable/disable toolbar buttons by permission.
    var i;
    var j;
    var Tbb;
    var perm;
    var disabled;
    var buttons = imce.toolbarButtons;
    for (i in buttons) {
      if (!imce.owns(buttons, i)) {
        continue;
      }
      Tbb = buttons[i];
      if (perm = Tbb.permission) {
        perm = perm.split('|');
        disabled = true;
        for (j in perm) {
          if (Folder.getPermission(perm[j])) {
            disabled = false;
            break;
          }
        }
        Tbb.setDisabled(disabled);
      }
    }
  };


  /**
   * Updates the active sort state in the content header.
   */
  imce.updateHeader = function () {
    var newsort = imce.activeFolder.activeSort;
    var oldsort = imce.activeSort || {};
    var el = imce.contentHeaderEl;
    // Check if the sort has changed.
    if (newsort && (newsort.key !== oldsort.key || newsort.desc !== oldsort.desc)) {
      // Deactivate existing column
      if (oldsort.key) {
        $('[data-sort="' + oldsort.key + '"]', el).removeClass('sorted ' + (oldsort.desc ? 'desc' : 'asc'));
      }
      // Activate new column
      $('[data-sort="' + newsort.key + '"]', el).addClass('sorted ' + (newsort.desc ? 'desc' : 'asc'));
      // Store the values
      imce.activeSort = newsort;
    }
  };

  /**
   * Schedule preview updating.
   */
  imce.updatePreview = function () {
    clearTimeout(imce.previewTimer);
    imce.previewTimer = setTimeout(imce.doUpdatePreview, 100);
  };

  /**
   * Set preview of currently selected item.
   */
  imce.doUpdatePreview = function () {
    imce.previewItem(imce.countSelection() === 1 ? imce.getLastSelected() : null);
  };

  /**
   * Sets/clears item preview.
   */
  imce.previewItem = function (Item) {
    var currentItem = imce.previewingItem;
    if (imce.previewingItem = Item) {
      $(imce.previewEl).html(Item.createPreviewEl());
      imce.trigger('previewItem', Item);
    }
    else if (currentItem) {
      imce.previewEl.innerHTML = '';
    }
  };

  /**
   * Schedule status update.
   */
  imce.updateStatus = function () {
    clearTimeout(imce.statusTimer);
    imce.statusTimer = setTimeout(imce.doUpdateStatus, 100);
  };

  /**
   * Updates active folder status.
   */
  imce.doUpdateStatus = function () {
    var Folder = imce.activeFolder;
    if (Folder) {
      $(imce.contentStatusEl).html(Folder.formatStatus());
    }
  };


  /**
   * Returns name filtering regexp.
   */
  imce.getNameFilter = function () {
    var filters = imce.getConf('name_filters', []);
    // Dot files
    if (!imce.getConf('allow_dot_files')) {
      filters.push('^\\.|\\.$');
    }
    return filters.length ? new RegExp(filters.join('|')) : false;
  };

  /**
   * Groups an array of items by parent folder and type.
   */
  imce.groupItems = function (items) {
    var i;
    var Item;
    var path;
    var selected;
    var key;
    var names;
    var group = {};
    for (i in items) {
      if (!imce.owns(items, i)) {
        continue;
      }
      Item = items[i];
      path = Item.parent.getPath();
      selected = group[path] = imce.owns(group, path) ? group[path] : {};
      key = Item.isFolder ? 'subfolders' : 'files';
      names = selected[key] = selected[key] || {};
      names[Item.name] = Item;
    }
    return group;
  };

  /**
   * Checks parent folder permissions of the given items.
   */
  imce.validatePermissions = function (items, filePerm, subfolderPerm) {
    var path;
    var Folder;
    var selected;
    var groups = imce.groupItems(items);
    for (path in groups) {
      if (!imce.owns(groups, path)) {
        continue;
      }
      Folder = imce.getFolder(path);
      selected = groups[path];
      // Check file permission if the selection contains files
      if (selected.files && (filePerm == null || !Folder.getPermission(filePerm))) {
        return false;
      }
      // Check folder permission if the selection contains folders
      if (selected.subfolders && (subfolderPerm == null || !Folder.getPermission(subfolderPerm))) {
        return false;
      }
    }
    return true;
  };

  /**
   * Checks if items contain any predefined folder.
   */
  imce.validatePredefinedPath = function (items) {
    var i;
    var Item;
    var Folder;
    for (i in items) {
      if (!imce.owns(items, i)) {
        continue;
      }
      Item = items[i];
      if (Item.isFolder) {
        if (Folder = Item.hasPredefinedPath()) {
          imce.setMessage(Drupal.t('%path is a predefined path and can not be modified.', {'%path': Folder.getPath()}));
          return false;
        }
      }
    }
    return true;
  };

  /**
   * Validates the number of items.
   */
  imce.validateCount = function (items) {
    if (!items.length) {
      imce.setMessage(Drupal.t('Please select a file.'));
      return false;
    }
    return true;
  };

  /**
   * Validates item extensions against an allowed list.
   */
  imce.validateExtensions = function (items, exts) {
    for (var i in items) {
      if (imce.owns(items, i) && !imce.validateExtension(items[i].ext, exts)) {
        return false;
      }
    }
    return true;
  };

  /**
   * Validates an extension against an allowed list.
   */
  imce.validateExtension = function (ext, exts) {
    if (!ext || $.inArray(ext.toLowerCase(), exts.toLowerCase().split(/[\s,]+/)) === -1) {
      imce.setMessage(Drupal.t('Only files with the following extensions are allowed: %files-allowed.', {'%files-allowed': exts}));
      return false;
    }
    return true;
  };

  /**
   * Validates a file name.
   */
  imce.validateFileName = function (name) {
    // Basic validation
    if (!name || name === '.' || name === '..' || !name.length || name.length > 240) {
      return false;
    }
    // Test name filters
    var regex = imce.getNameFilter();
    if (regex && regex.test(name)) {
      imce.setMessage(Drupal.t('%filename is not allowed.', {'%filename': name}));
      return false;
    }
    // Test chars forbidden in various operating systems.
    if (/^\s|\s$|[\/\\:\*\?\x22<>\|\x00-\x1F]/.test(name)) {
      imce.setMessage(Drupal.t('%filename contains invalid characters. Use only alphanumeric characters for better portability.', {'%filename': name}));
      return false;
    }
    return true;
  };

  /**
   * Validates min/max image dimensions.
   */
  imce.validateDimensions = function (items, width, height) {
    // Check min dimensions
    if (width < 1 || height < 1) {
      return false;
    }
    // Check max dimensions.
    var maxwidth = imce.getConf('maxwidth');
    var maxheight = imce.getConf('maxheight');
    if (maxwidth && width > maxwidth || maxheight && height > maxheight) {
      imce.setMessage(Drupal.t('Image dimensions must be smaller than %dimensions pixels.', {'%dimensions': maxwidth + 'x' + maxwidth}));
      return false;
    }
    return true;
  };

  /**
   * Checks if all the selected items are images.
   */
  imce.validateImageTypes = function (items) {
    // Require width when lazy dimensions is not enabled.
    // Otherwise require an image extension.
    const lazy = imce.getConf('lazy_dimensions', false);
    for (const Item of items) {
      if ((!lazy && !Item.width) || !Item.hasImageExtension()) {
        imce.setMessage(
          Drupal.t('%name is not a supported image type.', {
            '%name': Item.name,
          }),
        );
        return false;
      }
    }
    return true;
  };

  /**
   * Checks if a file name has image extension.
   *
   * @param {string} name
   *  File name.
   *
   * @return {boolean}
   *  Boolean.
   */
  imce.hasImageExtension = function (name) {
    let re = imce.imageExtensionsRE;
    if (!re) {
      let exts = imce.getConf('image_extensions', 'jpg jpeg png gif webp avif');
      exts = exts.trim().replace(/ +/g, '|');
      re = new RegExp(`\\.(${exts})$`, 'i');
      imce.imageExtensionsRE = re;
    }
    return re.test(`${name}`);
  };

  /**
   * Keydown event for the file manager.
   */
  imce.eFmKeydown = function (event) {
    return imce.eFireShortcut.call(this, event);
  };

  /**
   * Refresh handler for the file manager.
   */
  imce.eFmRefresh = function () {
    imce.activeFolder.load();
  };

  /**
   * Mousedown event for folder tree.
   */
  imce.eTreeMousedown = function (event) {
    // Manually focus as the browser default might have been prevented.
    this.focus();
  };

  /**
   * Touchstart event for folder tree.
   */
  imce.eTreeTouchstart = function (event) {
    this.focus();
  };

  /**
   * Keydown event for folder tree.
   */
  imce.eTreeKeydown = function (event) {
    return imce.eFireShortcut.call(this, event, 'tree');
  };

  /**
   * Tree shortcut: Enter.
   */
  imce.eTreeEnter = function () {
    imce.activeFolder.open();
  };

  /**
   * Tree shortcut: UP.
   */
  imce.eTreeUp = function (e) {
    var Folder = imce.activeFolder;
    var prvEl;
    var prvFolder;
    if (prvEl = Folder.branchEl.previousSibling) {
      if (prvFolder = prvEl.Folder) {
        while (prvFolder.expanded) {
          if (prvEl = prvFolder.subtreeEl.lastChild) {
            prvFolder = prvEl.Folder;
          }
        }
      }
    }
    else {
      prvFolder = Folder.parent;
    }
    if (prvFolder) {
      prvFolder.activate();
      imce.scrollToEl(prvFolder.branchEl, imce.treeEl);
    }
  };

  /**
   * Tree shortcut: DOWN.
   */
  imce.eTreeDown = function (e) {
    var Folder = imce.activeFolder;
    var nextEl;
    var nextFolder;
    if (Folder.expanded && (nextEl = Folder.subtreeEl.firstChild)) {
      nextFolder = nextEl.Folder;
    }
    else {
      // noinspection Eslint
      do {
        if (nextEl = Folder.branchEl.nextSibling) {
          nextFolder = nextEl.Folder;
          break;
        }
      } while (Folder = Folder.parent);
    }
    if (nextFolder) {
      nextFolder.activate();
      imce.scrollToEl(nextFolder.branchEl, imce.treeEl);
    }
  };

  /**
   * Tree shortcut: LEFT/RIGHT.
   */
  imce.eTreeLR = function (e) {
    var Folder = imce.activeFolder;
    if (e.keyCode === 39 ^ Folder.expanded) {
      $(Folder.branchToggleEl).trigger('click');
    }
  };

  /**
   * Mousedown event for tree resizer.
   */
  imce.eTreeResizerMousedown = function (event) {
    return imce.eTreeResizerDown.call(this, imce.eFix(event));
  };

  /**
   * Touch start event for tree resizer.
   */
  imce.eTreeResizerTouchstart = function (event) {
    return imce.eCommonTouchstart(event, imce.eTreeResizerDown, this);
  };

  /**
   * Common Down event for tree resizer.
   */
  imce.eTreeResizerDown = function (e, isTouch) {
    this.startX = e.pageX;
    this.startW = $(imce.treeEl).width();
    this.maxW = this.startW + $(imce.contentEl).width();
    imce.bindDragDrop(imce.eTreeResizerDrag, null, null, isTouch);
    return false;
  };

  /**
   * Drag event for tree resizer.
   */
  imce.eTreeResizerDrag = function (e) {
    var el = imce.treeResizerEl;
    $(imce.treeEl).width(Math.min(el.maxW, Math.max(el.startW + e.pageX - el.startX, 0)));
    e.preventDefault();
  };

  /**
   * Mousedown event for content area.
   */
  imce.eContentMousedown = function (event) {
    // Manually focus as the browser default might have been prevented.
    this.focus();
  };

  /**
   * Touchstart event for content area.
   */
  imce.eContentTouchstart = function (event) {
    this.focus();
  };

  /**
   * Keydown event for content area.
   */
  imce.eContentKeydown = function (event) {
    return imce.eFireShortcut.call(this, event, 'content');
  };

  /**
   * Scroll event for content area.
   */
  imce.eContentScroll = function (event) {
    imce.updateContentPositions();
    setTimeout(imce.updateContentPositions);
  };

  /**
   * Click event for content header.
   */
  imce.eContentHeaderClick = function (event) {
    var key;
    var e = imce.eFix(event);
    var Folder = imce.activeFolder;
    var sort = Folder.activeSort || {};
    if (key = e.target.getAttribute('data-sort')) {
      Folder.sortItems(key, key === sort.key ? !sort.desc : sort.desc);
    }
  };

  /**
   * Update content header position on content scroll.
   */
  imce.updateContentPositions = function () {
    var top = imce.contentEl.scrollTop;
    imce.contentHeaderEl.style.top = top + 'px';
    imce.contentStatusEl.style.bottom = -top + 'px';
  };

  /**
   * Content shortcut: ENTER.
   */
  imce.eContentEnter = function (e) {
    var Item = imce.getLastSelected();
    if (Item) {
      Item.dblClick();
    }
  };

  /**
   * Content shortcut: Ctrl+A.
   */
  imce.eContentCtrlA = function (e) {
    imce.selectAll();
  };

  /**
   * Content shortcut: LEFT/RIGHT/UP/DOWN
   */
  imce.eContentArrow = function (e) {
    var Item;
    var i = 0;
    var Folder = imce.activeFolder;
    var key = e.keyCode;
    if (Item = imce.getLastSelected()) {
      i = Folder.indexOf(Item) + (key % 2 ? key - 38 : imce.countElPerRow(Item.el) * (key - 39));
    }
    if (Item = Folder.getItemAt(i)) {
      Item.click(e);
      Item.scrollIntoView();
    }
  };

  /**
   * Mousedown event for body resizer.
   */
  imce.eBodyResizerMousedown = function (event) {
    return imce.eBodyResizerDown.call(this, imce.eFix(event));
  };

  /**
   * Touch start event for body resizer.
   */
  imce.eBodyResizerTouchstart = function (event) {
    return imce.eCommonTouchstart(event, imce.eBodyResizerDown, this);
  };

  /**
   * Common Down event for body resizer.
   */
  imce.eBodyResizerDown = function (e, isTouch) {
    this.startY = e.pageY;
    this.startH = $(imce.bodyEl).height();
    this.maxH = this.startH + $(imce.previewEl).height();
    imce.bindDragDrop(imce.eBodyResizerDrag, null, null, isTouch);
    return false;
  };

  /**
   * Drag event for body resizer.
   */
  imce.eBodyResizerDrag = function (e) {
    var el = imce.bodyResizerEl;
    var bodyH = Math.min(Math.max(el.startH + e.pageY - el.startY, 0), el.maxH);
    $(imce.bodyEl).height(bodyH);
    $(imce.previewEl).height(el.maxH - bodyH);
    e.preventDefault();
  };

  /**
   * Beforeunload event for window.
   */
  imce.eWinBeforeunload = function (e) {
    // Store active sort.
    var data = {};
    if (data.activeSort = imce.activeSort) {
      imce.trigger('storeLocalData', data);
      try {
        localStorage.setItem('imce.local', JSON.stringify(data));
      }
      catch (err) {
        imce.delayError(err);
      }
    }
  };

  /**
   * Resize event for window.
   */
  imce.eWinResize = function (e) {
    var pdiff;
    var diff = imce.getWindowSize().height - imce.fmEl.offsetHeight;
    // Distribute the excess space to the body and preview elements.
    if (diff) {
      var $bodyEl = $(imce.bodyEl);
      var $prvEl = $(imce.previewEl);
      if ($prvEl[0].offsetHeight) {
        pdiff = parseInt(diff / 2);
        diff -= pdiff;
        $prvEl.height($prvEl.height() + pdiff);
      }
      $bodyEl.height($bodyEl.height() + diff);
    }
  };


  /**
   * Binds an handler by type.
   */
  imce.bind = function (type, handler) {
    var events = imce.events;
    var handlers = events[type];
    if (!handlers) {
      handlers = events[type] = {};
    }
    handlers['' + handler] = handler;
  };

  /**
   * Unbinds an handler by type.
   */
  imce.unbind = function (type, handler) {
    var events = imce.events;
    var handlers = events[type];
    if (handlers) {
      if (1 in arguments) {
        delete handlers['' + handler];
      }
      else {
        delete events[type];
      }
    }
  };

  /**
   * Triggers handlers by type.
   */
  imce.trigger = function (type) {
    var i;
    var handler;
    var handlers = imce.events[type];
    var ret = [];
    if (handlers) {
      for (i in handlers) {
        if (handler = handlers[i]) {
          if (handler.apply) {
            ret.push(handler.apply(imce, Array.prototype.slice.call(arguments, 1)));
          }
        }
      }
    }
    return ret;
  };


  /**
   * Adds a shortcut handler to an area.
   */
  imce.addShortcut = function (shortcut, handler, area) {
    var shortcuts;
    if (shortcuts = imce.getAreaShortcuts(area)) {
      shortcuts[shortcut.toUpperCase()] = handler;
    }
  };

  /**
   * Returns a shortcut handler.
   */
  imce.getShortcut = function (shortcut, area) {
    var shortcuts;
    if (shortcuts = imce.getAreaShortcuts(area)) {
      return shortcuts[shortcut.toUpperCase()];
    }
  };

  /**
   * Removes a shortcut handler.
   */
  imce.removeShortcut = function (shortcut, area) {
    var shortcuts;
    if (shortcuts = imce.getAreaShortcuts(area)) {
      delete shortcuts[shortcut.toUpperCase()];
    }
  };

  /**
   * Executes a shortcut handler.
   * Returns true if shortcut exists and is executed successfully.
   */
  imce.fireShortcut = function (shortcut, area) {
    var handler = imce.getShortcut(shortcut, area);
    if (handler) {
      // DOM element
      if (handler.click) {
        handler.click();
        return true;
      }
      // Callback
      if (handler.apply) {
        // Shortcuts returning false are considered disabled.
        return handler.apply(this, Array.prototype.slice.call(arguments, 2)) !== false;
      }
    }
  };

  /**
   * Returns a shortcut handler.
   */
  imce.getAreaShortcuts = function (area) {
    if (!area) {
      area = 'fm';
    }
    return imce.shortcuts[area];
  };


  /**
   * Builds a shortcut string from an event.
   */
  imce.eBuildShortcut = function (e) {
    var symbol;
    var key = e.keyCode;
    var shortcut = '';
    if (key && (symbol = imce.getKeySymbols(key))) {
      if (e.ctrlKey) {
        shortcut += 'CTRL+';
      }
      if (e.altKey) {
        shortcut += 'ALT+';
      }
      if (e.shiftKey) {
        shortcut += 'SHIFT+';
      }
      shortcut += symbol;
    }
    return shortcut;
  };

  /**
   * Event helper for imce shortcut firing.
   */
  imce.eFireShortcut = function (event, area) {
    var e = event || window.event;
    var shortcut = imce.eBuildShortcut(e);
    // Prevent default if shortcut is executed.
    if (shortcut) {
      e = $.event.fix(e);
      if (imce.fireShortcut.call(this, shortcut, area, e)) {
        e.stopPropagation();
        return false;
      }
    }
  };

  /**
   * Returns key symbols allowed in shortcuts.
   */
  imce.getKeySymbols = function (key) {
    var i;
    var symbols = imce.keySymbols;
    if (!symbols) {
      // Custom keys
      symbols = imce.keySymbols = {
        8: 'BACKSPACE',
        9: 'TAB',
        13: 'ENTER',
        27: 'ESC',
        32: 'SPACE',
        37: 'LEFT',
        38: 'UP',
        39: 'RIGHT',
        40: 'DOWN',
        46: 'DEL'
      };
      // Add numbers
      for (i = 0; i < 10; i++) {
        symbols[48 + i] = '' + i;
      }
      // Add letters
      for (i = 65; i < 91; i++) {
        symbols[i] = String.fromCharCode(i);
      }
      // Add function keys
      for (i = 1; i < 13; i++) {
        symbols[111 + i] = 'F' + i;
      }
    }
    return (0 in arguments) ? symbols[key] : symbols;
  };


  /**
   * Creates an ajax request for a specific operation.
   */
  imce.ajax = function (jsop, opt) {
    return $.ajax(imce.ajaxPrepare(jsop, opt));
  };

  /**
   * Prepares ajax options.
   */
  imce.ajaxPrepare = function (jsop, opt) {
    // Prepare data
    var path;
    var Folder = opt && opt.activeFolder != null ? opt.activeFolder : imce.activeFolder;
    var data = {jsop: jsop, token: imce.getConf('token')};
    if (Folder) {
      if (path = Folder.getPath()) {
        data.active_path = path;
      }
    }
    // Extend defaults
    return $.extend(true, imce.ajaxDefaults(), {data: data, activeFolder: Folder}, opt);
  };

  /**
   * Returns ajax default options.
   */
  imce.ajaxDefaults = function () {
    return {
      url: imce.getConf('ajax_url'),
      type: 'POST',
      dataType: 'json',
      beforeSend: imce.ajaxBeforeSend,
      success: imce.ajaxSuccess,
      error: imce.ajaxError,
      complete: imce.ajaxComplete
    };
  };

  /**
   * Creates an ajax request for a specific operation on the given items.
   */
  imce.ajaxItems = function (jsop, items, opt) {
    return imce.ajax(jsop, $.extend(true, imce.ajaxItemsOpt(items), opt));
  };

  /**
   * Creates an ajax options object including the items as the selection data.
   */
  imce.ajaxItemsOpt = function (items) {
    return {data: {selection: imce.getItemPaths(items)}};
  };

  /**
   * Default before send handler.
   */
  imce.ajaxBeforeSend = function (xhr, opt) {
    var handler;
    var Folder;
    if (handler = opt.customBeforeSend) {
      if (handler.apply(this, arguments) === false) {
        opt.activeFolder = null;
        return false;
      }
    }
    if (Folder = opt.activeFolder) {
      Folder.setBusy(true);
    }
  };

  /**
   * Default ajax success handler.
   */
  imce.ajaxSuccess = function (response, status) {
    var handler;
    var opt = this;
    // Make the response available in complete handlers.
    opt.response = response;
    imce.ajaxProcessResponse(response);
    if (handler = opt.customSuccess) {
      handler.apply(opt, arguments);
    }
  };

  /**
   * Default ajax complete handler.
   */
  imce.ajaxComplete = function (xhr, status) {
    var Folder;
    var handler;
    var opt = this;
    if (Folder = opt.activeFolder) {
      Folder.setBusy(false);
    }
    if (handler = opt.customComplete) {
      handler.apply(opt, arguments);
    }
    opt.response = opt.activeFolder = null;
  };

  /**
   * Default ajax error handler.
   */
  imce.ajaxError = function (xhr, status, e) {
    if (status !== 'abort') {
      imce.setMessage('<pre class="imce-ajax-error">' + Drupal.checkPlain(imce.ajaxErrorMessage(xhr, this.url)) + '</pre>');
    }
  };

  /**
   * Processes the ajax response.
   */
  imce.ajaxProcessResponse = function (response) {
    if (response) {
      imce.ajaxProcessRemoved(response);
      imce.ajaxProcessAdded(response);
      imce.ajaxProcessMessages(response);
    }
  };

  /**
   * Processes the added items in the response.
   */
  imce.ajaxProcessAdded = function (response) {
    var path;
    var Folder;
    var added;
    if (added = response.added) {
      for (path in added) {
        if (Folder = imce.addFolder(path)) {
          Folder.addContent(added[path], true);
          imce.contentEl.focus();
        }
      }
    }
  };

  /**
   * Processes the removed items in the response.
   */
  imce.ajaxProcessRemoved = function (response) {
    var i;
    var Item;
    var paths = response.removed;
    if (paths) {
      for (i in paths) {
        if (Item = imce.getItem(paths[i])) {
          Item.remove();
        }
      }
    }
  };

  /**
   * Processes the messages in the response.
   */
  imce.ajaxProcessMessages = function (response) {
    var i;
    var type;
    var msgs = response.messages;
    if (msgs) {
      for (type in msgs) {
        if (imce.owns(msgs, type)) {
          for (i in msgs[type]) {
            if (msgs[type].hasOwnProperty(i)) {
              imce.setMessage(msgs[type][i], type);
            }
          }
        }
      }
    }
  };

  /**
   * Generates an ajax error message.
   */
  imce.ajaxErrorMessage = function (xhr, url) {
    var msg = Drupal.t('An AJAX HTTP error occurred.');
    msg += '\n' + Drupal.t('Path: !uri', {'!uri': url});
    msg += '\n' + Drupal.t('HTTP Result Code: !status', {'!status': xhr.status || 0});
    msg += '\n' + Drupal.t('StatusText: !statusText', {'!statusText': xhr.statusText || 'N/A'});
    msg += '\n' + Drupal.t('ResponseText: !responseText', {'!responseText': xhr.responseText || 'N/A'});
    return msg;
  };

  /**
   * Returns an array of item paths.
   */
  imce.getItemPaths = function (items) {
    return $.map(items, imce.getItemPath);
  };

  /**
   * Returns the path of an item.
   */
  imce.getItemPath = function (Item) {
    return Item.getPath();
  };


  /**
   * Set a status message.
   */
  imce.setMessage = function (msg, type) {
    if (!type) {
      type = 'error';
    }
    var mq = imce.messageQueue;
    var len = mq.length;
    var msgId = msg + ':' + type;
    // Skip if it's identical to the last message
    if (len && mq[len - 1].msgId === msgId) {
      return false;
    }
    // Add the message
    mq[len] = imce.createMessageEl(msg, type);
    mq[len].msgId = msgId;
    // Schedule the processing at a later time to queue new messages.
    if (!imce.pmqScheduled) {
      imce.pmqScheduled = setTimeout(imce.processMessageQueue, 100);
    }
    return false;
  };

  /**
   * Process message queue.
   */
  imce.processMessageQueue = function () {
    var mq = imce.messageQueue;
    if (mq.length) {
      // Display all messages
      $(imce.createMessagePopupEl()).html(mq).fadeIn(200);
      // Empty array.
      mq.length = 0;
      // Mousedown close
      $(document).on('mousedown', imce.eMPopDocMousedown);
      // Auto close
      imce.mPopCloseTimer = setTimeout(imce.mPopClose, 2500);
    }
  };

  /**
   * Closes currently open message popup.
   */
  imce.mPopClose = function () {
    // Time up but still hovering. Do not close. A new timer will be set on mouseout.
    if (imce.mPopHovering) {
      imce.mPopCloseTimerUp = 1;
      return imce.mPopCloseTimerUp;
    }
    // Time up or mousedown
    clearTimeout(imce.mPopCloseTimer);
    imce.mPopCloseTimerUp = 0;
    $(document).off('mousedown', imce.eMPopDocMousedown);
    $(imce.messagePopupEl).fadeOut(400, imce.processMessageQueueNext);
  };

  /**
   * Continue processing the remaining messages.
   */
  imce.processMessageQueueNext = function () {
    imce.pmqScheduled = 0;
    if (imce.messageQueue.length) {
      imce.pmqScheduled = setTimeout(imce.processMessageQueue, 250);
    }
  };

  /**
   * Mouseover event for message popup.
   */
  imce.eMPopMouseenter = function (e) {
    imce.mPopHovering = 1;
    // Clear the shorter timer set on mouseleave
    if (imce.mPopCloseTimerUp) {
      clearTimeout(imce.mPopCloseTimer);
    }
  };

  /**
   * Mouseout event for message popup.
   */
  imce.eMPopMouseleave = function (e) {
    imce.mPopHovering = 0;
    // Set a shorter close timer if the long time is up
    if (imce.mPopCloseTimerUp) {
      imce.mPopCloseTimer = setTimeout(imce.mPopClose, 2000);
    }
  };

  /**
   * Mousedown event for document in order to close message popup.
   */
  imce.eMPopDocMousedown = function (e) {
    // Close the popup if the mousedown is outside of it.
    if (!imce.mPopHovering) {
      imce.mPopClose();
    }
  };

  /**
   * Creates a message element.
   */
  imce.createMessageEl = function (msg, type) {
    var el = imce.createEl('<div class="imce-message imce-ficon"><div class="imce-message-content"></div></div>');
    el.className += ' ' + type;
    el.firstChild.innerHTML = msg;
    return el;
  };

  /**
   * Creates the message popup element.
   */
  imce.createMessagePopupEl = function () {
    var el = imce.messagePopupEl;
    if (!el) {
      el = imce.messagePopupEl = imce.createLayer('imce-message-popup', imce.fmEl);
      $(el).on('mouseenter', imce.eMPopMouseenter).on('mouseleave', imce.eMPopMouseleave);
    }
    return el;
  };


  /**
   * Checks a permission in a folder conf.
   */
  imce.permissionInFolderConf = function (permission, folderConf) {
    var permissions = folderConf && folderConf.permissions;
    return !!(permissions && ((permission in permissions) ? permissions[permission] : permissions.all));
  };

  /**
   * Checks if a permission exists in any of the predefined folders.
   */
  imce.hasPermission = function (permission, conf) {
    var i;
    var folders = (conf || imce.conf).folders;
    if (folders) {
      for (i in folders) {
        if (imce.permissionInFolderConf(permission, folders[i])) {
          return true;
        }
      }
    }
    return false;
  };

  /**
   * Sorting helpers.
   */
  imce.sortText = function (a, b) {
    return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
  };
  imce.sortNum = function (a, b) {
    return (a || 0) - (b || 0);
  };
  imce.sortNumericProperty = function (a, b, prop) {
    // Do not change sort within folders
    var result = (a.isFolder ? -1 : 1);
    if (a.isFolder === b.isFolder) {
      result = imce.sortNum(a[prop] || 0, b[prop] || 0);
    }
    return result;
  };
  imce.sortBranchName = function (a, b) {
    return imce.sortText(a.name, b.name);
  };

  /**
   * Property sorters.
   */
  imce.sorters.name = function (a, b) {
    var result = (a.isFolder ? -1 : 1);
    if (a.isFolder === b.isFolder) {
      result = imce.sortText(a.name, b.name);
    }
    return result;
  };
  imce.sorters.date = function (a, b) {
    return imce.sortNumericProperty(a, b, 'date');
  };
  imce.sorters.size = function (a, b) {
    return imce.sortNumericProperty(a, b, 'size');
  };
  imce.sorters.width = function (a, b) {
    return imce.sortNumericProperty(a, b, 'width');
  };
  imce.sorters.height = function (a, b) {
    return imce.sortNumericProperty(a, b, 'height');
  };
  imce.sorters.ext = function (a, b) {
    var result = (a.isFolder ? -1 : 1);
    if (a.isFolder === b.isFolder) {
      result = (a.isFolder ? 0 : imce.sortText(a.ext || '', b.ext || ''));
    }
    return result;
  };

  /**
   * Splits a path into dirpath and filename.
   */
  imce.splitPath = function (path) {
    if (typeof path === 'string' && path !== '') {
      var parts = path.split('/');
      var filename = parts.pop();
      var dirpath = parts.join('/');
      if (filename !== '') {
        return [dirpath === '' ? '.' : dirpath, filename];
      }
    }
  };

  /**
   * Creates a file path by joining a folder path and a filename.
   */
  imce.joinPaths = function (dirpath, filename) {
    if (dirpath === '.') {
      return filename;
    }
    if (filename === '.') {
      return dirpath;
    }
    if (dirpath.substring(dirpath.length - 1) !== '/') {
      dirpath += '/';
    }
    return dirpath + filename;
  };

  /**
   * Returns query parameters from the current URL.
   */
  imce.getQuery = function (name) {
    var i;
    var part;
    var parts;
    var str;
    var query = imce.query;
    if (!query) {
      query = imce.query = {};
      if (str = location.search) {
        parts = str.substring(1).split('&');
        for (i in parts) {
          if (imce.owns(parts, i)) {
            part = parts[i].split('=');
            query[imce.decode(part[0])] = part[1] ? imce.decode(part[1]) : '';
          }
        }
      }
    }
    return name ? query[name] : query;
  };

  /**
   * Wrapper around decodeURIComponent.
   * Avoids malformed uri exception.
   */
  imce.decode = function (str) {
    try {
      str = decodeURIComponent(str);
    }
    catch (err) {
      imce.delayError(err);
    }
    return str;
  };

  /**
   * Throws an error after a minimum delay.
   */
  imce.delayError = function (err) {
    setTimeout(function () {
      throw err;
    });
  };

  /**
   * Formats item date.
   */
  imce.formatDate = function (timestamp, dayOnly) {
    var D;
    var p0;
    var ret = '';
    if (timestamp) {
      D = new Date(timestamp * 1000);
      p0 = imce.prependZero;
      ret = D.getFullYear() + '-' + p0(D.getMonth() + 1) + '-' + p0(D.getDate());
      if (!dayOnly) {
        ret += ' ' + p0(D.getHours()) + ':' + p0(D.getMinutes());
      }
    }
    return ret;
  };

  /**
   * Formats item size.
   */
  imce.formatSize = function (size) {
    if (size == null) {
      return '';
    }
    if (!size || size < 100) {
      return Drupal.formatPlural(size, '1 byte', '@count bytes', {'@count': size || 0});
    }
    if (size < 1048576) {
      return Drupal.t('@size KB', {'@size': imce.round(size / 1024, 1)});
    }
    return Drupal.t('@size MB', {'@size': imce.round(size / 1048576, 1)});
  };

  /**
   * Formats content items status.
   */
  imce.formatItemsStatus = function (count, size) {
    return Drupal.t('!items (!size)', {
      '!items': Drupal.formatPlural(count, '1 item', '@count items'),
      '!size': imce.formatSize(size)
    });
  };

  /**
   * Prepends 0 to numbers smaller than 10.
   */
  imce.prependZero = function (num) {
    return num < 10 ? '0' + num : num;
  };

  /**
   * Rounds a number to the given precision
   */
  imce.round = function (num, precision) {
    var n = Math.pow(10, precision);
    return Math.round(num * n) / n;
  };

  /**
   * Returns the extension of a file name.
   */
  imce.getExt = function (name) {
    var pos = name.lastIndexOf('.');
    return pos === -1 ? '' : name.substring(pos + 1);
  };

  /**
   * Checks if an object owns a property.
   */
  imce.owns = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  };

  /**
   * Returns the first item that has a property.
   */
  imce.getFirstItem = function (items, prop, state) {
    var i;
    var item;
    if (typeof state === "undefined") {
      state = true;
    }
    for (i in items) {
      if (imce.owns(items, i)) {
        item = items[i];
        if (!prop || (item[prop] ? state : !state)) {
          return item;
        }
      }
    }
  };

  /**
   * Resolves a string to a handler under the given scope.
   */
  imce.resolveHandler = function (str, scope) {
    if (!str) {
      return;
    }
    var i;
    var obj = scope || window;
    var parts = str.split('.');
    var len = parts.length;
    for (i = 0; i < len && (obj = obj[parts[i]]); i++) {
      // empty
    }
    return i === len && obj && obj.call && obj.apply ? obj : false;
  };

  /**
   * Creates a DOM element from html string.
   */
  imce.createEl = function (html) {
    var el;
    var div = imce._div;
    if (!div) {
      div = imce._div = document.createElement('div');
    }
    div.innerHTML = html;
    el = div.firstChild;
    div.removeChild(el);
    return el;
  };

  /**
   * Creates a layer element.
   */
  imce.createLayer = function (cname, parent) {
    var layer = imce.createEl('<div class="imce-layer"></div>');
    if (cname) {
      layer.className += ' ' + cname;
    }
    if (parent !== false) {
      (parent || document.body).appendChild(layer);
    }
    return layer;
  };

  /**
   * Removes element without removing events.
   */
  imce.removeEl = function (el) {
    var parent = el.parentNode;
    if (parent) {
      parent.removeChild(el);
    }
  };

  /**
   * Returns window inner size.
   */
  imce.getWindowSize = function () {
    return {
      width: window.innerWidth || document.documentElement.clientWidth,
      height: window.innerHeight || document.documentElement.clientHeight
    };
  };

  /**
   * Returns scroll values of the window.
   */
  imce.getWindowScroll = function () {
    if (typeof window.pageXOffset === "undefined") {
      var el = document.documentElement;
      return {left: el.scrollLeft, top: el.scrollTop};
    }
    return {left: window.pageXOffset, top: window.pageYOffset};
  };

  /**
   * Fixes and converts an event into jQuery event.
   */
  imce.eFix = function (event) {
    return $.event.fix(event || window.event);
  };

  /**
   * Scroll an element into view inside the scrollable wrapper.
   */
  imce.scrollToEl = function (el, wrpEl, diffTop, diffBottom) {
    if (el.offsetWidth && wrpEl.scrollHeight > wrpEl.clientHeight) {
      var elTop = $(el).offset().top;
      var elBottom = elTop + el.offsetHeight;
      var wrpTop = $(wrpEl).offset().top;
      var wrpBottom = wrpTop + wrpEl.offsetHeight;
      wrpTop += diffTop || 0;
      wrpBottom -= diffBottom || 0;
      // Check top positions
      if (elTop < wrpTop) {
        wrpEl.scrollTop -= wrpTop - elTop + 10;
      }
      else if (wrpBottom < elBottom) {
        // Consider el height might be bigger than the wrapper height.
        // Get the minimum among top space and required scroll.
        wrpEl.scrollTop += Math.min(elBottom - wrpBottom + 10, elTop - wrpTop - 10);
      }
    }
  };

  /**
   * Returns number of the elements that can fit in a row inside the parent.
   */
  imce.countElPerRow = function (el) {
    return Math.max(1, parseInt(el.parentNode.clientWidth / $(el).outerWidth(true)));
  };

  /**
   * Makes the element stay inside window boundaries.
   */
  imce.fixPosition = function (el) {
    var pos = $(el).offset();
    var winSize = imce.getWindowSize();
    var winScroll = imce.getWindowScroll();
    var scrollbar = 18;
    var extraX = pos.left - winScroll.left + el.offsetWidth - winSize.width + scrollbar;
    var extraY = pos.top - winScroll.top + el.offsetHeight - winSize.height + scrollbar;
    // Shift to left
    if (extraX > 0) {
      el.style.left = Math.max(0, pos.left - extraX) + 'px';
    }
    // Shift to top
    if (extraY > 0) {
      el.style.top = Math.max(0, pos.top - extraY) + 'px';
    }
  };


  /**
   * Bind drag drop callbacks to the document
   */
  imce.bindDragDrop = function (drag, drop, data, isTouch) {
    var edata = {drag: drag, drop: drop, data: data, isTouch: isTouch};
    $(document).on(isTouch ? 'touchmove' : 'mousemove', edata, imce.eDocDrag).on(isTouch ? 'touchend' : 'mouseup', edata, imce.eDocDrop);
  };

  /**
   * Default drag event for document which is set by imce.bindDragDrop
   */
  imce.eDocDrag = function (e) {
    var edata = e.data;
    // Call custom drag event if set.
    if (edata.drag) {
      // Fix touch event
      if (edata.isTouch) {
        e = imce.eTouchFix(e, e.originalEvent.changedTouches[0]);
      }
      return edata.drag.call(this, e);
    }
  };

  /**
   * Default drop event for document which is set by imce.bindDragDrop
   */
  imce.eDocDrop = function (e) {
    var edata = e.data;
    $(document).off(edata.isTouch ? 'touchmove' : 'mousemove', imce.eDocDrag).off(edata.isTouch ? 'touchend' : 'mouseup', imce.eDocDrop);
    // Call custom drop event if set.
    if (edata.drop) {
      // Fix touch event
      if (edata.isTouch) {
        e = imce.eTouchFix(e, e.originalEvent.changedTouches[0]);
      }
      return edata.drop.call(this, e);
    }
  };

  /**
   * Fix touch events
   */
  imce.eTouchFix = function (e, touch) {
    // Make sure e is a jquery event object that is writable.
    e = $.event.fix(e);
    if (touch && typeof touch.pageX !== "undefined") {
      e.pageX = touch.pageX;
      e.pageY = touch.pageY;
      e.clientX = touch.clientX;
      e.clientY = touch.clientY;
    }
    return e;
  };

  /**
   * Common touchstart event.
   */
  imce.eCommonTouchstart = function (event, callback, context) {
    var touch;
    var touches = event.changedTouches;
    // Skip event for multi-touch
    if (touches && (touch = touches[0]) && !touches[1]) {
      if (callback && callback.call) {
        return callback.call(context || this, imce.eTouchFix(event, touch), true);
      }
      // Prevent default.
      return false;
    }
  };

})(jQuery, Drupal);
