<commit_before>var lbry = {
  isConnected: false,
  rootPath: '.',
  daemonConnectionString: 'http://localhost:5279/lbryapi',
  colors: {
    primary: '#155B4A'
  },
  defaultClientSettings: {
    showNsfw: false,
  }
};

lbry.jsonrpc_call = function (connectionString, method, params, callback, errorCallback, connectFailedCallback, timeout) {
  var xhr = new XMLHttpRequest;
  if (typeof connectFailedCallback !== 'undefined') {
    if (timeout) {
      xhr.timeout = timeout;
    }

    xhr.addEventListener('error', function (e) {
      connectFailedCallback(e);
    });
    xhr.addEventListener('timeout', function() {
      connectFailedCallback(new Error('XMLHttpRequest connection timed out'));
    })
  }
  xhr.addEventListener('load', function() {
    var response = JSON.parse(xhr.responseText);

    if (response.error) {
      if (errorCallback) {
        errorCallback(response.error);
      } else {
        var errorEvent = new CustomEvent('unhandledError', {
          detail: {
            connectionString: connectionString,
            method: method,
            params: params,
            code: response.error.code,
            message: response.error.message,
            data: response.error.data
          }
        });
        document.dispatchEvent(errorEvent)
      }
    } else if (callback) {
      callback(response.result);
    }
  });

  if (connectFailedCallback) {
    xhr.addEventListener('error', function (event) {
      connectFailedCallback(event);
    });
  } else {
    xhr.addEventListener('error', function (event) {
      var errorEvent = new CustomEvent('unhandledError', {
        detail: {
          connectionString: connectionString,
          method: method,
          params: params,
          code: xhr.status,
          message: 'Connection to API server failed'
        }
      });
      document.dispatchEvent(errorEvent);
    });
  }

  xhr.open('POST', connectionString, true);
  xhr.send(JSON.stringify({
    'jsonrpc': '2.0',
    'method': method,
    'params': params,
    'id': 0
  }));
}

lbry.call = function (method, params, callback, errorCallback, connectFailedCallback) {
  lbry.jsonrpc_call(lbry.daemonConnectionString, method, [params], callback, errorCallback, connectFailedCallback);
}

//core
lbry.connect = function(callback)
{
  // Check every half second to see if the daemon's running.
  // Returns true to callback once connected, or false if it takes too long and we give up.
  function checkDaemonRunning(tryNum=0) {
    lbry.daemonRunningStatus(function (runningStatus) {
      if (runningStatus) {
        lbry.isConnected = true;
        callback(true);
      } else {
        if (tryNum <= 600) { // Move # of tries into constant or config option
          setTimeout(function () {
            checkDaemonRunning(tryNum + 1);
          }, 500);
        } else {
          callback(false);
        }
      }
    });
  }
  checkDaemonRunning();
}

lbry.daemonRunningStatus = function (callback) {
  // Returns true/false whether the daemon is running (i.e. fully conncected to the network),
  // or null if the AJAX connection to the daemon fails.

  lbry.call('is_running', {}, callback, null, function () {
    callback(null);
  });
};

lbry.getDaemonStatus = function (callback) {
  lbry.call('daemon_status', {}, callback);
};

lbry.getStartNotice = function(callback) {
  lbry.call('get_start_notice', {}, callback);
}

lbry.checkFirstRun = function(callback) {
  lbry.call('is_first_run', {}, callback);
}

lbry.getNewAddress = function(callback) {
  lbry.call('get_new_address', {}, callback);
}

lbry.checkAddressIsMine = function(address, callback) {
  lbry.call('address_is_mine', {address: address}, callback);
}

lbry.getDaemonSettings = function(callback) {
  lbry.call('get_settings', {}, callback);
}

lbry.setDaemonSettings = function(settings, callback) {
  lbry.call('set_settings', settings, callback);
}

lbry.setDaemonSetting = function(setting, value, callback) {
  var setSettingsArgs = {};
  setSettingsArgs[setting] = value;
  lbry.call('set_settings', setSettingsArgs, callback)
}

lbry.getBalance = function(callback)
{
  lbry.call("get_balance", {}, callback);
}

lbry.sendToAddress = function(amount, address, callback, errorCallback)
{
  lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback);
}

lbry.resolveName = function(name, callback) {
  lbry.call('resolve_name', { 'name': name }, callback, () => {
    // For now, assume any error means the name was not resolved
    callback(null);
  });
}

lbry.getStream = function(name, callback) {
  lbry.call('get', { 'name': name }, callback);
};

lbry.getClaimInfo = function(name, callback) {
  lbry.call('get_claim_info', { name: name }, callback);
}

lbry.getMyClaim = function(name, callback) {
  lbry.call('get_my_claim', { name: name }, callback);
}

lbry.getCostEstimate = function(name, callback) {
  lbry.call('get_est_cost', { name: name }, callback);
}

lbry.getFileStatus = function(name, callback) {
  lbry.call('get_lbry_file', { 'name': name }, callback);
}

lbry.getFilesInfo = function(callback) {
  lbry.call('get_lbry_files', {}, callback);
}

lbry.startFile = function(name, callback) {
  lbry.call('start_lbry_file', { name: name }, callback);
}

lbry.stopFile = function(name, callback) {
  lbry.call('stop_lbry_file', { name: name }, callback);
}

lbry.deleteFile = function(name, deleteTargetFile=true, callback) {
  lbry.call('delete_lbry_file', {
    name: name,
    delete_target_file: deleteTargetFile,
  }, callback);
}

lbry.revealFile = function(path, callback) {
  lbry.call('reveal', { path: path }, callback);
}

lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0) {
  // Calls callback with file info when it appears in the list of files returned by lbry.getFilesInfo().
  // If timeoutCallback is provided, it will be called if the file fails to appear.
  lbry.getFilesInfo(function(filesInfo) {
    for (var fileInfo of filesInfo) {
      if (fileInfo.lbry_uri == name) {
        callback(fileInfo);
        return;
      }
    }

    if (tryNum <= 200) {
      setTimeout(function() { lbry.getFileInfoWhenListed(name, callback, timeoutCallback, tryNum + 1) }, 250);
    } else if (timeoutCallback) {
      timeoutCallback();
    }
  });
}

lbry.publish = function(params, fileListedCallback, publishedCallback, errorCallback) {
  // Publishes a file.
  // The optional fileListedCallback is called when the file becomes available in
  // lbry.getFilesInfo() during the publish process.

  // Use ES6 named arguments instead of directly passing param dict?
  lbry.call('publish', params, publishedCallback, (errorInfo) => {
    errorCallback({
      name: fault.fault,
      message: fault.faultString,
    });
  });
  if (fileListedCallback) {
    lbry.getFileInfoWhenListed(params.name, function(fileInfo) {
      fileListedCallback(fileInfo);
    });
  }
}

lbry.getVersionInfo = function(callback) {
  lbry.call('version', {}, callback);
};

lbry.checkNewVersionAvailable = function(callback) {
  lbry.call('version', {}, function(versionInfo) {
    var ver = versionInfo.lbrynet_version.split('.');

    var maj = parseInt(ver[0]),
        min = parseInt(ver[1]),
        patch = parseInt(ver[2]);

    var remoteVer = versionInfo.remote_lbrynet.split('.');
    var remoteMaj = parseInt(remoteVer[0]),
        remoteMin = parseInt(remoteVer[1]),
        remotePatch = parseInt(remoteVer[2]);

    if (maj < remoteMaj) {
      var newVersionAvailable = true;
    } else if (maj == remoteMaj) {
      if (min < remoteMin) {
        var newVersionAvailable = true;
      } else if (min == remoteMin) {
        var newVersionAvailable = (patch < remotePatch);
      } else {
        var newVersionAvailable = false;
      }
    } else {
      var newVersionAvailable = false;
    }
    callback(newVersionAvailable);
  }, function(err) {
    if (err.fault == 'NoSuchFunction') {
      // Really old daemon that can't report a version
      callback(true);
    }
  });
}

lbry.getClientSettings = function() {
  var outSettings = {};
  for (let setting of Object.keys(lbry.defaultClientSettings)) {
    var localStorageVal = localStorage.getItem('setting_' + setting);
    outSettings[setting] = (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal));
  }
  return outSettings;
}

lbry.getClientSetting = function(setting) {
  var localStorageVal = localStorage.getItem('setting_' + setting);
  return (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal));
}

lbry.setClientSettings = function(settings) {
  for (let setting of Object.keys(settings)) {
    lbry.setClientSetting(setting, settings[setting]);
  }
}

lbry.setClientSetting = function(setting, value) {
  return localStorage.setItem('setting_' + setting, JSON.stringify(value));
}

lbry.reportBug = function(message, callback) {
  lbry.call('upload_log', {
    name_prefix: 'report',
    exclude_previous: false,
    force: true,
    message: message
  }, callback);
}

//utilities
lbry.formatCredits = function(amount, precision)
{
  return amount.toFixed(precision || 1).replace(/\.?0+$/, '');
}

lbry.formatName = function(name) {
  // Converts LBRY name to standard format (all lower case, no special characters)
  return name.toLowerCase().replace(/[^a-z0-9\-]/g, '');
}

lbry.loadJs = function(src, type, onload)
{
  var lbryScriptTag = document.getElementById('lbry'),
      newScriptTag = document.createElement('script'),
      type = type || 'text/javascript';

  newScriptTag.src = src;
  newScriptTag.type = type;
  if (onload)
  {
    newScript.onload = onload;
  }
  lbryScriptTag.parentNode.insertBefore(newScriptTag, lbryScriptTag);
}

lbry.imagePath = function(file)
{
  return lbry.rootPath + '/img/' + file;
}

lbry.getMediaType = function(contentType, fileName) {
  if (contentType) {
    return /^[^/]+/.exec(contentType);
  } else if (fileName) {
    var dotIndex = fileName.lastIndexOf('.');
    if (dotIndex == -1) {
      return 'unknown';
    }

    var ext = fileName.substr(dotIndex + 1);
    if (/^mp4|mov|m4v|flv|f4v$/i.test(ext)) {
      return 'video';
    } else if (/^mp3|m4a|aac|wav|flac|ogg$/i.test(ext)) {
      return 'audio';
    } else if (/^html|htm|pdf|odf|doc|docx|md|markdown|txt$/i.test(ext)) {
      return 'document';
    } else {
      return 'unknown';
    }
  } else {
    return 'unknown';
  }
}

lbry.stop = function(callback) {
  lbry.call('stop', {}, callback);
};

<commit_msg>Update key names in API error reporting<commit_after>var lbry = {
  isConnected: false,
  rootPath: '.',
  daemonConnectionString: 'http://localhost:5279/lbryapi',
  colors: {
    primary: '#155B4A'
  },
  defaultClientSettings: {
    showNsfw: false,
  }
};

lbry.jsonrpc_call = function (connectionString, method, params, callback, errorCallback, connectFailedCallback, timeout) {
  var xhr = new XMLHttpRequest;
  if (typeof connectFailedCallback !== 'undefined') {
    if (timeout) {
      xhr.timeout = timeout;
    }

    xhr.addEventListener('error', function (e) {
      connectFailedCallback(e);
    });
    xhr.addEventListener('timeout', function() {
      connectFailedCallback(new Error('XMLHttpRequest connection timed out'));
    })
  }
  xhr.addEventListener('load', function() {
    var response = JSON.parse(xhr.responseText);

    if (response.error) {
      if (errorCallback) {
        errorCallback(response.error);
      } else {
        var errorEvent = new CustomEvent('unhandledError', {
          detail: {
            connectionString: connectionString,
            method: method,
            params: params,
            code: response.error.faultCode,
            message: response.error.fault,
            data: response.error.faultString
          }
        });
        document.dispatchEvent(errorEvent)
      }
    } else if (callback) {
      callback(response.result);
    }
  });

  if (connectFailedCallback) {
    xhr.addEventListener('error', function (event) {
      connectFailedCallback(event);
    });
  } else {
    xhr.addEventListener('error', function (event) {
      var errorEvent = new CustomEvent('unhandledError', {
        detail: {
          connectionString: connectionString,
          method: method,
          params: params,
          code: xhr.status,
          message: 'Connection to API server failed'
        }
      });
      document.dispatchEvent(errorEvent);
    });
  }

  xhr.open('POST', connectionString, true);
  xhr.send(JSON.stringify({
    'jsonrpc': '2.0',
    'method': method,
    'params': params,
    'id': 0
  }));
}

lbry.call = function (method, params, callback, errorCallback, connectFailedCallback) {
  lbry.jsonrpc_call(lbry.daemonConnectionString, method, [params], callback, errorCallback, connectFailedCallback);
}

//core
lbry.connect = function(callback)
{
  // Check every half second to see if the daemon's running.
  // Returns true to callback once connected, or false if it takes too long and we give up.
  function checkDaemonRunning(tryNum=0) {
    lbry.daemonRunningStatus(function (runningStatus) {
      if (runningStatus) {
        lbry.isConnected = true;
        callback(true);
      } else {
        if (tryNum <= 600) { // Move # of tries into constant or config option
          setTimeout(function () {
            checkDaemonRunning(tryNum + 1);
          }, 500);
        } else {
          callback(false);
        }
      }
    });
  }
  checkDaemonRunning();
}

lbry.daemonRunningStatus = function (callback) {
  // Returns true/false whether the daemon is running (i.e. fully conncected to the network),
  // or null if the AJAX connection to the daemon fails.

  lbry.call('is_running', {}, callback, null, function () {
    callback(null);
  });
};

lbry.getDaemonStatus = function (callback) {
  lbry.call('daemon_status', {}, callback);
};

lbry.getStartNotice = function(callback) {
  lbry.call('get_start_notice', {}, callback);
}

lbry.checkFirstRun = function(callback) {
  lbry.call('is_first_run', {}, callback);
}

lbry.getNewAddress = function(callback) {
  lbry.call('get_new_address', {}, callback);
}

lbry.checkAddressIsMine = function(address, callback) {
  lbry.call('address_is_mine', {address: address}, callback);
}

lbry.getDaemonSettings = function(callback) {
  lbry.call('get_settings', {}, callback);
}

lbry.setDaemonSettings = function(settings, callback) {
  lbry.call('set_settings', settings, callback);
}

lbry.setDaemonSetting = function(setting, value, callback) {
  var setSettingsArgs = {};
  setSettingsArgs[setting] = value;
  lbry.call('set_settings', setSettingsArgs, callback)
}

lbry.getBalance = function(callback)
{
  lbry.call("get_balance", {}, callback);
}

lbry.sendToAddress = function(amount, address, callback, errorCallback)
{
  lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback);
}

lbry.resolveName = function(name, callback) {
  lbry.call('resolve_name', { 'name': name }, callback, () => {
    // For now, assume any error means the name was not resolved
    callback(null);
  });
}

lbry.getStream = function(name, callback) {
  lbry.call('get', { 'name': name }, callback);
};

lbry.getClaimInfo = function(name, callback) {
  lbry.call('get_claim_info', { name: name }, callback);
}

lbry.getMyClaim = function(name, callback) {
  lbry.call('get_my_claim', { name: name }, callback);
}

lbry.getCostEstimate = function(name, callback) {
  lbry.call('get_est_cost', { name: name }, callback);
}

lbry.getFileStatus = function(name, callback) {
  lbry.call('get_lbry_file', { 'name': name }, callback);
}

lbry.getFilesInfo = function(callback) {
  lbry.call('get_lbry_files', {}, callback);
}

lbry.startFile = function(name, callback) {
  lbry.call('start_lbry_file', { name: name }, callback);
}

lbry.stopFile = function(name, callback) {
  lbry.call('stop_lbry_file', { name: name }, callback);
}

lbry.deleteFile = function(name, deleteTargetFile=true, callback) {
  lbry.call('delete_lbry_file', {
    name: name,
    delete_target_file: deleteTargetFile,
  }, callback);
}

lbry.revealFile = function(path, callback) {
  lbry.call('reveal', { path: path }, callback);
}

lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0) {
  // Calls callback with file info when it appears in the list of files returned by lbry.getFilesInfo().
  // If timeoutCallback is provided, it will be called if the file fails to appear.
  lbry.getFilesInfo(function(filesInfo) {
    for (var fileInfo of filesInfo) {
      if (fileInfo.lbry_uri == name) {
        callback(fileInfo);
        return;
      }
    }

    if (tryNum <= 200) {
      setTimeout(function() { lbry.getFileInfoWhenListed(name, callback, timeoutCallback, tryNum + 1) }, 250);
    } else if (timeoutCallback) {
      timeoutCallback();
    }
  });
}

lbry.publish = function(params, fileListedCallback, publishedCallback, errorCallback) {
  // Publishes a file.
  // The optional fileListedCallback is called when the file becomes available in
  // lbry.getFilesInfo() during the publish process.

  // Use ES6 named arguments instead of directly passing param dict?
  lbry.call('publish', params, publishedCallback, (errorInfo) => {
    errorCallback({
      name: fault.fault,
      message: fault.faultString,
    });
  });
  if (fileListedCallback) {
    lbry.getFileInfoWhenListed(params.name, function(fileInfo) {
      fileListedCallback(fileInfo);
    });
  }
}

lbry.getVersionInfo = function(callback) {
  lbry.call('version', {}, callback);
};

lbry.checkNewVersionAvailable = function(callback) {
  lbry.call('version', {}, function(versionInfo) {
    var ver = versionInfo.lbrynet_version.split('.');

    var maj = parseInt(ver[0]),
        min = parseInt(ver[1]),
        patch = parseInt(ver[2]);

    var remoteVer = versionInfo.remote_lbrynet.split('.');
    var remoteMaj = parseInt(remoteVer[0]),
        remoteMin = parseInt(remoteVer[1]),
        remotePatch = parseInt(remoteVer[2]);

    if (maj < remoteMaj) {
      var newVersionAvailable = true;
    } else if (maj == remoteMaj) {
      if (min < remoteMin) {
        var newVersionAvailable = true;
      } else if (min == remoteMin) {
        var newVersionAvailable = (patch < remotePatch);
      } else {
        var newVersionAvailable = false;
      }
    } else {
      var newVersionAvailable = false;
    }
    callback(newVersionAvailable);
  }, function(err) {
    if (err.fault == 'NoSuchFunction') {
      // Really old daemon that can't report a version
      callback(true);
    }
  });
}

lbry.getClientSettings = function() {
  var outSettings = {};
  for (let setting of Object.keys(lbry.defaultClientSettings)) {
    var localStorageVal = localStorage.getItem('setting_' + setting);
    outSettings[setting] = (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal));
  }
  return outSettings;
}

lbry.getClientSetting = function(setting) {
  var localStorageVal = localStorage.getItem('setting_' + setting);
  return (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal));
}

lbry.setClientSettings = function(settings) {
  for (let setting of Object.keys(settings)) {
    lbry.setClientSetting(setting, settings[setting]);
  }
}

lbry.setClientSetting = function(setting, value) {
  return localStorage.setItem('setting_' + setting, JSON.stringify(value));
}

lbry.reportBug = function(message, callback) {
  lbry.call('upload_log', {
    name_prefix: 'report',
    exclude_previous: false,
    force: true,
    message: message
  }, callback);
}

//utilities
lbry.formatCredits = function(amount, precision)
{
  return amount.toFixed(precision || 1).replace(/\.?0+$/, '');
}

lbry.formatName = function(name) {
  // Converts LBRY name to standard format (all lower case, no special characters)
  return name.toLowerCase().replace(/[^a-z0-9\-]/g, '');
}

lbry.loadJs = function(src, type, onload)
{
  var lbryScriptTag = document.getElementById('lbry'),
      newScriptTag = document.createElement('script'),
      type = type || 'text/javascript';

  newScriptTag.src = src;
  newScriptTag.type = type;
  if (onload)
  {
    newScript.onload = onload;
  }
  lbryScriptTag.parentNode.insertBefore(newScriptTag, lbryScriptTag);
}

lbry.imagePath = function(file)
{
  return lbry.rootPath + '/img/' + file;
}

lbry.getMediaType = function(contentType, fileName) {
  if (contentType) {
    return /^[^/]+/.exec(contentType);
  } else if (fileName) {
    var dotIndex = fileName.lastIndexOf('.');
    if (dotIndex == -1) {
      return 'unknown';
    }

    var ext = fileName.substr(dotIndex + 1);
    if (/^mp4|mov|m4v|flv|f4v$/i.test(ext)) {
      return 'video';
    } else if (/^mp3|m4a|aac|wav|flac|ogg$/i.test(ext)) {
      return 'audio';
    } else if (/^html|htm|pdf|odf|doc|docx|md|markdown|txt$/i.test(ext)) {
      return 'document';
    } else {
      return 'unknown';
    }
  } else {
    return 'unknown';
  }
}

lbry.stop = function(callback) {
  lbry.call('stop', {}, callback);
};

<|endoftext|>


<commit_before>/**
 * @file DraggableList component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import isArray from 'lodash/isArray';
import withScrolling, {createVerticalStrength} from 'react-dnd-scrollzone';
import classNames from 'classnames';

import DraggableListItem from '../_DraggableListItem';
import Tip from '../Tip';
import Theme from '../Theme';

import SelectMode from '../_statics/SelectMode';
import LIST_SEPARATOR from '../_statics/ListSeparator';

import Util from '../_vendors/Util';
import Event from '../_vendors/Event';
import Calculation from '../_vendors/Calculation';

const ScrollingComponent = withScrolling('div');

class DraggableList extends Component {

    static SelectMode = SelectMode;
    static LIST_SEPARATOR = LIST_SEPARATOR;
    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            data: props.data,
            value: Calculation.getInitValue(props)
        };

    }

    listItemMoveHandler = (dragIndex, hoverIndex, props) => {

        const {data} = this.state,
            dragItem = data.splice(dragIndex, 1);

        data.splice(hoverIndex, 0, ...dragItem);

        this.setState({
            data
        }, () => {
            const {onSequenceChange} = this.props;
            onSequenceChange && onSequenceChange(data);
        });

    };

    listItemSelectHandler = (item, index) => {

        const {selectMode} = this.props;

        let {value} = this.state;

        if (selectMode === SelectMode.MULTI_SELECT) {

            if (!value || !isArray(value)) {
                value = [];
            }

            value.push(item);

        } else if (selectMode === SelectMode.SINGLE_SELECT) {
            value = item;
        }

        this.setState({
            value
        }, () => {
            const {onItemSelect, onChange} = this.props;
            onItemSelect && onItemSelect(item, index);
            onChange && onChange(value, index);
        });

    };

    listItemDeselectHandler = (item, index) => {

        const {selectMode} = this.props;

        if (selectMode !== SelectMode.MULTI_SELECT) {
            return;
        }

        const {valueField, displayField} = this.props;
        let {value} = this.state;

        if (!value || !isArray(value)) {
            value = [];
        } else {
            value = value.filter(valueItem => {
                return Util.getValueByValueField(valueItem, valueField, displayField)
                    != Util.getValueByValueField(item, valueField, displayField);
            });
        }

        this.setState({
            value
        }, () => {
            const {onItemDeselect, onChange} = this.props;
            onItemDeselect && onItemDeselect(item, index);
            onChange && onChange(value, index);
        });

    };

    componentWillReceiveProps(nextProps) {

        let state;

        if (nextProps.data !== this.state.data) {
            state = state ? state : {};
            state.data = nextProps.data;
        }
        if (nextProps.value !== this.state.value) {
            state = state ? state : {};
            state.value = Calculation.getInitValue(nextProps);
        }

        if (state) {
            this.setState(state);
        }

    }

    render() {

        const {

                children, className, style, theme, itemHeight,

                selectTheme, selectMode, radioUncheckedIconCls, radioCheckedIconCls,
                checkboxUncheckedIconCls, checkboxCheckedIconCls, checkboxIndeterminateIconCls,

                idField, valueField, displayField, descriptionField, disabled, isLoading, renderer, onItemClick,

                scrollSpeed, scrollBuffer

            } = this.props,
            {data, value} = this.state,

            listClassName = classNames('draggable-list', {
                [className]: className
            });

        return (
            <ScrollingComponent className={listClassName}
                                disabled={disabled}
                                style={style}
                                strengthMultiplier={scrollSpeed}
                                verticalStrength={createVerticalStrength(scrollBuffer)}
                                onWheel={e => {
                                    Event.wheelHandler(e, this.props);
                                }}>

                {
                    data && data.map((item, index) => {

                        if (item === LIST_SEPARATOR) {
                            return

;
                        }

                        return typeof item === 'object' ?
                            (
                                <DraggableListItem key={index}
                                                   {...item}
                                                   index={index}
                                                   style={{height: itemHeight}}
                                                   theme={item.theme || theme}
                                                   selectTheme={item.selectTheme || selectTheme}
                                                   radioUncheckedIconCls={item.radioUncheckedIconCls || radioUncheckedIconCls}
                                                   radioCheckedIconCls={item.radioCheckedIconCls || radioCheckedIconCls}
                                                   checkboxUncheckedIconCls={item.checkboxUncheckedIconCls || checkboxUncheckedIconCls}
                                                   checkboxCheckedIconCls={item.checkboxCheckedIconCls || checkboxCheckedIconCls}
                                                   checkboxIndeterminateIconCls={item.checkboxIndeterminateIconCls || checkboxIndeterminateIconCls}
                                                   data={item}
                                                   checked={Calculation.isItemChecked(item, value, this.props)}
                                                   value={Util.getValueByValueField(item, valueField, displayField)}
                                                   text={Util.getTextByDisplayField(item, displayField, valueField)}
                                                   desc={item[descriptionField] || null}
                                                   disabled={disabled || item.disabled}
                                                   isLoading={isLoading || item.isLoading}
                                                   selectMode={selectMode}
                                                   renderer={renderer}
                                                   onMove={this.listItemMoveHandler}
                                                   onClick={e => {
                                                       onItemClick && onItemClick(item, index, e);
                                                       item.onClick && item.onClick(e);
                                                   }}
                                                   onSelect={() => {
                                                       this.listItemSelectHandler(item, index);
                                                   }}
                                                   onDeselect={() => {
                                                       this.listItemDeselectHandler(item, index);
                                                   }}/>
                            )
                            :
                            (
                                <DraggableListItem key={index}
                                                   index={index}
                                                   style={{height: itemHeight}}
                                                   theme={item.theme || theme}
                                                   selectTheme={item.selectTheme || selectTheme}
                                                   radioUncheckedIconCls={item.radioUncheckedIconCls || radioUncheckedIconCls}
                                                   radioCheckedIconCls={item.radioCheckedIconCls || radioCheckedIconCls}
                                                   checkboxUncheckedIconCls={item.checkboxUncheckedIconCls || checkboxUncheckedIconCls}
                                                   checkboxCheckedIconCls={item.checkboxCheckedIconCls || checkboxCheckedIconCls}
                                                   checkboxIndeterminateIconCls={item.checkboxIndeterminateIconCls || checkboxIndeterminateIconCls}
                                                   data={item}
                                                   checked={Calculation.isItemChecked(item, value, this.props)}
                                                   value={item}
                                                   text={item}
                                                   disabled={disabled}
                                                   isLoading={isLoading}
                                                   selectMode={selectMode}
                                                   renderer={renderer}
                                                   onMove={this.listItemMoveHandler}
                                                   onClick={e => {
                                                       onItemClick && onItemClick(item, index, e);
                                                   }}
                                                   onSelect={() => {
                                                       this.listItemSelectHandler(item, index);
                                                   }}
                                                   onDeselect={() => {
                                                       this.listItemDeselectHandler(item, index);
                                                   }}/>
                            );

                    })
                }

                {children}

            </ScrollingComponent>
        );
    }
}

DraggableList.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * The theme of the list item.
     */
    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * The theme of the list item select radio or checkbox.
     */
    selectTheme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * The mode of listItem.
     */
    selectMode: PropTypes.oneOf(Util.enumerateValue(SelectMode)),

    /**
     * Children passed into the ListItem.
     */
    data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({

        /**
         * The CSS class name of the list button.
         */
        className: PropTypes.string,

        /**
         * Override the styles of the list button.
         */
        style: PropTypes.object,

        /**
         * The theme of the list button.
         */
        theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

        /**
         * The text value of the list button.Type can be string or number.
         */
        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The list item's display text. Type can be string, number or bool.
         */
        text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The desc value of the list button. Type can be string or number.
         */
        desc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * If true,the list item will be disabled.
         */
        disabled: PropTypes.bool,

        /**
         * If true,the button will be have loading effect.
         */
        isLoading: PropTypes.bool,

        /**
         * If true,the element's ripple effect will be disabled.
         */
        disableTouchRipple: PropTypes.bool,

        /**
         * Use this property to display an icon. It will display on the left.
         */
        iconCls: PropTypes.string,

        /**
         * Use this property to display an icon. It will display on the right.
         */
        rightIconCls: PropTypes.string,

        /**
         * The message of tip.
         */
        tip: PropTypes.string,

        /**
         * The position of tip.
         */
        tipPosition: PropTypes.oneOf(Util.enumerateValue(Tip.Position)),

        /**
         * If true,the item will have center displayed ripple effect.
         */
        rippleDisplayCenter: PropTypes.bool,

        /**
         * You can create a complicated renderer callback instead of value and desc prop.
         */
        itemRenderer: PropTypes.func,

        /**
         * Callback function fired when a list item touch-tapped.
         */
        onClick: PropTypes.func

    }), PropTypes.string, PropTypes.number, PropTypes.symbol])),

    value: PropTypes.any,

    /**
     * The id field name in data. (default: "id")
     */
    idField: PropTypes.string,

    /**
     * The value field name in data. (default: "value")
     */
    valueField: PropTypes.string,

    /**
     * The display field name in data. (default: "text")
     */
    displayField: PropTypes.string,

    /**
     * The description field name in data. (default: "desc")
     */
    descriptionField: PropTypes.string,

    /**
     * If true, the list will be disabled.
     */
    disabled: PropTypes.bool,

    /**
     * If true, the list will be at loading status.
     */
    isLoading: PropTypes.bool,

    /**
     * The speed of scroll bar.
     */
    scrollSpeed: PropTypes.number,

    /**
     * The number of overflows.
     */
    scrollBuffer: PropTypes.number,

    shouldPreventContainerScroll: PropTypes.bool,

    radioUncheckedIconCls: PropTypes.string,
    radioCheckedIconCls: PropTypes.string,
    checkboxUncheckedIconCls: PropTypes.string,
    checkboxCheckedIconCls: PropTypes.string,
    checkboxIndeterminateIconCls: PropTypes.string,

    /**
     * You can create a complicated renderer callback instead of value and desc prop.
     */
    renderer: PropTypes.func,

    /**
     * Callback function fired when the list-item touch tap.
     */
    onItemClick: PropTypes.func,

    /**
     * Callback function fired when the list-item select.
     */
    onItemSelect: PropTypes.func,

    /**
     * Callback function fired when the list-item deselect.
     */
    onItemDeselect: PropTypes.func,

    /**
     * Callback function fired when the list changed.
     */
    onChange: PropTypes.func,

    /**
     * Callback function fired when select item sequence changed.
     */
    onSequenceChange: PropTypes.func,

    /**
     * Callback function fired when wrapper wheeled.
     */
    onWheel: PropTypes.func

};

DraggableList.defaultProps = {

    theme: Theme.DEFAULT,

    selectTheme: Theme.DEFAULT,
    selectMode: SelectMode.SINGLE_SELECT,

    idField: 'id',
    valueField: 'value',
    displayField: 'text',
    descriptionField: 'desc',
    disabled: false,

    scrollSpeed: 20,
    scrollBuffer: 40,
    shouldPreventContainerScroll: true,

    checkboxUncheckedIconCls: 'far fa-square',
    checkboxCheckedIconCls: 'fas fa-check-square',
    checkboxIndeterminateIconCls: 'fas fa-minus-square'

};

export default DraggableList;<commit_msg>update DraggableList<commit_after>/**
 * @file DraggableList component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import isArray from 'lodash/isArray';
import withScrolling, {createVerticalStrength} from 'react-dnd-scrollzone';
import classNames from 'classnames';

import DraggableListItem from '../_DraggableListItem';
import Tip from '../Tip';
import Theme from '../Theme';

import SelectMode from '../_statics/SelectMode';
import LIST_SEPARATOR from '../_statics/ListSeparator';

import Util from '../_vendors/Util';
import Event from '../_vendors/Event';
import Calculation from '../_vendors/Calculation';

const ScrollingComponent = withScrolling('div');

class DraggableList extends Component {

    static SelectMode = SelectMode;
    static LIST_SEPARATOR = LIST_SEPARATOR;
    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            data: props.data,
            value: Calculation.getInitValue(props)
        };

    }

    listItemMoveHandler = (dragIndex, hoverIndex, props) => {

        const {data} = this.state,
            dragItem = data.splice(dragIndex, 1);

        data.splice(hoverIndex, 0, ...dragItem);

        this.setState({
            data
        }, () => {
            const {onSequenceChange} = this.props;
            onSequenceChange && onSequenceChange(data);
        });

    };

    listItemSelectHandler = (item, index) => {

        const {selectMode} = this.props;

        let {value} = this.state;

        if (selectMode === SelectMode.MULTI_SELECT) {

            if (!value || !isArray(value)) {
                value = [];
            }

            value.push(item);

        } else if (selectMode === SelectMode.SINGLE_SELECT) {
            value = item;
        }

        this.setState({
            value
        }, () => {
            const {onItemSelect, onChange} = this.props;
            onItemSelect && onItemSelect(item, index);
            onChange && onChange(value, index);
        });

    };

    listItemDeselectHandler = (item, index) => {

        const {selectMode} = this.props;

        if (selectMode !== SelectMode.MULTI_SELECT) {
            return;
        }

        const {valueField, displayField} = this.props;
        let {value} = this.state;

        if (!value || !isArray(value)) {
            value = [];
        } else {
            value = value.filter(valueItem => {
                return Util.getValueByValueField(valueItem, valueField, displayField)
                    != Util.getValueByValueField(item, valueField, displayField);
            });
        }

        this.setState({
            value
        }, () => {
            const {onItemDeselect, onChange} = this.props;
            onItemDeselect && onItemDeselect(item, index);
            onChange && onChange(value, index);
        });

    };

    componentWillReceiveProps(nextProps) {

        let state;

        if (nextProps.data !== this.state.data) {
            state = state ? state : {};
            state.data = nextProps.data;
        }
        if (nextProps.value !== this.state.value) {
            state = state ? state : {};
            state.value = Calculation.getInitValue(nextProps);
        }

        if (state) {
            this.setState(state);
        }

    }

    render() {

        const {

                children, className, style, theme, itemHeight,

                selectTheme, selectMode, radioUncheckedIconCls, radioCheckedIconCls,
                checkboxUncheckedIconCls, checkboxCheckedIconCls, checkboxIndeterminateIconCls,

                idField, valueField, displayField, descriptionField, disabled, isLoading, renderer, onItemClick,

                scrollSpeed, scrollBuffer

            } = this.props,
            {data, value} = this.state,

            listClassName = classNames('draggable-list', {
                [className]: className
            });

        return (
            <ScrollingComponent className={listClassName}
                                disabled={disabled}
                                style={style}
                                strengthMultiplier={scrollSpeed}
                                verticalStrength={createVerticalStrength(scrollBuffer)}
                                onWheel={e => Event.wheelHandler(e, this.props)}>

                {
                    data && data.map((item, index) => item === LIST_SEPARATOR ?

                        :
                        typeof item === 'object' ?
                            <DraggableListItem key={index}
                                               {...item}
                                               index={index}
                                               style={{height: itemHeight}}
                                               theme={item.theme || theme}
                                               selectTheme={item.selectTheme || selectTheme}
                                               radioUncheckedIconCls={item.radioUncheckedIconCls || radioUncheckedIconCls}
                                               radioCheckedIconCls={item.radioCheckedIconCls || radioCheckedIconCls}
                                               checkboxUncheckedIconCls={item.checkboxUncheckedIconCls || checkboxUncheckedIconCls}
                                               checkboxCheckedIconCls={item.checkboxCheckedIconCls || checkboxCheckedIconCls}
                                               checkboxIndeterminateIconCls={item.checkboxIndeterminateIconCls || checkboxIndeterminateIconCls}
                                               data={item}
                                               checked={Calculation.isItemChecked(item, value, this.props)}
                                               value={Util.getValueByValueField(item, valueField, displayField)}
                                               text={Util.getTextByDisplayField(item, displayField, valueField)}
                                               desc={item[descriptionField] || null}
                                               disabled={disabled || item.disabled}
                                               isLoading={isLoading || item.isLoading}
                                               selectMode={selectMode}
                                               renderer={renderer}
                                               onMove={this.listItemMoveHandler}
                                               onClick={e => {
                                                   onItemClick && onItemClick(item, index, e);
                                                   item.onClick && item.onClick(e);
                                               }}
                                               onSelect={() => this.listItemSelectHandler(item, index)}
                                               onDeselect={() => this.listItemDeselectHandler(item, index)}/>
                            :
                            <DraggableListItem key={index}
                                               index={index}
                                               style={{height: itemHeight}}
                                               theme={item.theme || theme}
                                               selectTheme={item.selectTheme || selectTheme}
                                               radioUncheckedIconCls={item.radioUncheckedIconCls || radioUncheckedIconCls}
                                               radioCheckedIconCls={item.radioCheckedIconCls || radioCheckedIconCls}
                                               checkboxUncheckedIconCls={item.checkboxUncheckedIconCls || checkboxUncheckedIconCls}
                                               checkboxCheckedIconCls={item.checkboxCheckedIconCls || checkboxCheckedIconCls}
                                               checkboxIndeterminateIconCls={item.checkboxIndeterminateIconCls || checkboxIndeterminateIconCls}
                                               data={item}
                                               checked={Calculation.isItemChecked(item, value, this.props)}
                                               value={item}
                                               text={item}
                                               disabled={disabled}
                                               isLoading={isLoading}
                                               selectMode={selectMode}
                                               renderer={renderer}
                                               onMove={this.listItemMoveHandler}
                                               onClick={e => onItemClick && onItemClick(item, index, e)}
                                               onSelect={() => this.listItemSelectHandler(item, index)}
                                               onDeselect={() => this.listItemDeselectHandler(item, index)}/>
                    )
                }

                {children}

            </ScrollingComponent>
        );
    }
}

DraggableList.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * The theme of the list item.
     */
    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * The theme of the list item select radio or checkbox.
     */
    selectTheme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * The mode of listItem.
     */
    selectMode: PropTypes.oneOf(Util.enumerateValue(SelectMode)),

    /**
     * Children passed into the ListItem.
     */
    data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({

        /**
         * The CSS class name of the list button.
         */
        className: PropTypes.string,

        /**
         * Override the styles of the list button.
         */
        style: PropTypes.object,

        /**
         * The theme of the list button.
         */
        theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

        /**
         * The text value of the list button.Type can be string or number.
         */
        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The list item's display text. Type can be string, number or bool.
         */
        text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The desc value of the list button. Type can be string or number.
         */
        desc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * If true,the list item will be disabled.
         */
        disabled: PropTypes.bool,

        /**
         * If true,the button will be have loading effect.
         */
        isLoading: PropTypes.bool,

        /**
         * If true,the element's ripple effect will be disabled.
         */
        disableTouchRipple: PropTypes.bool,

        /**
         * Use this property to display an icon. It will display on the left.
         */
        iconCls: PropTypes.string,

        /**
         * Use this property to display an icon. It will display on the right.
         */
        rightIconCls: PropTypes.string,

        /**
         * The message of tip.
         */
        tip: PropTypes.string,

        /**
         * The position of tip.
         */
        tipPosition: PropTypes.oneOf(Util.enumerateValue(Tip.Position)),

        /**
         * If true,the item will have center displayed ripple effect.
         */
        rippleDisplayCenter: PropTypes.bool,

        /**
         * You can create a complicated renderer callback instead of value and desc prop.
         */
        itemRenderer: PropTypes.func,

        /**
         * Callback function fired when a list item touch-tapped.
         */
        onClick: PropTypes.func

    }), PropTypes.string, PropTypes.number, PropTypes.symbol])),

    value: PropTypes.any,

    /**
     * The id field name in data. (default: "id")
     */
    idField: PropTypes.string,

    /**
     * The value field name in data. (default: "value")
     */
    valueField: PropTypes.string,

    /**
     * The display field name in data. (default: "text")
     */
    displayField: PropTypes.string,

    /**
     * The description field name in data. (default: "desc")
     */
    descriptionField: PropTypes.string,

    /**
     * If true, the list will be disabled.
     */
    disabled: PropTypes.bool,

    /**
     * If true, the list will be at loading status.
     */
    isLoading: PropTypes.bool,

    /**
     * The speed of scroll bar.
     */
    scrollSpeed: PropTypes.number,

    /**
     * The number of overflows.
     */
    scrollBuffer: PropTypes.number,

    shouldPreventContainerScroll: PropTypes.bool,

    radioUncheckedIconCls: PropTypes.string,
    radioCheckedIconCls: PropTypes.string,
    checkboxUncheckedIconCls: PropTypes.string,
    checkboxCheckedIconCls: PropTypes.string,
    checkboxIndeterminateIconCls: PropTypes.string,

    /**
     * You can create a complicated renderer callback instead of value and desc prop.
     */
    renderer: PropTypes.func,

    /**
     * Callback function fired when the list-item touch tap.
     */
    onItemClick: PropTypes.func,

    /**
     * Callback function fired when the list-item select.
     */
    onItemSelect: PropTypes.func,

    /**
     * Callback function fired when the list-item deselect.
     */
    onItemDeselect: PropTypes.func,

    /**
     * Callback function fired when the list changed.
     */
    onChange: PropTypes.func,

    /**
     * Callback function fired when select item sequence changed.
     */
    onSequenceChange: PropTypes.func,

    /**
     * Callback function fired when wrapper wheeled.
     */
    onWheel: PropTypes.func

};

DraggableList.defaultProps = {

    theme: Theme.DEFAULT,

    selectTheme: Theme.DEFAULT,
    selectMode: SelectMode.SINGLE_SELECT,

    idField: 'id',
    valueField: 'value',
    displayField: 'text',
    descriptionField: 'desc',
    disabled: false,

    scrollSpeed: 20,
    scrollBuffer: 40,
    shouldPreventContainerScroll: true,

    checkboxUncheckedIconCls: 'far fa-square',
    checkboxCheckedIconCls: 'fas fa-check-square',
    checkboxIndeterminateIconCls: 'fas fa-minus-square'

};

export default DraggableList;<|endoftext|>


<commit_before>// this function Copyright (C) 2015 Ryan Morr
// http://ryanmorr.com/using-mutation-observers-to-watch-for-element-availability/
(function(win){
    'use strict';

    var listeners = [],
    doc = win.document,
    MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
    observer;

    function ready(selector, fn){
        // Store the selector and callback to be monitored
        listeners.push({
            selector: selector,
            fn: fn
        });
        if(!observer){
            // Watch for changes in the document
            observer = new MutationObserver(check);
            observer.observe(doc.documentElement, {
                childList: true,
                subtree: true
            });
        }
        // Check if the element is currently in the DOM
        check();
    }

    function check(){
        // Check the DOM for elements matching a stored selector
        for(var i = 0, len = listeners.length, listener, elements; i < len; i++){
            listener = listeners[i];
            // Query for elements matching the specified selector
            elements = doc.querySelectorAll(listener.selector);
            for(var j = 0, jLen = elements.length, element; j < jLen; j++){
                element = elements[j];
                // Make sure the callback isn't invoked with the
                // same element more than once
                if(!element.ready){
                    element.ready = true;
                    // Invoke the callback with the element
                    listener.fn.call(element, element);
                }
            }
        }
    }

    // Expose `ready`
    win.ready = ready;

})(this);

if (!String.prototype.format) {
  String.prototype.format = function() {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function(match, number) {
      return typeof args[number] != 'undefined'
        ? args[number]
        : match
      ;
    });
  };
}

function pad(num, size) {
    var s = num + "";
    while (s.length < size) s = "0" + s;
    return s;
}

// for date suffix checking
function include(arr, obj) {
    return (arr.indexOf(obj) != -1);
}

function modifyPlaylistPage() {
	// remove the total duration container before injecting it
	var total_duration = document.getElementById('total-duration-container');
	if (total_duration) { total_duration.parentNode.removeChild(total_duration); }

	// sum all timestamps to get the playlist's total number of seconds
	var total_seconds = 0;
	var timestamps = document.querySelectorAll(".timestamp > *");

	Array.prototype.forEach.call(timestamps, function(element, i) {
		var value = element.innerText;

		// finding 3 parts means H:M:SS
		// finding 2 parts means M:SS
		var parts = value.split(":");
		if (parts.length == 3) { total_seconds += parseInt(parts[0]*60*60) + parseInt(parts[1]*60) + parseInt(parts[2]); }
		else { total_seconds += parseInt(parts[0]*60) + parseInt(parts[1]); }
	});

	// ignore playlists without any videos
	if (total_seconds === 0) { return; }

	// add a new container with the playlist's total duration
	var years = Math.floor(total_seconds / 31536000);
	var days = Math.floor((total_seconds % 31536000) / 86400);
	var hours = Math.floor(((total_seconds % 31536000) % 86400) / 3600);
	var minutes = Math.floor((((total_seconds % 31536000) % 86400) % 3600) / 60);
	var seconds = (((total_seconds % 31536000) % 86400) % 3600) % 60;

	// version 1: the numerical version on the righthand side of the container
	var total_duration_short = "";
	if (years > 0) { total_duration_short += years + ":"; }
	if (days > 0) { total_duration_short += days + ":"; }
	if (hours > 0) { total_duration_short += pad(hours, 2) + ":"; }
	total_duration_short += pad(minutes, 2) + ":";
	total_duration_short += pad(seconds, 2);

	// version 2: the human readable version on the lefthand side of the container
	var play_all_link = document.getElementsByClassName("playlist-play-all")[0].getAttribute("href");
	var total_duration_long = "
";
	if (years > 0) { total_duration_long += years + " years, "; }
	if (days > 0) { total_duration_long += days + " days, "; }
	if (hours > 0) { total_duration_long += hours + " hours, "; }
	total_duration_long += minutes + " minutes and ";
	total_duration_long += seconds + " seconds.";
	total_duration_long += ' That\'s for a total of  videos ( deleted and/or private videos were excluded).
';
	total_duration_long += '
Start watching now and you will finish before you know it. Click here to begin.
';

	// inject container
	document.querySelector('div.branded-page-v2-primary-col > div.yt-card').insertAdjacentHTML('afterend', '\

 \

 \

 \

 \

 \
						Total playlist time \

 \
						' + total_duration_long + ' \
						\

 \
						' + total_duration_short + ' \

 \

 \

 \

 \

 \
	');

	// use the playlist's total number of seconds to calculate the point in time at which the user will have finished watching the entire playlist
	var finished_watching = new Date();
	finished_watching.setSeconds(finished_watching.getSeconds() + total_seconds);

	// 1st, 2nd, 3rd, 4th, etc.
	var suffix = "th";
	if (include([1, 21, 31], finished_watching.getDate())) { suffix = "st"; }
	else if (include([2, 22], finished_watching.getDate())) { suffix = "nd"; }
	else if (include([3, 23], finished_watching.getDate())) { suffix = "rd"; }

	// human readable notation for days and months
	// to be used for the projected time and date
	const all_days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
	const all_months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

	// inject projected time and date in the new container
	var finished_watching_formatted = "{0} {1} {2}{3}, {4} at {5}:{6}:{7}".format(
		all_days[finished_watching.getDay()],
		all_months[finished_watching.getMonth()],
		finished_watching.getDate(),
		suffix,
		finished_watching.getFullYear(),
		pad(finished_watching.getHours(), 2),
		pad(finished_watching.getMinutes(), 2),
		pad(finished_watching.getSeconds(), 2)
	);
	document.getElementById("finished_watching").innerText = "on " + finished_watching_formatted;

	// inject video counts in the new container
	var available_videos = document.getElementsByClassName("timestamp").length;
	var total_videos = document.getElementsByClassName("pl-video").length;
	var unavailable_videos = total_videos - available_videos;
	document.getElementById('available_videos').innerText = available_videos;
	document.getElementById('unavailable_videos').innerText = unavailable_videos;
}

ready('#pl-video-list', function(element) {
	// deal with the possibility of a node with id 'pl-video-list' on a page other than /playlist
	if (/^\/playlist.*/.test(window.location.pathname) === false) {	return;	}

	// execute on element load
	modifyPlaylistPage();

	// execute on element change
	var videos = document.getElementById('pl-video-list');
	var observer = new MutationObserver(function(mutations) {
		mutations.forEach(function(mutation) {
			modifyPlaylistPage();
		});
	});
	observer.observe(videos, { childList: true });
});

function modifyWatchPage() {
	// remove the already existing text container before injected a new one
	var control_playlist_duration = document.getElementById('control-playlist-duration');
	if (control_playlist_duration) { control_playlist_duration.parentNode.removeChild(control_playlist_duration); }

	// get the url to the playlist's page
	var playlist_url = document.querySelector("*[data-full-list-id]");
	if (playlist_url === null) { return; }

	// extract playlist id from the playlist's url
	var playlist_id = playlist_url.getAttribute("data-full-list-id");
	if (playlist_id === null) { return; }
	console.info(playlist_id);

	// construct the url for obtaining the total playlist's duration from the backend server
	var total_duration_url = "https://youtube-playlist-enhancer.appspot.com/GetPlaylistDuration/v1/?playlist_id=" + playlist_id;
	var request = new XMLHttpRequest();
	request.open('GET', total_duration_url, true);
	request.responseType = "json";
	request.onload = function() {
		// handle a good response
		if (this.status == 200) {
			// assign JSON response to a variable
			var j = this.response;
			console.log(j);

			// get the playlist's total duration
			var total_seconds = j.total_duration;

			// constructing all the different time parts from the playlist's total duration in seconds
			var hours = Math.floor(total_seconds / 3600);
			var minutes = Math.floor((total_seconds % 3600) / 60);
			var seconds = (total_seconds % 3600) % 60;
			console.log(hours + " hours");
			console.log(minutes + " minutes");
			console.log(seconds + " seconds");

			// HH:MM:SS format for in-player use
			var total_duration = "";
			if (hours > 0) { total_duration += pad(hours, 2) + ":"; }
			total_duration += pad(minutes, 2) + ":";
			total_duration += pad(seconds, 2);
			console.log("total duration = " + total_duration);

			// align the new text container to the center
			var controls = document.getElementsByClassName("ytp-chrome-controls");
			if (controls.length != 1) { return; }
			controls[0].style.textAlign = "center";

			// get the time currently into the video
			var current_time_elements = document.getElementsByClassName("ytp-time-current");
			if (current_time_elements.length != 1) { return; }
			var current_time = current_time_elements[0].innerText;
			console.log(current_time);

			// inject the new text container
			var left_controls = document.getElementsByClassName("ytp-left-controls");
			if (controls.length != 1) { return; }
			left_controls[0].insertAdjacentHTML('afterend', 'Playlist: 12:34 / 56:18 (90%)');
		}
		// handle a bad response
		else {
			console.error('Expected status code 200, got %s instead.', this.status);
		}
	}
	request.onerror = function() {
	    console.error('Unable to make an HTTP request.');
	}
	request.send();

}

ready('#watch-appbar-playlist', function(element) {
	if (/^\/watch.*/.test(window.location.pathname) === false) { return; }
	// modifyWatchPage();
});<commit_msg>the extension now successfully passes JSHint<commit_after>/*jshint esversion: 6 */
/*jshint multistr: true */

// this function Copyright (C) 2015 Ryan Morr
// http://ryanmorr.com/using-mutation-observers-to-watch-for-element-availability/
(function(win){
    'use strict';

    var listeners = [],
    doc = win.document,
    MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
    observer;

    function ready(selector, fn){
        // Store the selector and callback to be monitored
        listeners.push({
            selector: selector,
            fn: fn
        });
        if(!observer){
            // Watch for changes in the document
            observer = new MutationObserver(check);
            observer.observe(doc.documentElement, {
                childList: true,
                subtree: true
            });
        }
        // Check if the element is currently in the DOM
        check();
    }

    function check(){
        // Check the DOM for elements matching a stored selector
        for(var i = 0, len = listeners.length, listener, elements; i < len; i++){
            listener = listeners[i];
            // Query for elements matching the specified selector
            elements = doc.querySelectorAll(listener.selector);
            for(var j = 0, jLen = elements.length, element; j < jLen; j++){
                element = elements[j];
                // Make sure the callback isn't invoked with the
                // same element more than once
                if(!element.ready){
                    element.ready = true;
                    // Invoke the callback with the element
                    listener.fn.call(element, element);
                }
            }
        }
    }

    // Expose `ready`
    win.ready = ready;

})(this);

if (!String.prototype.format) {
  String.prototype.format = function() {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function(match, number) {
      return typeof args[number] != 'undefined' ? args[number] : match
      ;
    });
  };
}

function pad(num, size) {
    var s = num + "";
    while (s.length < size) s = "0" + s;
    return s;
}

// for date suffix checking
function include(arr, obj) {
    return (arr.indexOf(obj) != -1);
}

function modifyPlaylistPage() {
	// remove the total duration container before injecting it
	var total_duration = document.getElementById('total-duration-container');
	if (total_duration) { total_duration.parentNode.removeChild(total_duration); }

	// sum all timestamps to get the playlist's total number of seconds
	var total_seconds = 0;
	var timestamps = document.querySelectorAll(".timestamp > *");

	Array.prototype.forEach.call(timestamps, function(element, i) {
		var value = element.innerText;

		// finding 3 parts means H:M:SS
		// finding 2 parts means M:SS
		var parts = value.split(":");
		if (parts.length == 3) { total_seconds += parseInt(parts[0]*60*60) + parseInt(parts[1]*60) + parseInt(parts[2]); }
		else { total_seconds += parseInt(parts[0]*60) + parseInt(parts[1]); }
	});

	// ignore playlists without any videos
	if (total_seconds === 0) { return; }

	// add a new container with the playlist's total duration
	var years = Math.floor(total_seconds / 31536000);
	var days = Math.floor((total_seconds % 31536000) / 86400);
	var hours = Math.floor(((total_seconds % 31536000) % 86400) / 3600);
	var minutes = Math.floor((((total_seconds % 31536000) % 86400) % 3600) / 60);
	var seconds = (((total_seconds % 31536000) % 86400) % 3600) % 60;

	// version 1: the numerical version on the righthand side of the container
	var total_duration_short = "";
	if (years > 0) { total_duration_short += years + ":"; }
	if (days > 0) { total_duration_short += days + ":"; }
	if (hours > 0) { total_duration_short += pad(hours, 2) + ":"; }
	total_duration_short += pad(minutes, 2) + ":";
	total_duration_short += pad(seconds, 2);

	// version 2: the human readable version on the lefthand side of the container
	var play_all_link = document.getElementsByClassName("playlist-play-all")[0].getAttribute("href");
	var total_duration_long = "
";
	if (years > 0) { total_duration_long += years + " years, "; }
	if (days > 0) { total_duration_long += days + " days, "; }
	if (hours > 0) { total_duration_long += hours + " hours, "; }
	total_duration_long += minutes + " minutes and ";
	total_duration_long += seconds + " seconds.";
	total_duration_long += ' That\'s for a total of  videos ( deleted and/or private videos were excluded).
';
	total_duration_long += '
Start watching now and you will finish before you know it. Click here to begin.
';

	// inject container
	document.querySelector('div.branded-page-v2-primary-col > div.yt-card').insertAdjacentHTML('afterend', '\

 \

 \

 \

 \

 \
						Total playlist time \

 \
						' + total_duration_long + ' \
						\

 \
						' + total_duration_short + ' \

 \

 \

 \

 \

 \
	');

	// use the playlist's total number of seconds to calculate the point in time at which the user will have finished watching the entire playlist
	var finished_watching = new Date();
	finished_watching.setSeconds(finished_watching.getSeconds() + total_seconds);

	// 1st, 2nd, 3rd, 4th, etc.
	var suffix = "th";
	if (include([1, 21, 31], finished_watching.getDate())) { suffix = "st"; }
	else if (include([2, 22], finished_watching.getDate())) { suffix = "nd"; }
	else if (include([3, 23], finished_watching.getDate())) { suffix = "rd"; }

	// human readable notation for days and months
	// to be used for the projected time and date
	const all_days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
	const all_months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

	// inject projected time and date in the new container
	var finished_watching_formatted = "{0} {1} {2}{3}, {4} at {5}:{6}:{7}".format(
		all_days[finished_watching.getDay()],
		all_months[finished_watching.getMonth()],
		finished_watching.getDate(),
		suffix,
		finished_watching.getFullYear(),
		pad(finished_watching.getHours(), 2),
		pad(finished_watching.getMinutes(), 2),
		pad(finished_watching.getSeconds(), 2)
	);
	document.getElementById("finished_watching").innerText = "on " + finished_watching_formatted;

	// inject video counts in the new container
	var available_videos = document.getElementsByClassName("timestamp").length;
	var total_videos = document.getElementsByClassName("pl-video").length;
	var unavailable_videos = total_videos - available_videos;
	document.getElementById('available_videos').innerText = available_videos;
	document.getElementById('unavailable_videos').innerText = unavailable_videos;
}

ready('#pl-video-list', function(element) {
	// deal with the possibility of a node with id 'pl-video-list' on a page other than /playlist
	if (/^\/playlist.*/.test(window.location.pathname) === false) {	return;	}

	// execute on element load
	modifyPlaylistPage();

	// execute on element change
	var videos = document.getElementById('pl-video-list');
	var observer = new MutationObserver(function(mutations) {
		mutations.forEach(function(mutation) {
			modifyPlaylistPage();
		});
	});
	observer.observe(videos, { childList: true });
});

function modifyWatchPage() {
	// remove the already existing text container before injected a new one
	var control_playlist_duration = document.getElementById('control-playlist-duration');
	if (control_playlist_duration) { control_playlist_duration.parentNode.removeChild(control_playlist_duration); }

	// get the url to the playlist's page
	var playlist_url = document.querySelector("*[data-full-list-id]");
	if (playlist_url === null) { return; }

	// extract playlist id from the playlist's url
	var playlist_id = playlist_url.getAttribute("data-full-list-id");
	if (playlist_id === null) { return; }
	console.info(playlist_id);

	// construct the url for obtaining the total playlist's duration from the backend server
	var total_duration_url = "https://youtube-playlist-enhancer.appspot.com/GetPlaylistDuration/v1/?playlist_id=" + playlist_id;
	var request = new XMLHttpRequest();
	request.open('GET', total_duration_url, true);
	request.responseType = "json";
	request.onload = function() {
		// handle a good response
		if (this.status == 200) {
			// assign JSON response to a variable
			var j = this.response;
			console.log(j);

			// get the playlist's total duration
			var total_seconds = j.total_duration;

			// constructing all the different time parts from the playlist's total duration in seconds
			var hours = Math.floor(total_seconds / 3600);
			var minutes = Math.floor((total_seconds % 3600) / 60);
			var seconds = (total_seconds % 3600) % 60;
			console.log(hours + " hours");
			console.log(minutes + " minutes");
			console.log(seconds + " seconds");

			// HH:MM:SS format for in-player use
			var total_duration = "";
			if (hours > 0) { total_duration += pad(hours, 2) + ":"; }
			total_duration += pad(minutes, 2) + ":";
			total_duration += pad(seconds, 2);
			console.log("total duration = " + total_duration);

			// align the new text container to the center
			var controls = document.getElementsByClassName("ytp-chrome-controls");
			if (controls.length != 1) { return; }
			controls[0].style.textAlign = "center";

			// get the time currently into the video
			var current_time_elements = document.getElementsByClassName("ytp-time-current");
			if (current_time_elements.length != 1) { return; }
			var current_time = current_time_elements[0].innerText;
			console.log(current_time);

			// inject the new text container
			var left_controls = document.getElementsByClassName("ytp-left-controls");
			if (controls.length != 1) { return; }
			left_controls[0].insertAdjacentHTML('afterend', 'Playlist: 12:34 / 56:18 (90%)');
		}
		// handle a bad response
		else {
			console.error('Expected status code 200, got %s instead.', this.status);
		}
	};
	request.onerror = function() {
	    console.error('Unable to make an HTTP request.');
	};
	request.send();

}

ready('#watch-appbar-playlist', function(element) {
	if (/^\/watch.*/.test(window.location.pathname) === false) { return; }
	// modifyWatchPage();
});<|endoftext|>


<commit_before>var ShowFilterDefault = 'Показать фильтр';
var HideFilterDefault = 'Скрыть фильтр';

/////// LAYOUT POSTPROCESSING HELPERS

/**
   Filter formatting

   обрабатывает форму фильтра поиска при выводе списков в панели управления
   позволяет скрывать/показывать фильтр. при необходимости отображает форму поиска в конце списка.
   запоминает состояние отображения (скрыт/доуступен) конкретного фильтра в cookies

   TODO: продублировать описание на английском. показать пример использования.
*/
Limb.Class('CMS.Filter',
{
  __construct:function(showFilterStr, hideFilterStr)
  {
    this.showFilterStr = showFilterStr || ShowFilterDefault || '';
    this.hideFilterStr = hideFilterStr || HideFilterDefault || '';

    this.mainFilter = jQuery('.filter');
    this.mainFilterForm = this.mainFilter.find('form');
    if (!this.mainFilter[0])
      return;

    this.list = this.mainFilter.nextAll('.list').eq(0).css('margin-bottom','0');

    var togllerFilterHTML = '' + this.hideFilterStr + '';
    var bottomFilterHTML = '
' + this.showFilterStr + '
';

    this.mainFilter.prepend(togllerFilterHTML);
    this.list.after(bottomFilterHTML);

    this.cloneFilter = this.list.next('.filter');
    this.cloneFilterForm = this.cloneFilter.find('form');

    this.mainToggler = this.mainFilter.find('.toggler');
    this.cloneToggler = this.cloneFilter.find('.toggler');

    this._initBehavior();

    if(!cookieCMS.get(window.location + '.filter'))
      this.toggleFilter(this.mainToggler);
  },

  openFilter: function(toggler,obj)
  {
    var toggler = jQuery(toggler);
    var cloneToggler = null;
    var mainToggler = null;
    var filter = toggler.parent('.filter');

    if (filter.next().hasClass('list'))
    {
     cloneToggler = filter.next().next('.filter').find('.toggler');
     mainToggler = toggler;
    }
    if (filter.prev().hasClass('list'))
    {
     mainToggler = filter.prev().prev('.filter').find('.toggler');
     cloneToggler = toggler;
    }

    this.toggleFilter(toggler);

    this.setFilterCookie();

    var form = mainToggler.next('form')[0] ||  cloneToggler.next('form')[0];

    if (!form)
      return;

    form = jQuery(form);

    if (!toggler.next('form')[0])
    {
      if (!form.parent('.filter').hasClass('closedFilter'))
        this.toggleFilter(form.prev('.toggler'));
      form.appendTo(filter);
    }

  },

  toggleFilter: function(toggler)
  {
    var filter = toggler.parent('.filter');
    if (filter.hasClass('closedFilter'))
    {
      filter.removeClass('closedFilter');
      toggler.addClass('closingToggler').removeClass('openingToggler').children().text(this.hideFilterStr);
    }
    else
    {
      filter.addClass('closedFilter');
      toggler.addClass('openingToggler').removeClass('closingToggler').children().text(this.showFilterStr)
    }
  },

  setFilterCookie: function(){

    if(cookieCMS.get(window.location + '.filter'))
      cookieCMS.remove(window.location + '.filter');
    else
      cookieCMS.set(window.location + '.filter');
  },

  _initBehavior: function(){
    var obj = this;
    this.mainToggler.click(function(){obj.openFilter(this,obj)});
    this.cloneToggler.click(function(){obj.openFilter(this,obj)})
  }

});

jQuery.fn.accordion_cp = function(options) {
    // options
    var SLIDE_DOWN_SPEED = 'fast';
    var SLIDE_UP_SPEED = 'fast';
    var startClosed = options && options.start && options.start == 'closed';
    var on = options && options.on && (typeof options.on == 'number' && options.on > 0) ? options.on - 1 : 0;
    return this.each(function() {
        jQuery(this).addClass('accordion'); // use to activate styling
        jQuery(this).find('dd').hide();

        jQuery(this).find('dt').click(function() {
            var current = jQuery(this.parentNode).find('dd:visible');
            var next = jQuery(this).find('+dd');
            if(next[0] == current[0])
              return;

            if (current[0] != next[0]) {
                current.slideUp(SLIDE_UP_SPEED);
            }
            if (next.is(':visible')) {
                next.slideUp(SLIDE_UP_SPEED);
            } else {
                next.slideDown(SLIDE_DOWN_SPEED);
            }
        });
        if (!startClosed) {
            var elem = jQuery(this).find("dd > ul > li.current").parents('dd');
            if(!elem.get()[0])
            {
              jQuery(this).find('dd:first').slideDown(SLIDE_DOWN_SPEED);
              return;
            }
            elem.slideDown(SLIDE_DOWN_SPEED);
        }
    });
};

Limb.Class('CMS.SidebarToggler',
{
  __construct:function()
  {
     this.sidebar = jQuery('#sidebar');
     this.content = jQuery('#main_col');
     this.bool = false;

     var togglerHTML = '
Навигация«
';
     this.sidebar.prepend(togglerHTML);

     this.toggler = this.sidebar.find('.sidebar_toggler');

     if(cookieCMS.get('hiddenLimbSidebar'))
       this.toggleSidebar(this);

     this._initBehavior();
  },

  toggleSidebar:function(object){
    var wrapper = jQuery('#wrapper');
    if (jQuery('#wrapper').hasClass('hiddenSidebar'))
    {
      wrapper.removeClass('hiddenSidebar');
      this.toggler.find('.arrow').text('«');
      cookieCMS.remove('hiddenLimbSidebar');
      if(jQuery.browser.msie)
        this.toggler.height('auto');
    }
    else
    {
      wrapper.addClass('hiddenSidebar');
      this.toggler.find('.arrow').text('»');
      cookieCMS.set('hiddenLimbSidebar', 1);
      if(jQuery.browser.msie)
        this.toggler.height(wrapper.height()-jQuery('#admin_panel').height()-15);
    }
  },

  _initBehavior: function(){
    var object = this;
    jQuery('.sidebar_toggler').click(function(){object.toggleSidebar(object);return false;});
  }

});

function initMainMenu(){
  //left navigation current item highlight
  var url = window.location.toString();
  var max = 0;
  var link = null;
  jQuery("#main_menu > dd > ul > li > a").each(function()
  {
    //finding the longest href
    if(url.indexOf(this.href) >= 0 && this.href.length > max)
    {
      link = this;
      max = this.href.length;
    }
  });

  if(link)
  {
    jQuery(link).parent().attr('class', 'current');
    jQuery(link).parents('dd').prev().addClass('current');
  }

  //sliding navigation support
  if(Limb.isFunction(jQuery.fn.accordion_cp))
    jQuery('#main_menu').accordion_cp();

};

Limb.Class('CMS.lightLimbVersion',
{
  light_enabled_text: "лайт-версия. выключить.",
  light_enabled_title: "Выключить лайт-версию дизайна. Будут работать дополнительные интерфейсные элементы. Будет удобнее, но дольше.",
  light_disabled_text: "лайт-версия. включить.",
  light_disabled_title: "Включить лайт-версию дизайна. Пзволяет отключить интерфейсный javascript для ускорения загрузки",

  __construct:function()
  {
    if(cookieCMS.get('lightLimbVersion'))
      jQuery('#admin_panel #user_data').before("" + this.light_enabled_text + "");
    else
      jQuery('#admin_panel #user_data').before("" + this.light_disabled_text + "");

    this.link = jQuery('#light_version');

    this._initBehavior();
  },
  toggle: function()
  {
    if(cookieCMS.get('lightLimbVersion'))
    {
      this.link.text(this.light_disabled_text).removeClass('enabled').attr('title', this.light_enabled_title);
      cookieCMS.remove('lightLimbVersion');
    }
    else
    {
      this.link.text(this.light_disabled_text).addClass('enabled').attr('title', this.light_disabled_title);
      cookieCMS.set('lightLimbVersion');
    }
    window.location.reload();//плохо, потому что при POST запросе выпадет диалог с уточнением перезагрузки
  },
  _initBehavior: function()
  {
    var obj = this;
    this.link.click(function(){obj.toggle();return false;})
  }
});

Limb.Class('CMS.cookie',
{
  __construct:function(CMScookieName)
  {
    this.CMScookieName = CMScookieName || "LimbCMS";
    if (!Limb.cookie(this.CMScookieName))
      Limb.cookie(this.CMScookieName, ";");
  },
  get: function(cookieName)
  {
    var cookie = Limb.cookie(this.CMScookieName);
    cookieName = ";" + cookieName + ";";
    if(cookie.match(cookieName))
      return true;
    else
      return false;
  },
  set: function(cookieName)
  {
    var cookie = Limb.cookie(this.CMScookieName);
    if(!cookie.match(";" + cookieName + ";"))
      Limb.cookie(this.CMScookieName, cookie + cookieName+';');
  },
  remove: function(cookieName)
  {
    var cookie = Limb.cookie(this.CMScookieName);
    cookie = cookie.replace(cookieName + ";", "");
    Limb.cookie(this.CMScookieName, cookie);
  }
});

/*
  Добавляет свойства к контентой части и боковой панели, благодоря которым
  скроллер для прокрутки доболяется не у документа, а у боковой панели, либо у контента

  TODO: продублировать описание на английском. показать пример использования.
*/

function initDocumentStructure(){
  var main_col = jQuery('#main_col');
  var sidebar = jQuery('#sidebar');

  var height = jQuery('#wrapper').height()-jQuery('#admin_panel').height();
  window.status = jQuery('#wrapper').height();
  main_col.height(height).css('overflow','auto');
  sidebar.height(height);
};

function setFocusOnPopupWindow()
{
  var popup_window = document.getElementById('TB_iframeContent');
  if(!popup_window)
    return;

  var firstElement  = jQuery(popup_window.contentWindow.document).find('a, :input')[0];
  if(firstElement)
    firstElement.focus();

  popup_window.contentWindow.document.onkeyup = function(e)
  {
    if (e == null)
      var keycode = event.keyCode;
    else
      var keycode = e.which;
    if(keycode == 27)
      popup_window.contentWindow.parent.tb_remove();
  };
}

function toggle_selected(toggle_obj)
{
  var parent = toggle_obj.form || jQuery(toggle_obj).parents('table');
  var mark = toggle_obj.checked;
  jQuery("input:checkbox[@name='ids[]']", parent).each(function(){jQuery(this).attr("checked", mark);});
}

/*============================== WINDOW READY ==============================*/
jQuery(window).ready(function(){

  /*Keep focus into popup window*/
  window.onfocus = setFocusOnPopupWindow;
  document.onfocus = setFocusOnPopupWindow;
  window.onkeydown = setFocusOnPopupWindow;
  document.onkeydown = setFocusOnPopupWindow

  cookieCMS = new CMS.cookie('LimbCMS');
  new CMS.lightLimbVersion();

  if(cookieCMS.get('lightLimbVersion'))
  {
    jQuery.fx.off = true;
    return false;
  }

  var currheight;
  if(!jQuery.browser.msie)
  {
    initDocumentStructure();
    jQuery(window).resize(initDocumentStructure);
  }

  /*Nice Button*/
  jQuery('input[type=button], input[type=submit]').wrap('');

  /*SideBar Toggle*/
  new CMS.SidebarToggler();

  // Fiter up/down sliding control
  new CMS.Filter(ShowFilterDefault, HideFilterDefault);

  /*Main Menu*/
  initMainMenu();

});

<commit_msg>[svn r7763] -- fix bug into CMS.Filter<commit_after>var ShowFilterDefault = 'Показать фильтр';
var HideFilterDefault = 'Скрыть фильтр';

/////// LAYOUT POSTPROCESSING HELPERS

/**
   Filter formatting

   обрабатывает форму фильтра поиска при выводе списков в панели управления
   позволяет скрывать/показывать фильтр. при необходимости отображает форму поиска в конце списка.
   запоминает состояние отображения (скрыт/доуступен) конкретного фильтра в cookies

   TODO: продублировать описание на английском. показать пример использования.
*/
Limb.Class('CMS.Filter',
{
  __construct:function(showFilterStr, hideFilterStr)
  {
    this.showFilterStr = showFilterStr || ShowFilterDefault || '';
    this.hideFilterStr = hideFilterStr || HideFilterDefault || '';

    this.mainFilter = jQuery('.filter');
    this.mainFilterForm = this.mainFilter.find('form');
    if (!this.mainFilter[0])
      return;

    this.list = this.mainFilter.next('.list').css('margin-bottom','0');

    var togllerFilterHTML = '' + this.hideFilterStr + '';
    var bottomFilterHTML = '
' + this.showFilterStr + '
';

    this.mainFilter.prepend(togllerFilterHTML);
    this.list.after(bottomFilterHTML);

    this.cloneFilter = this.list.next('.filter');
    this.cloneFilterForm = this.cloneFilter.find('form');

    this.mainToggler = this.mainFilter.find('.toggler');
    this.cloneToggler = this.cloneFilter.find('.toggler');

    this._initBehavior();

    if(!cookieCMS.get(window.location + '.filter'))
      this.toggleFilter(this.mainToggler);
  },

  openFilter: function(toggler,obj)
  {
    var toggler = jQuery(toggler);
    var cloneToggler = null;
    var mainToggler = null;
    var filter = toggler.parent('.filter');

    if (filter.next().hasClass('list'))
    {
     cloneToggler = filter.next().next('.filter').find('.toggler');
     mainToggler = toggler;
    }
    if (filter.prev().hasClass('list'))
    {
     mainToggler = filter.prev().prev('.filter').find('.toggler');
     cloneToggler = toggler;
    }

    this.toggleFilter(toggler);

    this.setFilterCookie();

    var form = mainToggler.next('form')[0] ||  cloneToggler.next('form')[0];

    if (!form)
      return;

    form = jQuery(form);

    if (!toggler.next('form')[0])
    {
      if (!form.parent('.filter').hasClass('closedFilter'))
        this.toggleFilter(form.prev('.toggler'));
      form.appendTo(filter);
    }

  },

  toggleFilter: function(toggler)
  {
    var filter = toggler.parent('.filter');
    if (filter.hasClass('closedFilter'))
    {
      filter.removeClass('closedFilter');
      toggler.addClass('closingToggler').removeClass('openingToggler').children().text(this.hideFilterStr);
    }
    else
    {
      filter.addClass('closedFilter');
      toggler.addClass('openingToggler').removeClass('closingToggler').children().text(this.showFilterStr)
    }
  },

  setFilterCookie: function(){

    if(cookieCMS.get(window.location + '.filter'))
      cookieCMS.remove(window.location + '.filter');
    else
      cookieCMS.set(window.location + '.filter');
  },

  _initBehavior: function(){
    var obj = this;
    this.mainToggler.click(function(){obj.openFilter(this,obj)});
    this.cloneToggler.click(function(){obj.openFilter(this,obj)})
  }

});

jQuery.fn.accordion_cp = function(options) {
    // options
    var SLIDE_DOWN_SPEED = 'fast';
    var SLIDE_UP_SPEED = 'fast';
    var startClosed = options && options.start && options.start == 'closed';
    var on = options && options.on && (typeof options.on == 'number' && options.on > 0) ? options.on - 1 : 0;
    return this.each(function() {
        jQuery(this).addClass('accordion'); // use to activate styling
        jQuery(this).find('dd').hide();

        jQuery(this).find('dt').click(function() {
            var current = jQuery(this.parentNode).find('dd:visible');
            var next = jQuery(this).find('+dd');
            if(next[0] == current[0])
              return;

            if (current[0] != next[0]) {
                current.slideUp(SLIDE_UP_SPEED);
            }
            if (next.is(':visible')) {
                next.slideUp(SLIDE_UP_SPEED);
            } else {
                next.slideDown(SLIDE_DOWN_SPEED);
            }
        });
        if (!startClosed) {
            var elem = jQuery(this).find("dd > ul > li.current").parents('dd');
            if(!elem.get()[0])
            {
              jQuery(this).find('dd:first').slideDown(SLIDE_DOWN_SPEED);
              return;
            }
            elem.slideDown(SLIDE_DOWN_SPEED);
        }
    });
};

Limb.Class('CMS.SidebarToggler',
{
  __construct:function()
  {
     this.sidebar = jQuery('#sidebar');
     this.content = jQuery('#main_col');
     this.bool = false;

     var togglerHTML = '
Навигация«
';
     this.sidebar.prepend(togglerHTML);

     this.toggler = this.sidebar.find('.sidebar_toggler');

     if(cookieCMS.get('hiddenLimbSidebar'))
       this.toggleSidebar(this);

     this._initBehavior();
  },

  toggleSidebar:function(object){
    var wrapper = jQuery('#wrapper');
    if (jQuery('#wrapper').hasClass('hiddenSidebar'))
    {
      wrapper.removeClass('hiddenSidebar');
      this.toggler.find('.arrow').text('«');
      cookieCMS.remove('hiddenLimbSidebar');
      if(jQuery.browser.msie)
        this.toggler.height('auto');
    }
    else
    {
      wrapper.addClass('hiddenSidebar');
      this.toggler.find('.arrow').text('»');
      cookieCMS.set('hiddenLimbSidebar', 1);
      if(jQuery.browser.msie)
        this.toggler.height(wrapper.height()-jQuery('#admin_panel').height()-15);
    }
  },

  _initBehavior: function(){
    var object = this;
    jQuery('.sidebar_toggler').click(function(){object.toggleSidebar(object);return false;});
  }

});

function initMainMenu(){
  //left navigation current item highlight
  var url = window.location.toString();
  var max = 0;
  var link = null;
  jQuery("#main_menu > dd > ul > li > a").each(function()
  {
    //finding the longest href
    if(url.indexOf(this.href) >= 0 && this.href.length > max)
    {
      link = this;
      max = this.href.length;
    }
  });

  if(link)
  {
    jQuery(link).parent().attr('class', 'current');
    jQuery(link).parents('dd').prev().addClass('current');
  }

  //sliding navigation support
  if(Limb.isFunction(jQuery.fn.accordion_cp))
    jQuery('#main_menu').accordion_cp();

};

Limb.Class('CMS.lightLimbVersion',
{
  light_enabled_text: "лайт-версия. выключить.",
  light_enabled_title: "Выключить лайт-версию дизайна. Будут работать дополнительные интерфейсные элементы. Будет удобнее, но дольше.",
  light_disabled_text: "лайт-версия. включить.",
  light_disabled_title: "Включить лайт-версию дизайна. Пзволяет отключить интерфейсный javascript для ускорения загрузки",

  __construct:function()
  {
    if(cookieCMS.get('lightLimbVersion'))
      jQuery('#admin_panel #user_data').before("" + this.light_enabled_text + "");
    else
      jQuery('#admin_panel #user_data').before("" + this.light_disabled_text + "");

    this.link = jQuery('#light_version');

    this._initBehavior();
  },
  toggle: function()
  {
    if(cookieCMS.get('lightLimbVersion'))
    {
      this.link.text(this.light_disabled_text).removeClass('enabled').attr('title', this.light_enabled_title);
      cookieCMS.remove('lightLimbVersion');
    }
    else
    {
      this.link.text(this.light_disabled_text).addClass('enabled').attr('title', this.light_disabled_title);
      cookieCMS.set('lightLimbVersion');
    }
    window.location.reload();//плохо, потому что при POST запросе выпадет диалог с уточнением перезагрузки
  },
  _initBehavior: function()
  {
    var obj = this;
    this.link.click(function(){obj.toggle();return false;})
  }
});

Limb.Class('CMS.cookie',
{
  __construct:function(CMScookieName)
  {
    this.CMScookieName = CMScookieName || "LimbCMS";
    if (!Limb.cookie(this.CMScookieName))
      Limb.cookie(this.CMScookieName, ";");
  },
  get: function(cookieName)
  {
    var cookie = Limb.cookie(this.CMScookieName);
    cookieName = ";" + cookieName + ";";
    if(cookie.match(cookieName))
      return true;
    else
      return false;
  },
  set: function(cookieName)
  {
    var cookie = Limb.cookie(this.CMScookieName);
    if(!cookie.match(";" + cookieName + ";"))
      Limb.cookie(this.CMScookieName, cookie + cookieName+';');
  },
  remove: function(cookieName)
  {
    var cookie = Limb.cookie(this.CMScookieName);
    cookie = cookie.replace(cookieName + ";", "");
    Limb.cookie(this.CMScookieName, cookie);
  }
});

/*
  Добавляет свойства к контентой части и боковой панели, благодоря которым
  скроллер для прокрутки доболяется не у документа, а у боковой панели, либо у контента

  TODO: продублировать описание на английском. показать пример использования.
*/

function initDocumentStructure(){
  var main_col = jQuery('#main_col');
  var sidebar = jQuery('#sidebar');

  var height = jQuery('#wrapper').height()-jQuery('#admin_panel').height();
  window.status = jQuery('#wrapper').height();
  main_col.height(height).css('overflow','auto');
  sidebar.height(height);
};

function setFocusOnPopupWindow()
{
  var popup_window = document.getElementById('TB_iframeContent');
  if(!popup_window)
    return;

  var firstElement  = jQuery(popup_window.contentWindow.document).find('a, :input')[0];
  if(firstElement)
    firstElement.focus();

  popup_window.contentWindow.document.onkeyup = function(e)
  {
    if (e == null)
      var keycode = event.keyCode;
    else
      var keycode = e.which;
    if(keycode == 27)
      popup_window.contentWindow.parent.tb_remove();
  };
}

function toggle_selected(toggle_obj)
{
  var parent = toggle_obj.form || jQuery(toggle_obj).parents('table');
  var mark = toggle_obj.checked;
  jQuery("input:checkbox[@name='ids[]']", parent).each(function(){jQuery(this).attr("checked", mark);});
}

/*============================== WINDOW READY ==============================*/
jQuery(window).ready(function(){

  /*Keep focus into popup window*/
  window.onfocus = setFocusOnPopupWindow;
  document.onfocus = setFocusOnPopupWindow;
  window.onkeydown = setFocusOnPopupWindow;
  document.onkeydown = setFocusOnPopupWindow

  cookieCMS = new CMS.cookie('LimbCMS');
  new CMS.lightLimbVersion();

  if(cookieCMS.get('lightLimbVersion'))
  {
    jQuery.fx.off = true;
    return false;
  }

  var currheight;
  if(!jQuery.browser.msie)
  {
    initDocumentStructure();
    jQuery(window).resize(initDocumentStructure);
  }

  /*Nice Button*/
  jQuery('input[type=button], input[type=submit]').wrap('');

  /*SideBar Toggle*/
  new CMS.SidebarToggler();

  // Fiter up/down sliding control
  new CMS.Filter(ShowFilterDefault, HideFilterDefault);

  /*Main Menu*/
  initMainMenu();

});

<|endoftext|>