<commit_before>"use strict";

var errors = require('./errors')
  , adb = require('../uiautomator/adb')
  , _ = require('underscore')
  , logger = require('../logger').get('appium')
  , deviceCommon = require('./device')
  , status = require("./uiauto/lib/status")
  //, NotImplementedError = errors.NotImplementedError
  , NotYetImplementedError = errors.NotYetImplementedError
  , parseXpath = require('./uiauto/appium/xpath').parseXpath
  , exec = require('child_process').exec
  , UnknownError = errors.UnknownError;

var Android = function(opts) {
  this.rest = opts.rest;
  this.opts = opts;
  this.apkPath = opts.apkPath;
  this.appPackage = opts.appPackage;
  this.appActivity = opts.appActivity;
  this.verbose = opts.verbose;
  this.queue = [];
  this.progress = 0;
  this.onStop = function() {};
  this.implicitWaitMs = 0;
  this.commandTimeoutMs = 60 * 1000;
  this.origCommandTimeoutMs = this.commandTimeoutMs;
  this.commandTimeout = null;
  this.shuttingDown = false;
  this.adb = null;
  this.capabilities = {
    platform: 'LINUX'
    , browserName: 'Android'
    , version: '4.1'
    , webStorageEnabled: false
    , takesScreenshots: true
    , javascriptEnabled: true
    , databaseEnabled: false
  };
};

// Clear data, close app, then start app.
Android.prototype.fastReset = function(cb) {
  // list instruments with: adb shell pm list instrumentation
  // targetPackage + '.clean' / clean.apk.Clear
  var me = this;
  var clearCmd = me.adb.adbCmd + ' shell am instrument ' + me.appPackage + '.clean/clean.apk.Clean';
  logger.debug("Clear command: " + clearCmd);
  exec(clearCmd, {}, function(err, stdout, stderr) {
    if (err) {
      logger.warn(stderr);
      cb(err);
    } else {
      me.adb.startApp(function(err) {
        if (err) {
          cb(err);
        } else {
          cb(null);
        }
      });
    }
  });
};

Android.prototype.start = function(cb, onDie) {
  if (typeof onDie === "function") {
    this.onStop = onDie;
  }
  var didLaunch = false;

  var onLaunch = _.bind(function(err) {
    if (err) {
      logger.error("ADB failed to launch!");
      this.adb = null;
      this.onStop = null;
      cb(err);
    } else {
      logger.info("ADB launched! Ready for commands (will time out in " +
                  (this.commandTimeoutMs / 1000) + "secs)");
      this.resetTimeout();
      didLaunch = true;
      cb(null);
    }
  }, this);

  var onExit = _.bind(function(code) {
    if (!didLaunch) {
      logger.error("ADB quit before it successfully launched");
      cb("ADB quit unexpectedly before successfully launching");
      code = code || 1;
    } else if (typeof this.cbForCurrentCmd === "function") {
      var error = new UnknownError("ADB died while responding to command, " +
                                   "please check appium logs!");
      this.cbForCurrentCmd(error, null);
      code = code || 1;
    }
    this.adb.uninstallApp(_.bind(function() {
      this.adb = null;
      this.shuttingDown = false;
      this.onStop(code);
      this.onStop = null;
    }, this));
  }, this);

  if (this.adb === null) {
    this.adb = adb(this.opts);
    this.adb.start(onLaunch, onExit);
  } else {
    logger.error("Tried to start ADB when we already have one running!");
  }
};

Android.prototype.timeoutWaitingForCommand = function() {
  logger.info("Didn't get a new command in " + (this.commandTimeoutMs / 1000) +
              " secs, shutting down...");
  //this.adb.sendShutdownCommand(function() {
    //logger.info("Sent shutdown command, waiting for ADB to stop...");
  //});
  this.stop();
};

Android.prototype.stop = function(cb) {
  if (this.commandTimeout) {
    clearTimeout(this.commandTimeout);
  }
  if (this.adb === null) {
    logger.info("Trying to stop adb but it already exited");
    if (cb) {
      cb();
    }
  } else {
    if (cb) {
      this.onStop = cb;
    }
    this.shuttingDown = true;
    this.adb.goToHome(_.bind(function() {
      this.adb.sendShutdownCommand(_.bind(function() {
        logger.info("Sent shutdown command, waiting for ADB to stop...");
      }, this));
    }, this));
    this.queue = [];
    this.progress = 0;
  }
};

Android.prototype.resetTimeout = function() {
  if (this.commandTimeout) {
    clearTimeout(this.commandTimeout);
  }
  this.commandTimeout = setTimeout(_.bind(this.timeoutWaitingForCommand, this),
      this.commandTimeoutMs);
};

Android.prototype.proxy = deviceCommon.proxy;
Android.prototype.respond = deviceCommon.respond;

Android.prototype.push = function(elem) {

  this.resetTimeout();
  this.queue.push(elem);

  var next = _.bind(function() {
    if (this.queue.length <= 0 || this.progress > 0) {
      return;
    }

    var target = this.queue.shift()
      , action = target[0][0]
      , params = typeof target[0][1] === "undefined" ? {} : target[0][1]
      , cb = target[1];

    this.cbForCurrentCmd = cb;

    this.progress++;

    if (this.adb && !this.shuttingDown) {
      this.adb.sendAutomatorCommand(action, params, _.bind(function(response) {
        this.cbForCurrentCmd = null;
        if (typeof cb === 'function') {
          this.respond(response, cb);
        }

        // maybe there's moar work to do
        this.progress--;
        next();
      }, this));
    } else {
      this.cbForCurrentCmd = null;
      var msg = "Tried to send command to non-existent Android device, " +
                 "maybe it shut down?";
      if (this.shuttingDown) {
        msg = "We're in the middle of shutting down the Android device, " +
              "so your request won't be executed. Sorry!";
      }

      this.respond({
        status: status.codes.UnknownError.code
        , value: msg
      }, cb);
      this.progress--;
      next();
    }
  }, this);

  next();
};

Android.prototype.waitForCondition = deviceCommon.waitForCondition;

Android.prototype.setCommandTimeout = function(secs, cb) {
  logger.info("Setting command timeout for android to " + secs + " secs");
  this.origCommandTimeoutMs = this.commandTimeoutMs;
  this.commandTimeoutMs = secs * 1000;
  this.resetTimeout();
  cb(null, {
    status: status.codes.Success.code
    , value: ''
  });
};

Android.prototype.resetCommandTimeout = function(cb) {
  this.commandTimeoutMs = this.origCommandTimeoutMs;
  this.resetTimeout();
  cb(null, {
    status: status.codes.Success.code
    , value: ''
  });
};

Android.prototype.getCommandTimeout = function(cb) {
  this.resetTimeout();
  cb(null, {
    status: status.codes.Success.code
    , value: this.commandTimeoutMs / 1000
  });
};

Android.prototype.findElement = function(strategy, selector, cb) {
  this.findElementOrElements(strategy, selector, false, "", cb);
};

Android.prototype.findElements = function(strategy, selector, cb) {
  this.findElementOrElements(strategy, selector, true, "", cb);
};

Android.prototype.findElementOrElements = function(strategy, selector, many, context, cb) {
  var params = {
    strategy: strategy
    , selector: selector
    , context: context
    , multiple: many
  };
  var xpathError = false;
  if (strategy === "xpath") {
    var xpathParams = parseXpath(selector);
    if (!xpathParams) {
      xpathError = true;
    } else {
      // massage for the javas
      if (xpathParams.attr === null) {
        xpathParams.attr = "";
      }
      if (xpathParams.constraint === null) {
        xpathParams.constraint = "";
      }
      params = _.extend(params, xpathParams);
    }
  }
  var doFind = _.bind(function(findCb) {
    this.proxy(["find", params], function(err, res) {
      if (err) {
        findCb(false, err, res);
      } else {
        if (!many && res.status === 0) {
          findCb(true, err, res);
        } else if (many && res.value.length > 0) {
          findCb(true, err, res);
        } else {
          findCb(false, err, res);
        }
      }
    });
  }, this);
  if (!xpathError) {
    this.waitForCondition(this.implicitWaitMs, doFind, cb);
  } else {
    cb(null, {
      status: status.codes.XPathLookupError.code
      , value: "Could not parse xpath data from " + selector
    });
  }
};

Android.prototype.findElementFromElement = function(element, strategy, selector, cb) {
  this.findElementOrElements(strategy, selector, false, element, cb);
};

Android.prototype.findElementsFromElement = function(element, strategy, selector, cb) {
  this.findElementOrElements(strategy, selector, true, element, cb);
};

Android.prototype.setValueImmediate = function(elementId, value, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.setValue = function(elementId, value, cb) {
  this.proxy(["element:setText", {elementId: elementId, text: value}], cb);
};

Android.prototype.click = function(elementId, cb) {
  this.proxy(["element:click", {elementId: elementId}], cb);
};

Android.prototype.complexTap = function(tapCount, touchCount, duration, x, y, elementId, cb) {
  this.proxy(["click", {x: x, y: y}], cb);
};

Android.prototype.clear = function(elementId, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getText = function(elementId, cb) {
  this.proxy(["element:getText", {elementId: elementId}], cb);
};

Android.prototype.getAttribute = function(elementId, attributeName, cb) {
  var p = {elementId: elementId, attribute: attributeName};
  this.proxy(["element:getAttribute", p], cb);
};

Android.prototype.getLocation = function(elementId, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getSize = function(elementId, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getWindowSize = function(windowHandle, cb) {
  this.proxy(["getDeviceSize"], cb);
};

Android.prototype.getPageIndex = function(elementId, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.keys = function(elementId, keys, cb) {
  this.proxy(["element:setText", {elementId: elementId, text: keys}], cb);
};

Android.prototype.frame = function(frame, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.implicitWait = function(ms, cb) {
  this.implicitWaitMs = parseInt(ms, 10);
  logger.info("Set Android implicit wait to " + ms + "ms");
  cb(null, {
    status: status.codes.Success.code
    , value: null
  });
};

Android.prototype.elementDisplayed = function(elementId, cb) {
  var p = {elementId: elementId, attribute: "displayed"};
  this.proxy(["element:getAttribute", p], cb);
};

Android.prototype.elementEnabled = function(elementId, cb) {
  var p = {elementId: elementId, attribute: "enabled"};
  this.proxy(["element:getAttribute", p], cb);
};

Android.prototype.getPageSource = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getAlertText = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.postAcceptAlert = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.postDismissAlert = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getOrientation = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.setOrientation = function(orientation, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getScreenshot = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.fakeFlick = function(xSpeed, ySpeed, swipe, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.fakeFlickElement = function(elementId, xoffset, yoffset, speed, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.swipe = function(startX, startY, endX, endY, duration, touchCount, elId, cb) {
  if (startX === 'null') {
    startX = 0.5;
  }
  if (startY === 'null') {
    startY = 0.5;
  }
  var swipeOpts = {
    startX: startX
    , startY: startY
    , endX: endX
    , endY: endY
    , steps: (duration * 1000) / 5
  };
  if (elId !== null) {
    swipeOpts.elementId = elId;
    this.proxy(["element:swipe", swipeOpts], cb);
  } else {
    this.proxy(["swipe", swipeOpts], cb);
  }
};

Android.prototype.flick = function(startX, startY, endX, endY, touchCount, elId, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.hideKeyboard = function(keyName, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.url = function(url, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.active = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getWindowHandle = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getWindowHandles = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.setWindow = function(name, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.clearWebView = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.execute = function(script, args, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.title = function(cb) {
    cb(new NotYetImplementedError(), null);
};

module.exports = function(opts) {
  return new Android(opts);
};
<commit_msg>Steps must be int or Java will crash<commit_after>"use strict";

var errors = require('./errors')
  , adb = require('../uiautomator/adb')
  , _ = require('underscore')
  , logger = require('../logger').get('appium')
  , deviceCommon = require('./device')
  , status = require("./uiauto/lib/status")
  //, NotImplementedError = errors.NotImplementedError
  , NotYetImplementedError = errors.NotYetImplementedError
  , parseXpath = require('./uiauto/appium/xpath').parseXpath
  , exec = require('child_process').exec
  , UnknownError = errors.UnknownError;

var Android = function(opts) {
  this.rest = opts.rest;
  this.opts = opts;
  this.apkPath = opts.apkPath;
  this.appPackage = opts.appPackage;
  this.appActivity = opts.appActivity;
  this.verbose = opts.verbose;
  this.queue = [];
  this.progress = 0;
  this.onStop = function() {};
  this.implicitWaitMs = 0;
  this.commandTimeoutMs = 60 * 1000;
  this.origCommandTimeoutMs = this.commandTimeoutMs;
  this.commandTimeout = null;
  this.shuttingDown = false;
  this.adb = null;
  this.capabilities = {
    platform: 'LINUX'
    , browserName: 'Android'
    , version: '4.1'
    , webStorageEnabled: false
    , takesScreenshots: true
    , javascriptEnabled: true
    , databaseEnabled: false
  };
};

// Clear data, close app, then start app.
Android.prototype.fastReset = function(cb) {
  // list instruments with: adb shell pm list instrumentation
  // targetPackage + '.clean' / clean.apk.Clear
  var me = this;
  var clearCmd = me.adb.adbCmd + ' shell am instrument ' + me.appPackage + '.clean/clean.apk.Clean';
  logger.debug("Clear command: " + clearCmd);
  exec(clearCmd, {}, function(err, stdout, stderr) {
    if (err) {
      logger.warn(stderr);
      cb(err);
    } else {
      me.adb.startApp(function(err) {
        if (err) {
          cb(err);
        } else {
          cb(null);
        }
      });
    }
  });
};

Android.prototype.start = function(cb, onDie) {
  if (typeof onDie === "function") {
    this.onStop = onDie;
  }
  var didLaunch = false;

  var onLaunch = _.bind(function(err) {
    if (err) {
      logger.error("ADB failed to launch!");
      this.adb = null;
      this.onStop = null;
      cb(err);
    } else {
      logger.info("ADB launched! Ready for commands (will time out in " +
                  (this.commandTimeoutMs / 1000) + "secs)");
      this.resetTimeout();
      didLaunch = true;
      cb(null);
    }
  }, this);

  var onExit = _.bind(function(code) {
    if (!didLaunch) {
      logger.error("ADB quit before it successfully launched");
      cb("ADB quit unexpectedly before successfully launching");
      code = code || 1;
    } else if (typeof this.cbForCurrentCmd === "function") {
      var error = new UnknownError("ADB died while responding to command, " +
                                   "please check appium logs!");
      this.cbForCurrentCmd(error, null);
      code = code || 1;
    }
    this.adb.uninstallApp(_.bind(function() {
      this.adb = null;
      this.shuttingDown = false;
      this.onStop(code);
      this.onStop = null;
    }, this));
  }, this);

  if (this.adb === null) {
    this.adb = adb(this.opts);
    this.adb.start(onLaunch, onExit);
  } else {
    logger.error("Tried to start ADB when we already have one running!");
  }
};

Android.prototype.timeoutWaitingForCommand = function() {
  logger.info("Didn't get a new command in " + (this.commandTimeoutMs / 1000) +
              " secs, shutting down...");
  //this.adb.sendShutdownCommand(function() {
    //logger.info("Sent shutdown command, waiting for ADB to stop...");
  //});
  this.stop();
};

Android.prototype.stop = function(cb) {
  if (this.commandTimeout) {
    clearTimeout(this.commandTimeout);
  }
  if (this.adb === null) {
    logger.info("Trying to stop adb but it already exited");
    if (cb) {
      cb();
    }
  } else {
    if (cb) {
      this.onStop = cb;
    }
    this.shuttingDown = true;
    this.adb.goToHome(_.bind(function() {
      this.adb.sendShutdownCommand(_.bind(function() {
        logger.info("Sent shutdown command, waiting for ADB to stop...");
      }, this));
    }, this));
    this.queue = [];
    this.progress = 0;
  }
};

Android.prototype.resetTimeout = function() {
  if (this.commandTimeout) {
    clearTimeout(this.commandTimeout);
  }
  this.commandTimeout = setTimeout(_.bind(this.timeoutWaitingForCommand, this),
      this.commandTimeoutMs);
};

Android.prototype.proxy = deviceCommon.proxy;
Android.prototype.respond = deviceCommon.respond;

Android.prototype.push = function(elem) {

  this.resetTimeout();
  this.queue.push(elem);

  var next = _.bind(function() {
    if (this.queue.length <= 0 || this.progress > 0) {
      return;
    }

    var target = this.queue.shift()
      , action = target[0][0]
      , params = typeof target[0][1] === "undefined" ? {} : target[0][1]
      , cb = target[1];

    this.cbForCurrentCmd = cb;

    this.progress++;

    if (this.adb && !this.shuttingDown) {
      this.adb.sendAutomatorCommand(action, params, _.bind(function(response) {
        this.cbForCurrentCmd = null;
        if (typeof cb === 'function') {
          this.respond(response, cb);
        }

        // maybe there's moar work to do
        this.progress--;
        next();
      }, this));
    } else {
      this.cbForCurrentCmd = null;
      var msg = "Tried to send command to non-existent Android device, " +
                 "maybe it shut down?";
      if (this.shuttingDown) {
        msg = "We're in the middle of shutting down the Android device, " +
              "so your request won't be executed. Sorry!";
      }

      this.respond({
        status: status.codes.UnknownError.code
        , value: msg
      }, cb);
      this.progress--;
      next();
    }
  }, this);

  next();
};

Android.prototype.waitForCondition = deviceCommon.waitForCondition;

Android.prototype.setCommandTimeout = function(secs, cb) {
  logger.info("Setting command timeout for android to " + secs + " secs");
  this.origCommandTimeoutMs = this.commandTimeoutMs;
  this.commandTimeoutMs = secs * 1000;
  this.resetTimeout();
  cb(null, {
    status: status.codes.Success.code
    , value: ''
  });
};

Android.prototype.resetCommandTimeout = function(cb) {
  this.commandTimeoutMs = this.origCommandTimeoutMs;
  this.resetTimeout();
  cb(null, {
    status: status.codes.Success.code
    , value: ''
  });
};

Android.prototype.getCommandTimeout = function(cb) {
  this.resetTimeout();
  cb(null, {
    status: status.codes.Success.code
    , value: this.commandTimeoutMs / 1000
  });
};

Android.prototype.findElement = function(strategy, selector, cb) {
  this.findElementOrElements(strategy, selector, false, "", cb);
};

Android.prototype.findElements = function(strategy, selector, cb) {
  this.findElementOrElements(strategy, selector, true, "", cb);
};

Android.prototype.findElementOrElements = function(strategy, selector, many, context, cb) {
  var params = {
    strategy: strategy
    , selector: selector
    , context: context
    , multiple: many
  };
  var xpathError = false;
  if (strategy === "xpath") {
    var xpathParams = parseXpath(selector);
    if (!xpathParams) {
      xpathError = true;
    } else {
      // massage for the javas
      if (xpathParams.attr === null) {
        xpathParams.attr = "";
      }
      if (xpathParams.constraint === null) {
        xpathParams.constraint = "";
      }
      params = _.extend(params, xpathParams);
    }
  }
  var doFind = _.bind(function(findCb) {
    this.proxy(["find", params], function(err, res) {
      if (err) {
        findCb(false, err, res);
      } else {
        if (!many && res.status === 0) {
          findCb(true, err, res);
        } else if (many && res.value.length > 0) {
          findCb(true, err, res);
        } else {
          findCb(false, err, res);
        }
      }
    });
  }, this);
  if (!xpathError) {
    this.waitForCondition(this.implicitWaitMs, doFind, cb);
  } else {
    cb(null, {
      status: status.codes.XPathLookupError.code
      , value: "Could not parse xpath data from " + selector
    });
  }
};

Android.prototype.findElementFromElement = function(element, strategy, selector, cb) {
  this.findElementOrElements(strategy, selector, false, element, cb);
};

Android.prototype.findElementsFromElement = function(element, strategy, selector, cb) {
  this.findElementOrElements(strategy, selector, true, element, cb);
};

Android.prototype.setValueImmediate = function(elementId, value, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.setValue = function(elementId, value, cb) {
  this.proxy(["element:setText", {elementId: elementId, text: value}], cb);
};

Android.prototype.click = function(elementId, cb) {
  this.proxy(["element:click", {elementId: elementId}], cb);
};

Android.prototype.complexTap = function(tapCount, touchCount, duration, x, y, elementId, cb) {
  this.proxy(["click", {x: x, y: y}], cb);
};

Android.prototype.clear = function(elementId, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getText = function(elementId, cb) {
  this.proxy(["element:getText", {elementId: elementId}], cb);
};

Android.prototype.getAttribute = function(elementId, attributeName, cb) {
  var p = {elementId: elementId, attribute: attributeName};
  this.proxy(["element:getAttribute", p], cb);
};

Android.prototype.getLocation = function(elementId, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getSize = function(elementId, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getWindowSize = function(windowHandle, cb) {
  this.proxy(["getDeviceSize"], cb);
};

Android.prototype.getPageIndex = function(elementId, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.keys = function(elementId, keys, cb) {
  this.proxy(["element:setText", {elementId: elementId, text: keys}], cb);
};

Android.prototype.frame = function(frame, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.implicitWait = function(ms, cb) {
  this.implicitWaitMs = parseInt(ms, 10);
  logger.info("Set Android implicit wait to " + ms + "ms");
  cb(null, {
    status: status.codes.Success.code
    , value: null
  });
};

Android.prototype.elementDisplayed = function(elementId, cb) {
  var p = {elementId: elementId, attribute: "displayed"};
  this.proxy(["element:getAttribute", p], cb);
};

Android.prototype.elementEnabled = function(elementId, cb) {
  var p = {elementId: elementId, attribute: "enabled"};
  this.proxy(["element:getAttribute", p], cb);
};

Android.prototype.getPageSource = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getAlertText = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.postAcceptAlert = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.postDismissAlert = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getOrientation = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.setOrientation = function(orientation, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getScreenshot = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.fakeFlick = function(xSpeed, ySpeed, swipe, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.fakeFlickElement = function(elementId, xoffset, yoffset, speed, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.swipe = function(startX, startY, endX, endY, duration, touchCount, elId, cb) {
  if (startX === 'null') {
    startX = 0.5;
  }
  if (startY === 'null') {
    startY = 0.5;
  }
  var swipeOpts = {
    startX: startX
    , startY: startY
    , endX: endX
    , endY: endY
    , steps: Math.round((duration * 1000) / 5)
  };
  if (elId !== null) {
    swipeOpts.elementId = elId;
    this.proxy(["element:swipe", swipeOpts], cb);
  } else {
    this.proxy(["swipe", swipeOpts], cb);
  }
};

Android.prototype.flick = function(startX, startY, endX, endY, touchCount, elId, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.hideKeyboard = function(keyName, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.url = function(url, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.active = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getWindowHandle = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getWindowHandles = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.setWindow = function(name, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.clearWebView = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.execute = function(script, args, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.title = function(cb) {
    cb(new NotYetImplementedError(), null);
};

module.exports = function(opts) {
  return new Android(opts);
};
<|endoftext|>


<commit_before>var dh = YAHOO.ext.DomHelper;
var commentTemplate = null;

// State variables
var gCommentSections = {};
var gYourComments = {};
var gReviews = {};

function getApiPath() {
    return '/api/json/reviewrequests/' + gReviewRequestId;
}

function normalizeURL(url) {
	i = url.indexOf("#");

	if (i != -1) {
		url = url.substring(0, i);
	}

	if (url[url.length - 1] == "/") {
		url = url.substring(0, url.length - 1);
	}

	return url;
}

function onEditComplete(field, value, callback) {
    asyncJsonRequest("POST", getApiPath() + '/draft/set/' + field + '/', {
            success: function(rsp) {
                if (callback) {
                    callback(getEl(field), rsp[field]);
                }
                showDraftBanner();
            }.createDelegate(this)
            // TODO: Handle errors
        },
        "value=" + encodeURIComponent(value)
    );
}

function registerEditor(field, multiline) {
	var editor = new RB.widgets.InlineEditor({
		el: field,
		multiline: multiline,
		cls: field + '-editor',
		showEditIcon: true
	});

	editor.on('complete',
		function(editor, value) { onEditComplete(field, value); },
		this, true);
}

function registerCommaListEditor(field, onComplete) {
	var editor = new RB.widgets.InlineCommaListEditor({
		el: field,
		cls: field + '-editor',
		showEditIcon: true,
		useEditIconOnly: true,
		notifyUnchangedCompletion: true
	});

	editor.on('complete', function(editor, value) {
		onEditComplete(field, value, onComplete);
	});
}

function onBugsChanged(el, list) {
	var str = "";

	for (var i = 0; i < list.length; i++) {
		if (gBugTrackerURL != "") {
			var url = gBugTrackerURL.replace("%s", list[i]);
			str += '' + list[i] + '';
		} else {
			str += list[i];
		}

		if (i < list.length - 1) {
			str += ", ";
		}
	}

	el.dom.innerHTML = str;
}

function onTargetPeopleChanged(el, list) {
	var str = "";

	for (var i = 0; i < list.length; i++) {
		str += "";
		str += list[i].username + "";

		if (i < list.length - 1) {
			str += ", ";
		}
	}

	el.dom.innerHTML = str;
}

function onTargetGroupsChanged(el, list) {
	var str = "";

	for (var i = 0; i < list.length; i++) {
		str += "";
		str += list[i].name + "";

		if (i < list.length - 1) {
			str += ", ";
		}
	}

	el.dom.innerHTML = str;
}

function disableDraftButtons() {
	getEl('btn-draft-save').dom.setAttribute('disabled', 'true');
	getEl('btn-draft-revert').dom.setAttribute('disabled', 'true');
}

function enableDraftButtons() {
	getEl('btn-draft-save').dom.removeAttribute('disabled');
	getEl('btn-draft-revert').dom.removeAttribute('disabled');
}

var errorID = 0;
function showError(text) {
	var id = 'error' + errorID;
	var closeHandler = "hideError('" + id + "');";

	dh.append(getEl('error').dom, {
		tag: 'div', id: id, children: [
			{tag: 'h1', html: 'Error:'},
			{html: text},
			{tag: 'input', type: 'submit',
			 value: 'Dismiss', onClick: closeHandler}
		]
	});
	errorID += 1;
}

function showServerError(specific) {
	showError(specific +
	          " Please try again later. If this continues to" +
	          " happen, please report it to your administrator");
}

function hideError(error) {
	var node = getEl(error);
	if (node != null) {
		node.remove();
	}
}

function submitDraft() {
	disableDraftButtons();
	asyncJsonRequest('POST', getApiPath() + '/draft/save/', {
		success: hideDraftBanner,
		failure: function(errmsg) {
			showServerError('Saving the draft has failed due to a server error:' + errmsg);
			enableDraftButtons();
		}
	});
}

function revertDraft() {
	disableDraftButtons();
	asyncJsonRequest('POST', getApiPath() + '/draft/discard/', {
		success: function() { window.location.reload(); },
		failure: function(errmsg) {
			showServerError('Reverting the draft has failed due to a server error:' + errmsg);
			enableDraftButtons();
		}
	});
}

function showDraftBanner() {
	if (getEl('draft') == null) {
		dh.append(getEl('main-banner').dom, {
			tag: 'div', id: 'draft', children: [
				{tag: 'h1', html: 'This review request is a draft.'},
				{html: ' Be sure to save when finished.'},
				{tag: 'input', type: 'submit', id: 'btn-draft-save',
				 value: 'Save', onClick: 'submitDraft();'},
				{tag: 'input', type: 'submit', id: 'btn-draft-revert',
				 value: 'Revert', onClick: 'revertDraft();'}
			]
		});
	}
}

function hideDraftBanner() {
	var node = getEl('draft') || getEl('discard');

	if (node != null) {
		node.remove();
	}
}

function showDiscardBanner() {
	dh.append(getEl('main-banner').dom, {
		tag: 'div', id: 'discard', children: [
			{tag: 'h1', html: 'Confirm Discard?'},
			{html:' This cannot be undone.'},
			{tag: 'input', type: 'submit', value: 'Confirm',
			 onClick: 'discardReview();'},
			{tag: 'input', type: 'submit', value: 'Cancel',
			 onClick: 'hideDraftBanner();'}
		]
	});
}

function discardReview() {
	window.location = normalizeURL(window.location.href) + "/discard/";
}

function showCommentForm(review_id, section_id) {
	if (commentTemplate == null) {
		commentTemplate = new YAHOO.ext.DomHelper.createTemplate({
			tag: 'li',
			cls: 'reply-comment draft',
			id: "{id}-item",
			children: [{
				tag: 'dl',
				children: [{
					tag: 'dt',
					children: [{
						tag: 'label',
						htmlFor: '{id}',
						children: [{
							tag: 'a',
							href: gUserURL,
							html: gUserFullName
						}]
					}, {
						tag: 'dd',
						children: [{
							tag: 'pre',
							id: '{id}'
						}]
					}]
				}]
			}]
		});
	}

	var list = getEl(section_id);
	var yourcomment_id = "yourcomment_" + section_id + "-draft";
	gYourComments[section_id] = yourcomment_id;

	commentTemplate.append(list.dom, {id: yourcomment_id});
	gCommentSections[section_id].yourcomment = getEl(yourcomment_id);

	var editor = registerCommentEditor(review_id, section_id, yourcomment_id);
	autosetAddCommentVisibility(section_id);

	editor.startEdit();
}

function removeCommentForm(review_id, section_id, yourcomment_id) {
	var item = getEl(yourcomment_id + "-item");
	item.hide(true, .35, item.remove.createDelegate(item))

	gYourComments[section_id] = null;
	gCommentSections[section_id].yourcomment = null;
	autosetAddCommentVisibility(section_id);

	gReviews[review_id].yourCommentCount--;

	if (gReviews[review_id].yourCommentCount == 0) {
		hideReplyDraftBanner(review_id);
	}
}

function registerCommentEditor(review_id, section_id, yourcomment_id) {
	gYourComments[section_id] = yourcomment_id;

	if (!gReviews[review_id]) {
		gReviews[review_id] = { yourCommentCount: 0 };
	}

	gReviews[review_id].yourCommentCount++;

	var editor = new RB.widgets.InlineEditor({
		el: getEl(yourcomment_id),
		multiline: true,
		cls: 'inline-comment-editor',
		stripTags: true,
		showEditIcon: true,
		notifyUnchangedCompletion: true
	});

	var cb = function(editor, value) {
		onReplyEditComplete(value, section_id, yourcomment_id);
	};

	editor.on('complete', cb, this, true);
	editor.on('cancel', cb, this, true);

	return editor;
}

function onReplyEditComplete(value, section_id, yourcomment_id) {
	var review_id = gCommentSections[section_id].review_id;

	var postData =
		"value="     + encodeURIComponent(value) + "&" +
		"id="        + gCommentSections[section_id].id + "&" +
		"type="      + gCommentSections[section_id].type + "&" +
		"review_id=" + review_id

    asyncJsonRequest(
        "POST", getApiPath() + "/reviews/" + review_id + "/replies/draft/", {
		success: function(rsp) {
			if (value.stripTags().strip() == "") {
				removeCommentForm(review_id, section_id, yourcomment_id);
			}
		}.createDelegate(this),

		failure: function(errmsg, rsp) {
			// TODO: Show an error
		}.createDelegate(this)
	}, postData);

	showReplyDraftBanner(review_id);
}

function registerCommentSection(reviewid, section_id, context_id, context_type) {
	gCommentSections[section_id] = {
		comments_list: getEl(section_id),
		add_comment: getEl('add_comment_' + section_id),
		review_id: reviewid,
		id: context_id,
		type: context_type
	};

	if (gYourComments[section_id]) {
		gCommentSections[section_id].yourcomment =
			getEl(gYourComments[section_id]);
		showReplyDraftBanner(reviewid);
	}

	autosetAddCommentVisibility(section_id);
}

function showReplyDraftBanner(review_id) {
	if (getEl(review_id + '-draft') == null) {
		dh.append(getEl(review_id + '-banner').dom, {
			tag: 'div', id: review_id + '-draft', children: [
				{tag: 'h1', html: 'This reply is a draft.'},
				{html: ' Be sure to save when finished.'},
				{tag: 'input', type: 'submit',
				 id: review_id + '-btn-draft-save',
				 value: 'Save',
				 onClick: "submitReplyDraft('" + review_id + "');"},
				{tag: 'input', type: 'submit',
				 id: review_id + '-btn-draft-discard',
				 value: 'Discard',
				 onClick: "discardReplyDraft('" + review_id + "');"}
			]
		});
	}
}

function submitReplyDraft(review_id) {
	disableReplyDraftButtons(review_id);
	var url = getApiPath() + '/reviews/' + review_id + '/replies/draft/save/';
    asyncJsonRequest("POST", url, {
		success: function() { window.location.reload(); },
		failure: function(rsp) {
			showServerError('Saving the reply draft has failed due to a server error.');
			enableReplyDraftButtons(review_id);
		}
	});
}

function discardReplyDraft(review_id) {
	disableReplyDraftButtons(review_id);
	var url = getApiPath() + '/reviews/' + review_id + '/replies/draft/discard/';
    asyncJsonRequest("POST", url, {
		success: function() { window.location.reload(); },
		failure: function(rsp) {
			showServerError('Discarding the reply draft has failed due to a server error.');
			enableReplyDraftButtons(review_id);
		}
	});
}

function disableReplyDraftButtons(review_id) {
	getEl(review_id + '-btn-draft-save').dom.setAttribute('disabled', 'true');
	getEl(review_id + '-btn-draft-discard').dom.setAttribute('disabled', 'true');
}

function enableReplyDraftButtons(review_id) {
	getEl(review_id + '-btn-draft-save').dom.removeAttribute('disabled');
	getEl(review_id + '-btn-draft-discard').dom.removeAttribute('disabled');
}

function hideReplyDraftBanner(review_id) {
	var node = getEl(review_id + '-draft') || getEl(review_id + '-discard');

	if (node != null) {
		node.remove();
	}
}

function autosetAddCommentVisibility(section_id) {
	if (gYourComments[section_id]) {
		gCommentSections[section_id].add_comment.hide(true);
	} else {
		gCommentSections[section_id].add_comment.show(true);
	}
}

function publishDraft() {
	var target_groups = document.getElementById("target_groups");
	var target_people = document.getElementById("target_people");

	if (target_groups.innerHTML.strip() == "" &&
	    target_people.innerHTML.strip() == "") {

		alert("There must be at least one reviewer before this review " +
		      "request can be published.");
	} else {
		window.location = normalizeURL(window.location.href) + "/publish/";
	}
}
<commit_msg>We publish reply drafts. We don't just save them like review request drafts. Make it clear that this is being published. Closes bug #57.<commit_after>var dh = YAHOO.ext.DomHelper;
var commentTemplate = null;

// State variables
var gCommentSections = {};
var gYourComments = {};
var gReviews = {};

function getApiPath() {
    return '/api/json/reviewrequests/' + gReviewRequestId;
}

function normalizeURL(url) {
	i = url.indexOf("#");

	if (i != -1) {
		url = url.substring(0, i);
	}

	if (url[url.length - 1] == "/") {
		url = url.substring(0, url.length - 1);
	}

	return url;
}

function onEditComplete(field, value, callback) {
    asyncJsonRequest("POST", getApiPath() + '/draft/set/' + field + '/', {
            success: function(rsp) {
                if (callback) {
                    callback(getEl(field), rsp[field]);
                }
                showDraftBanner();
            }.createDelegate(this)
            // TODO: Handle errors
        },
        "value=" + encodeURIComponent(value)
    );
}

function registerEditor(field, multiline) {
	var editor = new RB.widgets.InlineEditor({
		el: field,
		multiline: multiline,
		cls: field + '-editor',
		showEditIcon: true
	});

	editor.on('complete',
		function(editor, value) { onEditComplete(field, value); },
		this, true);
}

function registerCommaListEditor(field, onComplete) {
	var editor = new RB.widgets.InlineCommaListEditor({
		el: field,
		cls: field + '-editor',
		showEditIcon: true,
		useEditIconOnly: true,
		notifyUnchangedCompletion: true
	});

	editor.on('complete', function(editor, value) {
		onEditComplete(field, value, onComplete);
	});
}

function onBugsChanged(el, list) {
	var str = "";

	for (var i = 0; i < list.length; i++) {
		if (gBugTrackerURL != "") {
			var url = gBugTrackerURL.replace("%s", list[i]);
			str += '' + list[i] + '';
		} else {
			str += list[i];
		}

		if (i < list.length - 1) {
			str += ", ";
		}
	}

	el.dom.innerHTML = str;
}

function onTargetPeopleChanged(el, list) {
	var str = "";

	for (var i = 0; i < list.length; i++) {
		str += "";
		str += list[i].username + "";

		if (i < list.length - 1) {
			str += ", ";
		}
	}

	el.dom.innerHTML = str;
}

function onTargetGroupsChanged(el, list) {
	var str = "";

	for (var i = 0; i < list.length; i++) {
		str += "";
		str += list[i].name + "";

		if (i < list.length - 1) {
			str += ", ";
		}
	}

	el.dom.innerHTML = str;
}

function disableDraftButtons() {
	getEl('btn-draft-save').dom.setAttribute('disabled', 'true');
	getEl('btn-draft-revert').dom.setAttribute('disabled', 'true');
}

function enableDraftButtons() {
	getEl('btn-draft-save').dom.removeAttribute('disabled');
	getEl('btn-draft-revert').dom.removeAttribute('disabled');
}

var errorID = 0;
function showError(text) {
	var id = 'error' + errorID;
	var closeHandler = "hideError('" + id + "');";

	dh.append(getEl('error').dom, {
		tag: 'div', id: id, children: [
			{tag: 'h1', html: 'Error:'},
			{html: text},
			{tag: 'input', type: 'submit',
			 value: 'Dismiss', onClick: closeHandler}
		]
	});
	errorID += 1;
}

function showServerError(specific) {
	showError(specific +
	          " Please try again later. If this continues to" +
	          " happen, please report it to your administrator");
}

function hideError(error) {
	var node = getEl(error);
	if (node != null) {
		node.remove();
	}
}

function submitDraft() {
	disableDraftButtons();
	asyncJsonRequest('POST', getApiPath() + '/draft/save/', {
		success: hideDraftBanner,
		failure: function(errmsg) {
			showServerError('Saving the draft has failed due to a server error:' + errmsg);
			enableDraftButtons();
		}
	});
}

function revertDraft() {
	disableDraftButtons();
	asyncJsonRequest('POST', getApiPath() + '/draft/discard/', {
		success: function() { window.location.reload(); },
		failure: function(errmsg) {
			showServerError('Reverting the draft has failed due to a server error:' + errmsg);
			enableDraftButtons();
		}
	});
}

function showDraftBanner() {
	if (getEl('draft') == null) {
		dh.append(getEl('main-banner').dom, {
			tag: 'div', id: 'draft', children: [
				{tag: 'h1', html: 'This review request is a draft.'},
				{html: ' Be sure to save when finished.'},
				{tag: 'input', type: 'submit', id: 'btn-draft-save',
				 value: 'Save', onClick: 'submitDraft();'},
				{tag: 'input', type: 'submit', id: 'btn-draft-revert',
				 value: 'Revert', onClick: 'revertDraft();'}
			]
		});
	}
}

function hideDraftBanner() {
	var node = getEl('draft') || getEl('discard');

	if (node != null) {
		node.remove();
	}
}

function showDiscardBanner() {
	dh.append(getEl('main-banner').dom, {
		tag: 'div', id: 'discard', children: [
			{tag: 'h1', html: 'Confirm Discard?'},
			{html:' This cannot be undone.'},
			{tag: 'input', type: 'submit', value: 'Confirm',
			 onClick: 'discardReview();'},
			{tag: 'input', type: 'submit', value: 'Cancel',
			 onClick: 'hideDraftBanner();'}
		]
	});
}

function discardReview() {
	window.location = normalizeURL(window.location.href) + "/discard/";
}

function showCommentForm(review_id, section_id) {
	if (commentTemplate == null) {
		commentTemplate = new YAHOO.ext.DomHelper.createTemplate({
			tag: 'li',
			cls: 'reply-comment draft',
			id: "{id}-item",
			children: [{
				tag: 'dl',
				children: [{
					tag: 'dt',
					children: [{
						tag: 'label',
						htmlFor: '{id}',
						children: [{
							tag: 'a',
							href: gUserURL,
							html: gUserFullName
						}]
					}, {
						tag: 'dd',
						children: [{
							tag: 'pre',
							id: '{id}'
						}]
					}]
				}]
			}]
		});
	}

	var list = getEl(section_id);
	var yourcomment_id = "yourcomment_" + section_id + "-draft";
	gYourComments[section_id] = yourcomment_id;

	commentTemplate.append(list.dom, {id: yourcomment_id});
	gCommentSections[section_id].yourcomment = getEl(yourcomment_id);

	var editor = registerCommentEditor(review_id, section_id, yourcomment_id);
	autosetAddCommentVisibility(section_id);

	editor.startEdit();
}

function removeCommentForm(review_id, section_id, yourcomment_id) {
	var item = getEl(yourcomment_id + "-item");
	item.hide(true, .35, item.remove.createDelegate(item))

	gYourComments[section_id] = null;
	gCommentSections[section_id].yourcomment = null;
	autosetAddCommentVisibility(section_id);

	gReviews[review_id].yourCommentCount--;

	if (gReviews[review_id].yourCommentCount == 0) {
		hideReplyDraftBanner(review_id);
	}
}

function registerCommentEditor(review_id, section_id, yourcomment_id) {
	gYourComments[section_id] = yourcomment_id;

	if (!gReviews[review_id]) {
		gReviews[review_id] = { yourCommentCount: 0 };
	}

	gReviews[review_id].yourCommentCount++;

	var editor = new RB.widgets.InlineEditor({
		el: getEl(yourcomment_id),
		multiline: true,
		cls: 'inline-comment-editor',
		stripTags: true,
		showEditIcon: true,
		notifyUnchangedCompletion: true
	});

	var cb = function(editor, value) {
		onReplyEditComplete(value, section_id, yourcomment_id);
	};

	editor.on('complete', cb, this, true);
	editor.on('cancel', cb, this, true);

	return editor;
}

function onReplyEditComplete(value, section_id, yourcomment_id) {
	var review_id = gCommentSections[section_id].review_id;

	var postData =
		"value="     + encodeURIComponent(value) + "&" +
		"id="        + gCommentSections[section_id].id + "&" +
		"type="      + gCommentSections[section_id].type + "&" +
		"review_id=" + review_id

    asyncJsonRequest(
        "POST", getApiPath() + "/reviews/" + review_id + "/replies/draft/", {
		success: function(rsp) {
			if (value.stripTags().strip() == "") {
				removeCommentForm(review_id, section_id, yourcomment_id);
			}
		}.createDelegate(this),

		failure: function(errmsg, rsp) {
			// TODO: Show an error
		}.createDelegate(this)
	}, postData);

	showReplyDraftBanner(review_id);
}

function registerCommentSection(reviewid, section_id, context_id, context_type) {
	gCommentSections[section_id] = {
		comments_list: getEl(section_id),
		add_comment: getEl('add_comment_' + section_id),
		review_id: reviewid,
		id: context_id,
		type: context_type
	};

	if (gYourComments[section_id]) {
		gCommentSections[section_id].yourcomment =
			getEl(gYourComments[section_id]);
		showReplyDraftBanner(reviewid);
	}

	autosetAddCommentVisibility(section_id);
}

function showReplyDraftBanner(review_id) {
	if (getEl(review_id + '-draft') == null) {
		dh.append(getEl(review_id + '-banner').dom, {
			tag: 'div', id: review_id + '-draft', children: [
				{tag: 'h1', html: 'This reply is a draft.'},
				{html: ' Be sure to publish when finished.'},
				{tag: 'input', type: 'submit',
				 id: review_id + '-btn-draft-save',
				 value: 'Publish',
				 onClick: "submitReplyDraft('" + review_id + "');"},
				{tag: 'input', type: 'submit',
				 id: review_id + '-btn-draft-discard',
				 value: 'Discard',
				 onClick: "discardReplyDraft('" + review_id + "');"}
			]
		});
	}
}

function submitReplyDraft(review_id) {
	disableReplyDraftButtons(review_id);
	var url = getApiPath() + '/reviews/' + review_id + '/replies/draft/save/';
    asyncJsonRequest("POST", url, {
		success: function() { window.location.reload(); },
		failure: function(rsp) {
			showServerError('Saving the reply draft has failed due to a server error.');
			enableReplyDraftButtons(review_id);
		}
	});
}

function discardReplyDraft(review_id) {
	disableReplyDraftButtons(review_id);
	var url = getApiPath() + '/reviews/' + review_id + '/replies/draft/discard/';
    asyncJsonRequest("POST", url, {
		success: function() { window.location.reload(); },
		failure: function(rsp) {
			showServerError('Discarding the reply draft has failed due to a server error.');
			enableReplyDraftButtons(review_id);
		}
	});
}

function disableReplyDraftButtons(review_id) {
	getEl(review_id + '-btn-draft-save').dom.setAttribute('disabled', 'true');
	getEl(review_id + '-btn-draft-discard').dom.setAttribute('disabled', 'true');
}

function enableReplyDraftButtons(review_id) {
	getEl(review_id + '-btn-draft-save').dom.removeAttribute('disabled');
	getEl(review_id + '-btn-draft-discard').dom.removeAttribute('disabled');
}

function hideReplyDraftBanner(review_id) {
	var node = getEl(review_id + '-draft') || getEl(review_id + '-discard');

	if (node != null) {
		node.remove();
	}
}

function autosetAddCommentVisibility(section_id) {
	if (gYourComments[section_id]) {
		gCommentSections[section_id].add_comment.hide(true);
	} else {
		gCommentSections[section_id].add_comment.show(true);
	}
}

function publishDraft() {
	var target_groups = document.getElementById("target_groups");
	var target_people = document.getElementById("target_people");

	if (target_groups.innerHTML.strip() == "" &&
	    target_people.innerHTML.strip() == "") {

		alert("There must be at least one reviewer before this review " +
		      "request can be published.");
	} else {
		window.location = normalizeURL(window.location.href) + "/publish/";
	}
}
<|endoftext|>


<commit_before>
//
// TODO: Use WebGL and instancing for quicker renders
// TODO: Pause entire profiler: does it discard incoming samples or accept them?
//

PixelTimeRange = (function()
{
	function PixelTimeRange(start_us, span_us, span_px)
	{
		this.Span_px = span_px;
		this.Set(start_us, span_us);
	}

	PixelTimeRange.prototype.Set = function(start_us, span_us)
	{
		this.Start_us = start_us;
		this.Span_us = span_us;
		this.End_us = this.Start_us + span_us;
		this.usPerPixel = this.Span_px / this.Span_us;
	}

	PixelTimeRange.prototype.SetStart = function(start_us)
	{
		this.Start_us = start_us;
		this.End_us = start_us + this.Span_us;
	}

	PixelTimeRange.prototype.SetEnd = function(end_us)
	{
		this.End_us = end_us;
		this.Start_us = end_us - this.Span_us;
	}

	PixelTimeRange.prototype.SetPixelSpan = function(span_px)
	{
		this.Span_px = span_px;
		this.usPerPixel = this.Span_px / this.Span_us;
	}

	PixelTimeRange.prototype.PixelOffset = function(time_us)
	{
		return Math.floor((time_us - this.Start_us) * this.usPerPixel);
	}

	PixelTimeRange.prototype.PixelSize = function(time_us)
	{
		return time_us * this.usPerPixel;
	}

	return PixelTimeRange;
})();

TimelineRow = (function()
{
	var row_template = "								\

						\

		\
			<canvas class='TimelineRowCanvas'></canvas>		\

";

	function TimelineRow(name, width, parent_node)
	{
		this.Name = name;

		// Create the row HTML and add to the parent
		this.ContainerNode = DOM.Node.CreateHTML(row_template);
		this.Node = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowData");
		this.LabelNode = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowLabel");
		this.LabelNode.innerHTML = name;
		this.CanvasNode = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowCanvas");
		parent_node.appendChild(this.ContainerNode);

		// Setup the canvas
		this.Ctx = this.CanvasNode.getContext("2d");
		this.SetSize(width);
		this.Clear();

		// Frame index to start at when looking for first visible sample
		this.StartFrameIndex = 0;

		this.FrameHistory = null;
		this.VisibleFrames = [ ];
	}

	TimelineRow.prototype.SetSize = function(width)
	{
		// Must ALWAYS set the width/height properties together. Setting one on its own has weird side-effects.
		this.CanvasNode.width = width;
		this.CanvasNode.height = 18;
	}

	TimelineRow.prototype.Clear = function()
	{
		// Outer box is background colour, inner box shows the boundary between thread rows
		this.Ctx.fillStyle = "#2C2C2C"
		this.Ctx.fillRect(0, 0, this.CanvasNode.width, this.CanvasNode.height);
		this.Ctx.fillStyle = "#666"
		var b = 1;
		this.Ctx.fillRect(b, b * 2, this.CanvasNode.width - b * 3, this.CanvasNode.height - b);
	}

	TimelineRow.prototype.SetVisibleFrames = function(frame_history, time_range)
	{
		// Allow null frame history to use existing ones
		// Keep track of frame history for redraw events
		if (frame_history != null)
			this.FrameHistory = frame_history;
		if (this.FrameHistory == null)
			return;

		// Clear previous visible list
		this.VisibleFrames = [ ];
		if (this.FrameHistory.length == 0)
			return;

		// First do a back-track in case the time range moves negatively
		var start_frame_index = this.StartFrameIndex;
		while (start_frame_index > 0)
		{
			var frame = this.FrameHistory[start_frame_index];
			if (time_range.Start_us > frame.StartTime_us)
				break;
			start_frame_index--;
		}

		// Then search from this point for the first visible frame
		while (start_frame_index < this.FrameHistory.length)
		{
			var frame = this.FrameHistory[start_frame_index];
			if (frame.EndTime_us > time_range.Start_us)
				break;
			start_frame_index++;
		}

		// Gather all frames up to the end point
		this.StartFrameIndex = start_frame_index;
		for (var i = start_frame_index; i < this.FrameHistory.length; i++)
		{
			var frame = this.FrameHistory[i];
			if (frame.StartTime_us > time_range.End_us)
				break;
			this.VisibleFrames.push(frame);
		}
	}

	TimelineRow.prototype.Draw = function(time_range)
	{
		this.Clear();

		// Draw all root samples in the visible frame set
		for (var i in this.VisibleFrames)
		{
			var frame = this.VisibleFrames[i];

			for (var j in frame.Samples)
				DrawSample(this, time_range, frame.Samples[j]);
		}
	}

	function DrawSample(self, time_range, sample)
	{
		// Determine location of the sample
		var offset_x = time_range.PixelOffset(sample.cpu_us_start);
		var size_x = time_range.PixelSize(sample.cpu_us_length);

		// Clip to padded range
		size_x = Math.min(offset_x + size_x, self.CanvasNode.width - 5) - offset_x;
		offset_x = Math.max(offset_x, 4);

		self.Ctx.fillStyle = "#BBB";
		self.Ctx.fillRect(offset_x, 5, size_x, 10);
	}

	return TimelineRow;
})();

TimelineWindow = (function()
{
	var BORDER = 10;

	var box_template = "

";

	function TimelineWindow(wm, server)
	{
		// Ordered list of thread rows on the timeline
		this.ThreadRows = [ ];

		// Create window and containers
		this.Window = wm.AddWindow("Timeline", 10, 20, 100, 100);
		this.Window.ShowNoAnim();
		this.TimelineContainer = this.Window.AddControlNew(new WM.Container(10, 10, 800, 160));
		DOM.Node.AddClass(this.TimelineContainer.Node, "TimelineContainer");

		// Setup pause button
		this.Paused = false;
		this.PauseButton = this.Window.AddControlNew(new WM.Button("Pause", 10, 5, { toggle: true }));
		this.PauseButton.SetOnClick(Bind(OnPausePressed, this));

		var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel";
		DOM.Event.AddHandler(this.TimelineContainer.Node, mouse_wheel_event, Bind(OnMouseScroll, this));

		// Setup timeline dragging
		this.MouseDown = false;
		DOM.Event.AddHandler(this.TimelineContainer.Node, "mousedown", Bind(OnMouseDown, this));
		DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseup", Bind(OnMouseUp, this));
		DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseout", Bind(OnMouseOut, this));
		DOM.Event.AddHandler(this.TimelineContainer.Node, "mousemove", Bind(OnMouseMove, this));

		// Set time range AFTER the window has been created, as it uses the window to determine pixel coverage
		this.TimeRange = new PixelTimeRange(0, 2 * 1000 * 1000, RowWidth(this));
	}

	TimelineWindow.prototype.WindowResized = function(width, height, top_window)
	{
		// Resize window
		var top = top_window.Position[1] + top_window.Size[1] + 10;
		this.Window.SetPosition(10, top);
		this.Window.SetSize(width - 2 * 10, 120);

		// Resize controls
		var parent_size = this.Window.Size;
		this.TimelineContainer.SetPosition(BORDER, 30);
		this.TimelineContainer.SetSize(parent_size[0] - 2 * BORDER, 60);

		// Resize rows
		var row_width = RowWidth(this);
		for (var i in this.ThreadRows)
		{
			var row = this.ThreadRows[i];
			row.SetSize(row_width);
		}

		// Adjust time range to new width
		this.TimeRange.SetPixelSpan(row_width);
		DrawAllRows(this);
	}

	TimelineWindow.prototype.OnSamples = function(thread_name, frame_history)
	{
		if (this.Paused)
			return;

		// Shift the timeline to the last entry on this thread
		// As multiple threads come through here with different end frames, only do this for the latest
		var last_frame = frame_history[frame_history.length - 1];
		if (last_frame.EndTime_us > this.TimeRange.End_us)
			this.TimeRange.SetEnd(last_frame.EndTime_us);

		// Search for the index of this thread
		var thread_index = -1;
		for (var i in this.ThreadRows)
		{
			if (this.ThreadRows[i].Name == thread_name)
			{
				thread_index = i;
				break;
			}
		}

		// If this thread has not been seen before, add a new row to the list and re-sort
		if (thread_index == -1)
		{
			var row = new TimelineRow(thread_name, RowWidth(this), this.TimelineContainer.Node);
			this.ThreadRows.push(row);
			this.ThreadRows.sort(function(a, b) { return b.Name.localeCompare(a.Name); });

			// Search again for this new index
			for (var i in this.ThreadRows)
			{
				if (this.ThreadRows[i].Name == thread_name)
				{
					thread_index = i;
					break;
				}
			}
		}

		// Update visible frames for this row and redraw
		var thread_row = this.ThreadRows[thread_index];
		thread_row.SetVisibleFrames(frame_history, this.TimeRange);
		thread_row.Draw(this.TimeRange);
	}

	function RowOffset(self)
	{
		// Add sizing of the label
		// TODO: Use computed size
		return DOM.Node.GetPosition(self.TimelineContainer.Node)[0] + 87;
	}

	function RowWidth(self)
	{
		// Subtract sizing of the label
		// TODO: Use computed size
		return self.TimelineContainer.Size[0] - 87;
	}

	function DrawAllRows(self)
	{
		var time_range = self.TimeRange;
		for (var i in self.ThreadRows)
		{
			var thread_row = self.ThreadRows[i];
			thread_row.SetVisibleFrames(null, time_range);
			thread_row.Draw(time_range);
		}
	}

	function OnPausePressed(self)
	{
		self.Paused = self.PauseButton.IsPressed();
		if (self.Paused)
			self.PauseButton.SetText("Paused");
		else
			self.PauseButton.SetText("Pause");
	}

	function OnMouseScroll(self, evt)
	{
		var mouse_state = new Mouse.State(evt);
		var scale = 1.11;
			if (mouse_state.WheelDelta > 0)
				scale = 1 / scale;

		// What time is the mouse hovering over?
		var x = mouse_state.Position[0] - RowOffset(self);
		var time_us = self.TimeRange.Start_us + x / self.TimeRange.usPerPixel;

		// Calculate start time relative to the mouse hover position
		var time_start_us = self.TimeRange.Start_us - time_us;

		// Scale and offset back to the hover time
		self.TimeRange.Set(time_start_us * scale + time_us, self.TimeRange.Span_us * scale);

		DrawAllRows(self);
	}

	function OnMouseDown(self, evt)
	{
		self.MouseDown = true;
		DOM.Event.StopDefaultAction(evt);
	}

	function OnMouseUp(self, evt)
	{
		self.MouseDown = false;
	}

	function OnMouseOut(self, evt)
	{
		self.MouseDown = false;
	}

	function OnMouseMove(self, evt)
	{
		if (self.MouseDown)
		{
			var mouse_state = new Mouse.State(evt);

			// Get the time the mouse is over
			var x = mouse_state.Position[0] - RowOffset(self);
			var time_us = self.TimeRange.Start_us + x / self.TimeRange.usPerPixel;

			// Shift the visible time range with mouse movement
			var time_offset_us = mouse_state.PositionDelta[0] / self.TimeRange.usPerPixel;
			self.TimeRange.SetStart(self.TimeRange.Start_us - time_offset_us);

			DrawAllRows(self);
		}
	}

	return TimelineWindow;
})();<commit_msg>Remove mouseout handling as it was too hard to track compared with whatever benefits it tried to gain.<commit_after>
//
// TODO: Use WebGL and instancing for quicker renders
// TODO: Pause entire profiler: does it discard incoming samples or accept them?
//

PixelTimeRange = (function()
{
	function PixelTimeRange(start_us, span_us, span_px)
	{
		this.Span_px = span_px;
		this.Set(start_us, span_us);
	}

	PixelTimeRange.prototype.Set = function(start_us, span_us)
	{
		this.Start_us = start_us;
		this.Span_us = span_us;
		this.End_us = this.Start_us + span_us;
		this.usPerPixel = this.Span_px / this.Span_us;
	}

	PixelTimeRange.prototype.SetStart = function(start_us)
	{
		this.Start_us = start_us;
		this.End_us = start_us + this.Span_us;
	}

	PixelTimeRange.prototype.SetEnd = function(end_us)
	{
		this.End_us = end_us;
		this.Start_us = end_us - this.Span_us;
	}

	PixelTimeRange.prototype.SetPixelSpan = function(span_px)
	{
		this.Span_px = span_px;
		this.usPerPixel = this.Span_px / this.Span_us;
	}

	PixelTimeRange.prototype.PixelOffset = function(time_us)
	{
		return Math.floor((time_us - this.Start_us) * this.usPerPixel);
	}

	PixelTimeRange.prototype.PixelSize = function(time_us)
	{
		return time_us * this.usPerPixel;
	}

	return PixelTimeRange;
})();

TimelineRow = (function()
{
	var row_template = "								\

						\

		\
			<canvas class='TimelineRowCanvas'></canvas>		\

";

	function TimelineRow(name, width, parent_node)
	{
		this.Name = name;

		// Create the row HTML and add to the parent
		this.ContainerNode = DOM.Node.CreateHTML(row_template);
		this.Node = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowData");
		this.LabelNode = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowLabel");
		this.LabelNode.innerHTML = name;
		this.CanvasNode = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowCanvas");
		parent_node.appendChild(this.ContainerNode);

		// Setup the canvas
		this.Ctx = this.CanvasNode.getContext("2d");
		this.SetSize(width);
		this.Clear();

		// Frame index to start at when looking for first visible sample
		this.StartFrameIndex = 0;

		this.FrameHistory = null;
		this.VisibleFrames = [ ];
	}

	TimelineRow.prototype.SetSize = function(width)
	{
		// Must ALWAYS set the width/height properties together. Setting one on its own has weird side-effects.
		this.CanvasNode.width = width;
		this.CanvasNode.height = 18;
	}

	TimelineRow.prototype.Clear = function()
	{
		// Outer box is background colour, inner box shows the boundary between thread rows
		this.Ctx.fillStyle = "#2C2C2C"
		this.Ctx.fillRect(0, 0, this.CanvasNode.width, this.CanvasNode.height);
		this.Ctx.fillStyle = "#666"
		var b = 1;
		this.Ctx.fillRect(b, b * 2, this.CanvasNode.width - b * 3, this.CanvasNode.height - b);
	}

	TimelineRow.prototype.SetVisibleFrames = function(frame_history, time_range)
	{
		// Allow null frame history to use existing ones
		// Keep track of frame history for redraw events
		if (frame_history != null)
			this.FrameHistory = frame_history;
		if (this.FrameHistory == null)
			return;

		// Clear previous visible list
		this.VisibleFrames = [ ];
		if (this.FrameHistory.length == 0)
			return;

		// First do a back-track in case the time range moves negatively
		var start_frame_index = this.StartFrameIndex;
		while (start_frame_index > 0)
		{
			var frame = this.FrameHistory[start_frame_index];
			if (time_range.Start_us > frame.StartTime_us)
				break;
			start_frame_index--;
		}

		// Then search from this point for the first visible frame
		while (start_frame_index < this.FrameHistory.length)
		{
			var frame = this.FrameHistory[start_frame_index];
			if (frame.EndTime_us > time_range.Start_us)
				break;
			start_frame_index++;
		}

		// Gather all frames up to the end point
		this.StartFrameIndex = start_frame_index;
		for (var i = start_frame_index; i < this.FrameHistory.length; i++)
		{
			var frame = this.FrameHistory[i];
			if (frame.StartTime_us > time_range.End_us)
				break;
			this.VisibleFrames.push(frame);
		}
	}

	TimelineRow.prototype.Draw = function(time_range)
	{
		this.Clear();

		// Draw all root samples in the visible frame set
		for (var i in this.VisibleFrames)
		{
			var frame = this.VisibleFrames[i];

			for (var j in frame.Samples)
				DrawSample(this, time_range, frame.Samples[j]);
		}
	}

	function DrawSample(self, time_range, sample)
	{
		// Determine location of the sample
		var offset_x = time_range.PixelOffset(sample.cpu_us_start);
		var size_x = time_range.PixelSize(sample.cpu_us_length);

		// Clip to padded range
		size_x = Math.min(offset_x + size_x, self.CanvasNode.width - 5) - offset_x;
		offset_x = Math.max(offset_x, 4);

		self.Ctx.fillStyle = "#BBB";
		self.Ctx.fillRect(offset_x, 5, size_x, 10);
	}

	return TimelineRow;
})();

TimelineWindow = (function()
{
	var BORDER = 10;

	var box_template = "

";

	function TimelineWindow(wm, server)
	{
		// Ordered list of thread rows on the timeline
		this.ThreadRows = [ ];

		// Create window and containers
		this.Window = wm.AddWindow("Timeline", 10, 20, 100, 100);
		this.Window.ShowNoAnim();
		this.TimelineContainer = this.Window.AddControlNew(new WM.Container(10, 10, 800, 160));
		DOM.Node.AddClass(this.TimelineContainer.Node, "TimelineContainer");

		// Setup pause button
		this.Paused = false;
		this.PauseButton = this.Window.AddControlNew(new WM.Button("Pause", 10, 5, { toggle: true }));
		this.PauseButton.SetOnClick(Bind(OnPausePressed, this));

		var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel";
		DOM.Event.AddHandler(this.TimelineContainer.Node, mouse_wheel_event, Bind(OnMouseScroll, this));

		// Setup timeline dragging
		this.MouseDown = false;
		DOM.Event.AddHandler(this.TimelineContainer.Node, "mousedown", Bind(OnMouseDown, this));
		DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseup", Bind(OnMouseUp, this));
		DOM.Event.AddHandler(this.TimelineContainer.Node, "mousemove", Bind(OnMouseMove, this));

		// Set time range AFTER the window has been created, as it uses the window to determine pixel coverage
		this.TimeRange = new PixelTimeRange(0, 2 * 1000 * 1000, RowWidth(this));
	}

	TimelineWindow.prototype.WindowResized = function(width, height, top_window)
	{
		// Resize window
		var top = top_window.Position[1] + top_window.Size[1] + 10;
		this.Window.SetPosition(10, top);
		this.Window.SetSize(width - 2 * 10, 120);

		// Resize controls
		var parent_size = this.Window.Size;
		this.TimelineContainer.SetPosition(BORDER, 30);
		this.TimelineContainer.SetSize(parent_size[0] - 2 * BORDER, 60);

		// Resize rows
		var row_width = RowWidth(this);
		for (var i in this.ThreadRows)
		{
			var row = this.ThreadRows[i];
			row.SetSize(row_width);
		}

		// Adjust time range to new width
		this.TimeRange.SetPixelSpan(row_width);
		DrawAllRows(this);
	}

	TimelineWindow.prototype.OnSamples = function(thread_name, frame_history)
	{
		if (this.Paused)
			return;

		// Shift the timeline to the last entry on this thread
		// As multiple threads come through here with different end frames, only do this for the latest
		var last_frame = frame_history[frame_history.length - 1];
		if (last_frame.EndTime_us > this.TimeRange.End_us)
			this.TimeRange.SetEnd(last_frame.EndTime_us);

		// Search for the index of this thread
		var thread_index = -1;
		for (var i in this.ThreadRows)
		{
			if (this.ThreadRows[i].Name == thread_name)
			{
				thread_index = i;
				break;
			}
		}

		// If this thread has not been seen before, add a new row to the list and re-sort
		if (thread_index == -1)
		{
			var row = new TimelineRow(thread_name, RowWidth(this), this.TimelineContainer.Node);
			this.ThreadRows.push(row);
			this.ThreadRows.sort(function(a, b) { return b.Name.localeCompare(a.Name); });

			// Search again for this new index
			for (var i in this.ThreadRows)
			{
				if (this.ThreadRows[i].Name == thread_name)
				{
					thread_index = i;
					break;
				}
			}
		}

		// Update visible frames for this row and redraw
		var thread_row = this.ThreadRows[thread_index];
		thread_row.SetVisibleFrames(frame_history, this.TimeRange);
		thread_row.Draw(this.TimeRange);
	}

	function RowOffset(self)
	{
		// Add sizing of the label
		// TODO: Use computed size
		return DOM.Node.GetPosition(self.TimelineContainer.Node)[0] + 87;
	}

	function RowWidth(self)
	{
		// Subtract sizing of the label
		// TODO: Use computed size
		return self.TimelineContainer.Size[0] - 87;
	}

	function DrawAllRows(self)
	{
		var time_range = self.TimeRange;
		for (var i in self.ThreadRows)
		{
			var thread_row = self.ThreadRows[i];
			thread_row.SetVisibleFrames(null, time_range);
			thread_row.Draw(time_range);
		}
	}

	function OnPausePressed(self)
	{
		self.Paused = self.PauseButton.IsPressed();
		if (self.Paused)
			self.PauseButton.SetText("Paused");
		else
			self.PauseButton.SetText("Pause");
	}

	function OnMouseScroll(self, evt)
	{
		var mouse_state = new Mouse.State(evt);
		var scale = 1.11;
			if (mouse_state.WheelDelta > 0)
				scale = 1 / scale;

		// What time is the mouse hovering over?
		var x = mouse_state.Position[0] - RowOffset(self);
		var time_us = self.TimeRange.Start_us + x / self.TimeRange.usPerPixel;

		// Calculate start time relative to the mouse hover position
		var time_start_us = self.TimeRange.Start_us - time_us;

		// Scale and offset back to the hover time
		self.TimeRange.Set(time_start_us * scale + time_us, self.TimeRange.Span_us * scale);

		DrawAllRows(self);
	}

	function OnMouseDown(self, evt)
	{
		self.MouseDown = true;
		DOM.Event.StopDefaultAction(evt);
	}

	function OnMouseUp(self, evt)
	{
		self.MouseDown = false;
	}

	function OnMouseMove(self, evt)
	{
		if (self.MouseDown)
		{
			var mouse_state = new Mouse.State(evt);

			// Get the time the mouse is over
			var x = mouse_state.Position[0] - RowOffset(self);
			var time_us = self.TimeRange.Start_us + x / self.TimeRange.usPerPixel;

			// Shift the visible time range with mouse movement
			var time_offset_us = mouse_state.PositionDelta[0] / self.TimeRange.usPerPixel;
			self.TimeRange.SetStart(self.TimeRange.Start_us - time_offset_us);

			DrawAllRows(self);
		}
	}

	return TimelineWindow;
})();<|endoftext|>


<commit_before>/**
* the SrsBandwidth object.
* @param container the html container id.
* @param width a float value specifies the width of bandwidth.
* @param height a float value specifies the height of bandwidth.
* @param private_object [optional] an object that used as private object,
*       for example, the logic chat object which owner this bandwidth.
*/
function SrsBandwidth(container, width, height, private_object) {
    if (!SrsBandwidth.__id) {
        SrsBandwidth.__id = 100;
    }
    if (!SrsBandwidth.__bandwidths) {
        SrsBandwidth.__bandwidths = [];
    }

    SrsBandwidth.__bandwidths.push(this);

    this.private_object = private_object;
    this.container = container;
    this.width = width;
    this.height = height;
    this.id = SrsBandwidth.__id++;
    this.stream_url = null;
    this.callbackObj = null;

    // the callback set data.
    this.percent = 0;
    this.status = "";
    this.report = "";
    this.server = "";
}
/**
* user can set some callback, then start the bandwidth.
* @param url the bandwidth test url.
* callbacks:
*      on_bandwidth_ready():void, when srs bandwidth ready, user can play.
*      on_update_progress(percent:Number):void, when srs bandwidth update the progress.
*           percent:Number 100 means 100%.
*      on_update_status(status:String):void, when srs bandwidth update the status.
*           status:String the human readable status text.
*/
SrsBandwidth.prototype.render = function(url) {
    if (url) {
        this.stream_url = url;
    }

    // embed the flash.
    var flashvars = {};
    flashvars.id = this.id;
    flashvars.on_bandwidth_ready = "__srs_on_bandwidth_ready";
    flashvars.on_update_progress = "__srs_on_update_progress";
    flashvars.on_update_status = "__srs_on_update_status";
    flashvars.on_srs_info = "__srs_on_srs_info";
    flashvars.on_complete = "__srs_on_complete";

    var params = {};
    params.wmode = "opaque";
    params.allowFullScreen = "true";
    params.allowScriptAccess = "always";

    var attributes = {};

    var self = this;

    swfobject.embedSWF(
        "srs_bwt/release/srs_bwt.swf?_version="+srs_get_version_code(),
        this.container,
        this.width, this.height,
        "11.1.0", "js/AdobeFlashbandwidthInstall.swf",
        flashvars, params, attributes,
        function(callbackObj){
            self.callbackObj = callbackObj;
        }
    );

    return this;
}
/**
* play the stream.
* @param stream_url the url of stream, rtmp or http.
*/
SrsBandwidth.prototype.check_bandwidth = function(url) {
    this.stop();
    SrsBandwidth.__bandwidths.push(this);

    if (url) {
        this.stream_url = url;
    }

    this.callbackObj.ref.__check_bandwidth(this.stream_url);
}
SrsBandwidth.prototype.stop = function(url) {
    for (var i = 0; i < SrsBandwidth.__bandwidths.length; i++) {
        var bandwidth = SrsBandwidth.__bandwidths[i];

        if (bandwidth.id != this.id) {
            continue;
        }

        SrsBandwidth.__bandwidths.splice(i, 1);
        break;
    }

    this.callbackObj.ref.__stop();
}
SrsBandwidth.prototype.on_bandwidth_ready = function() {
}
SrsBandwidth.prototype.on_update_progress = function(percent) {
}
SrsBandwidth.prototype.on_update_status = function(status) {
}
SrsBandwidth.prototype.on_srs_info = function(srs_server, srs_primary_authors, srs_id, srs_pid, srs_server_ip) {
}
SrsBandwidth.prototype.on_complete = function(start_time, end_time, play_kbps, publish_kbps, play_bytes, publish_bytes, play_time, publish_time) {
}
function __srs_find_bandwidth(id) {
    for (var i = 0; i < SrsBandwidth.__bandwidths.length; i++) {
        var bandwidth = SrsBandwidth.__bandwidths[i];

        if (bandwidth.id != id) {
            continue;
        }

        return bandwidth;
    }

    throw new Error("bandwidth not found. id=" + id);
}
function __srs_on_bandwidth_ready(id) {
    var bandwidth = __srs_find_bandwidth(id);
    bandwidth.on_bandwidth_ready();
}
function __srs_on_update_progress(id, percent) {
    var bandwidth = __srs_find_bandwidth(id);
    bandwidth.percent = percent;
    bandwidth.on_update_progress(percent);
}
function __srs_on_update_status(id, code, data) {
    var bandwidth = __srs_find_bandwidth(id);

    var status = "";
    switch(code){
        case "NetConnection.Connect.Failed":
            status = "连接服务器失败！";
            break;
        case "NetConnection.Connect.Rejected":
            status = "服务器拒绝连接！";
            break;
        case "NetConnection.Connect.Success":
            status = "连接服务器成功!";
            break;
        case "NetConnection.Connect.Closed":
            if (bandwidth.report) {
                return;
            }
            status = "连接已断开!";
            break;
        case "srs.bwtc.play.start":
            status = "开始测试下行带宽";
            break;
        case "srs.bwtc.play.stop":
            status = "下行带宽测试完毕，" + data + "kbps，开始测试上行带宽。";
            break;
        default:
            return;
    }

    bandwidth.status = status;
    bandwidth.on_update_status(status);
}
function __srs_on_srs_info(id, srs_server, srs_primary_authors, srs_id, srs_pid, srs_server_ip) {
    var bandwidth = __srs_find_bandwidth(id);
    bandwidth.status = status;
    bandwidth.server = srs_server_ip;
    bandwidth.on_srs_info(srs_server, srs_primary_authors, srs_id, srs_pid, srs_server_ip);
}
function __srs_on_complete(id, start_time, end_time, play_kbps, publish_kbps, play_bytes, publish_bytes, play_time, publish_time) {
    var bandwidth = __srs_find_bandwidth(id);

    var status = "检测结束: " + bandwidth.server + " 上行: " + publish_kbps + " kbps" + " 下行: " + play_kbps + " kbps"
                + " 测试时间: " + Number((end_time - start_time) / 1000).toFixed(1) + " 秒";
    bandwidth.report = status;
    bandwidth.on_update_status(status);

    bandwidth.on_complete(start_time, end_time, play_kbps, publish_kbps, play_bytes, publish_bytes, play_time, publish_time);
}<commit_msg>refine the js library for bandwidth test.<commit_after>/**
* the SrsBandwidth library for js to do bandwidth test.
* @param container the html container id.
* @param width a float value specifies the width of bandwidth.
* @param height a float value specifies the height of bandwidth.
* @param private_object [optional] an object that used as private object,
*       for example, the logic chat object which owner this bandwidth.
* Usage:
    var bandwidth = new SrsBandwidth("player_id", 100, 1);
    bandwidth.on_bandwidth_ready = function() {
        // auto start check bandwidth when tool is ready.
        this.check_bandwidth(url);
    }
    bandwidth.on_update_progress = function(percent) {
        // console.log(percent + "%");
    }
    bandwidth.on_update_status = function(status) {
        // console.log(status);
    }
    bandwidth.on_srs_info = function(srs_server, srs_primary_authors, srs_id, srs_pid, srs_server_ip) {
        // console.log(
        //    "server:" + srs_server + ", authors:" + srs_primary_authors +
        //    ", srs_id:" + srs_id + ", srs_pid:" + srs_pid + ", ip:" + srs_server_ip
        //);
    }
    bandwidth.render("rtmp://dev:1935/app?key=35c9b402c12a7246868752e2878f7e0e&vhost=bandcheck.srs.com");
*/
function SrsBandwidth(container, width, height, private_object) {
    if (!SrsBandwidth.__id) {
        SrsBandwidth.__id = 100;
    }
    if (!SrsBandwidth.__bandwidths) {
        SrsBandwidth.__bandwidths = [];
    }

    SrsBandwidth.__bandwidths.push(this);

    this.private_object = private_object;
    this.container = container;
    this.width = width;
    this.height = height;
    this.id = SrsBandwidth.__id++;
    this.stream_url = null;
    this.callbackObj = null;

    // the callback set data.
    this.percent = 0;
    this.status = "";
    this.report = "";
    this.server = "";
}
/**
* user can set some callback, then start the bandwidth.
* @param url the bandwidth test url.
* callbacks:
*      on_bandwidth_ready():void, when srs bandwidth ready, user can play.
*      on_update_progress(percent:Number):void, when srs bandwidth update the progress.
*           percent:Number 100 means 100%.
*      on_update_status(status:String):void, when srs bandwidth update the status.
*           status:String the human readable status text.
*/
SrsBandwidth.prototype.render = function(url) {
    if (url) {
        this.stream_url = url;
    }

    // embed the flash.
    var flashvars = {};
    flashvars.id = this.id;
    flashvars.on_bandwidth_ready = "__srs_on_bandwidth_ready";
    flashvars.on_update_progress = "__srs_on_update_progress";
    flashvars.on_update_status = "__srs_on_update_status";
    flashvars.on_srs_info = "__srs_on_srs_info";
    flashvars.on_complete = "__srs_on_complete";

    var params = {};
    params.wmode = "opaque";
    params.allowFullScreen = "true";
    params.allowScriptAccess = "always";

    var attributes = {};

    var self = this;

    swfobject.embedSWF(
        "srs_bwt/release/srs_bwt.swf?_version="+srs_get_version_code(),
        this.container,
        this.width, this.height,
        "11.1.0", "js/AdobeFlashbandwidthInstall.swf",
        flashvars, params, attributes,
        function(callbackObj){
            self.callbackObj = callbackObj;
        }
    );

    return this;
}
/**
* play the stream.
* @param stream_url the url of stream, rtmp or http.
*/
SrsBandwidth.prototype.check_bandwidth = function(url) {
    this.stop();
    SrsBandwidth.__bandwidths.push(this);

    if (url) {
        this.stream_url = url;
    }

    this.callbackObj.ref.__check_bandwidth(this.stream_url);
}
SrsBandwidth.prototype.stop = function(url) {
    for (var i = 0; i < SrsBandwidth.__bandwidths.length; i++) {
        var bandwidth = SrsBandwidth.__bandwidths[i];

        if (bandwidth.id != this.id) {
            continue;
        }

        SrsBandwidth.__bandwidths.splice(i, 1);
        break;
    }

    this.callbackObj.ref.__stop();
}
SrsBandwidth.prototype.on_bandwidth_ready = function() {
}
SrsBandwidth.prototype.on_update_progress = function(percent) {
}
SrsBandwidth.prototype.on_update_status = function(status) {
}
SrsBandwidth.prototype.on_srs_info = function(srs_server, srs_primary_authors, srs_id, srs_pid, srs_server_ip) {
}
SrsBandwidth.prototype.on_complete = function(start_time, end_time, play_kbps, publish_kbps, play_bytes, publish_bytes, play_time, publish_time) {
}
function __srs_find_bandwidth(id) {
    for (var i = 0; i < SrsBandwidth.__bandwidths.length; i++) {
        var bandwidth = SrsBandwidth.__bandwidths[i];

        if (bandwidth.id != id) {
            continue;
        }

        return bandwidth;
    }

    throw new Error("bandwidth not found. id=" + id);
}
function __srs_on_bandwidth_ready(id) {
    var bandwidth = __srs_find_bandwidth(id);
    bandwidth.on_bandwidth_ready();
}
function __srs_on_update_progress(id, percent) {
    var bandwidth = __srs_find_bandwidth(id);
    bandwidth.percent = percent;
    bandwidth.on_update_progress(percent);
}
function __srs_on_update_status(id, code, data) {
    var bandwidth = __srs_find_bandwidth(id);

    var status = "";
    switch(code){
        case "NetConnection.Connect.Failed":
            status = "连接服务器失败！";
            break;
        case "NetConnection.Connect.Rejected":
            status = "服务器拒绝连接！";
            break;
        case "NetConnection.Connect.Success":
            status = "连接服务器成功!";
            break;
        case "NetConnection.Connect.Closed":
            if (bandwidth.report) {
                return;
            }
            status = "连接已断开!";
            break;
        case "srs.bwtc.play.start":
            status = "开始测试下行带宽";
            break;
        case "srs.bwtc.play.stop":
            status = "下行带宽测试完毕，" + data + "kbps，开始测试上行带宽。";
            break;
        default:
            return;
    }

    bandwidth.status = status;
    bandwidth.on_update_status(status);
}
function __srs_on_srs_info(id, srs_server, srs_primary_authors, srs_id, srs_pid, srs_server_ip) {
    var bandwidth = __srs_find_bandwidth(id);
    bandwidth.status = status;
    bandwidth.server = srs_server_ip;
    bandwidth.on_srs_info(srs_server, srs_primary_authors, srs_id, srs_pid, srs_server_ip);
}
function __srs_on_complete(id, start_time, end_time, play_kbps, publish_kbps, play_bytes, publish_bytes, play_time, publish_time) {
    var bandwidth = __srs_find_bandwidth(id);

    var status = "检测结束: " + bandwidth.server + " 上行: " + publish_kbps + " kbps" + " 下行: " + play_kbps + " kbps"
                + " 测试时间: " + Number((end_time - start_time) / 1000).toFixed(1) + " 秒";
    bandwidth.report = status;
    bandwidth.on_update_status(status);

    bandwidth.on_complete(start_time, end_time, play_kbps, publish_kbps, play_bytes, publish_bytes, play_time, publish_time);
}<|endoftext|>


<commit_before>var typewriter = (function () {

	function prepareElement(element, options) {

		// grab the text (as long as it doesn't have (&), (<), or (>) - see https://developer.mozilla.org/en-US/docs/Web/API/Element.innerHTML)
		var text = element.innerHTML;

		var destination = document.createElement('p');
		destination.className = 'typewriter-active';

		if (Modernizr.csstransitions) {

			var fragment = document.createDocumentFragment();

			// split text into characters
			var characters = text.split('');

			// create spans
			var span;

			var opts = options || {};

			var delay;

			// use delay if present,
			if (options.delay) {
				delay = options.delay;
				// otherwise use duration if present,
			} else if (options.duration) {
				delay = options.duration/characters.length;
				// otherwise provide default delay
			} else {
				delay = 0.05;
			}

			for (i = 0; i < characters.length; i++) {
				span = document.createElement('span');
				span.innerHTML = characters[i];

				// TODO: i get a feeling all this vendor prefix nonsense isn't clever

				// using .toFixed(3) - no need to use less than milliseconds
				span.style.setProperty('-webkit-transition-delay', (i*delay).toFixed(3)  + 's');
				span.style.setProperty('-moz-transition-delay',    (i*delay).toFixed(3)  + 's');
				span.style.setProperty('-o-transition-delay',      (i*delay).toFixed(3)  + 's');
				span.style.setProperty('transition-delay',         (i*delay).toFixed(3)  + 's');

				fragment.appendChild(span);
			}

			destination.appendChild(fragment);

		} else {

			destination.innerHTML = text;

		}

		var parent = element.parentNode;
		parent.insertBefore(destination, element);
		parent.removeChild(element);

	}

	function prepare(elements, options) {
		for (var i = 0; i < elements.length; i++) {
			prepareElement(elements[i], options || {});
		}
	}

	function type(element) {
		// wait 10 ms before typing - not exactly sure why i have to do this :(
		var promise = pinkySwear();

		setTimeout(function() {

			var children = element.children;
			for (i = 0; i < children.length; i++) {

				// add event listener on last child
				if (i === children.length - 1) {
					// TODO: add all the vendor prefixes
					children[i].addEventListener('transitionend', function(event) {
						promise(true);
					});

				}

				// show child
				children[i].className = 'show';

			}
		}, 10);

		return promise;
	}

	return {
		prepare: prepare,
		type: type
	};

})();<commit_msg>added vendor prefixes in js - still not sure if this is best approach<commit_after>var typewriter = (function () {

	var transitionEndPrefixes = [
		'webkitTransitionEnd',
		'oTransitionEnd',
		'otransitionend',
		'transitionend'
	];

	var prefixes = [
		'-webkit-',
		'-moz-',
		'-o-',
		''
	];

	function prepareElement(element, options) {

		// grab the text (as long as it doesn't have (&), (<), or (>) - see https://developer.mozilla.org/en-US/docs/Web/API/Element.innerHTML)
		var text = element.innerHTML;

		var destination = document.createElement('p');
		destination.className = 'typewriter-active';

		if (Modernizr.csstransitions) {

			var fragment = document.createDocumentFragment();

			// split text into characters
			var characters = text.split('');

			// create spans
			var span;

			var opts = options || {};

			var delay;
			var computedDelay;

			// use delay if present,
			if (options.delay) {
				delay = options.delay;
				// otherwise use duration if present,
			} else if (options.duration) {
				delay = options.duration/characters.length;
				// otherwise provide default delay
			} else {
				delay = 0.05;
			}

			for (i = 0; i < characters.length; i++) {
				span = document.createElement('span');
				span.innerHTML = characters[i];

				// using .toFixed(3) - no need to use less than milliseconds
				computedDelay = (i*delay).toFixed(3)  + 's';

				// TODO: i get a feeling all this vendor prefix nonsense isn't clever
				for (var j = 0; j < prefixes.length; j++) {
					span.style.setProperty(prefixes[j] + 'transition-delay', computedDelay);
				}

				fragment.appendChild(span);
			}

			destination.appendChild(fragment);

		} else {

			destination.innerHTML = text;

		}

		var parent = element.parentNode;
		parent.insertBefore(destination, element);
		parent.removeChild(element);

	}

	function prepare(elements, options) {
		for (var i = 0; i < elements.length; i++) {
			prepareElement(elements[i], options || {});
		}
	}

	function type(element) {
		// wait 10 ms before typing - not exactly sure why i have to do this :(
		var promise = pinkySwear();

		setTimeout(function() {

			var children = element.children;

			for (i = 0; i < children.length; i++) {

				// add event listener on last child
				if (i === children.length - 1) {

					// add all the vendor prefixes
					// this feels silly to me
					for (var j = 0; j < transitionEndPrefixes.length; j++) {
						children[i].addEventListener(transitionEndPrefixes[j], function(event) {
							promise(true);
						});
					}

				}

				// show child
				children[i].className = 'show';

			}
		}, 10);

		return promise;
	}

	return {
		prepare: prepare,
		type: type
	};

})();<|endoftext|>


<commit_before>import Quill from 'quill';
import ImageResize from 'quill-image-resize';
import MagicUrl from 'quill-magic-url';

Quill.register('modules/imageResize', ImageResize);
Quill.register('modules/magicUrl', MagicUrl);
const CodeBlockContainer = Quill.import('formats/code-block-container');
CodeBlockContainer.tagName = 'PRE';
Quill.register(CodeBlockContainer, true);

// table
const Table = Quill.import('formats/table-container');
const superCreate = Table.create.bind(Table);
Table.create = (value) => {
	const node = superCreate(value);
	node.classList.add('table');
	node.classList.add('table-bordered');
	return node;
};

Quill.register(Table, true);

// link without href
var Link = Quill.import('formats/link');

class MyLink extends Link {
	static create(value) {
		let node = super.create(value);
		value = this.sanitize(value);
		node.setAttribute('href', value);
		if (value.startsWith('/') || value.indexOf(window.location.host)) {
			// no href if internal link
			node.removeAttribute('target');
		}
		return node;
	}
}

Quill.register(MyLink, true);

// image uploader
const Uploader = Quill.import('modules/uploader');
Uploader.DEFAULTS.mimetypes.push('image/gif');

// inline style
const BackgroundStyle = Quill.import('attributors/style/background');
const ColorStyle = Quill.import('attributors/style/color');
const FontStyle = Quill.import('attributors/style/font');
const AlignStyle = Quill.import('attributors/style/align');
const DirectionStyle = Quill.import('attributors/style/direction');
Quill.register(BackgroundStyle, true);
Quill.register(ColorStyle, true);
Quill.register(FontStyle, true);
Quill.register(AlignStyle, true);
Quill.register(DirectionStyle, true);

// direction class
const DirectionClass = Quill.import('attributors/class/direction');
Quill.register(DirectionClass,true);

// replace font tag with span
const Inline = Quill.import('blots/inline');

class CustomColor extends Inline {
	constructor(domNode, value) {
		super(domNode, value);
		this.domNode.style.color = this.domNode.color;
		domNode.outerHTML = this.domNode.outerHTML.replace(/<font/g, '<span').replace(/<\/font>/g, '');
	}
}

CustomColor.blotName = "customColor";
CustomColor.tagName = "font";

Quill.register(CustomColor, true);

frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.form.ControlCode {
	make_wrapper() {
		super.make_wrapper();
	}

	make_input() {
		this.has_input = true;
		this.make_quill_editor();
	}

	make_quill_editor() {
		if (this.quill) return;
		this.quill_container = $('
').appendTo(this.input_area);
		this.quill = new Quill(this.quill_container[0], this.get_quill_options());
		this.bind_events();
	}

	bind_events() {
		this.quill.on('text-change', frappe.utils.debounce((delta, oldDelta, source) => {
			if (!this.is_quill_dirty(source)) return;

			const input_value = this.get_input_value();
			this.parse_validate_and_set_in_model(input_value);
		}, 300));

		$(this.quill.root).on('keydown', (e) => {
			const key = frappe.ui.keys && frappe.ui.keys.get_key(e);
			if (['ctrl+b', 'meta+b'].includes(key)) {
				e.stopPropagation();
			}
		});

		$(this.quill.root).on('drop', (e) => {
			e.stopPropagation();
		});

		// table commands
		this.$wrapper.on('click', '.ql-table .ql-picker-item', (e) => {
			const $target = $(e.currentTarget);
			const action = $target.data().value;
			e.preventDefault();

			const table = this.quill.getModule('table');
			if (action === 'insert-table') {
				table.insertTable(2, 2);
			} else if (action === 'insert-row-above') {
				table.insertRowAbove();
			} else if (action === 'insert-row-below') {
				table.insertRowBelow();
			} else if (action === 'insert-column-left') {
				table.insertColumnLeft();
			} else if (action === 'insert-column-right') {
				table.insertColumnRight();
			} else if (action === 'delete-row') {
				table.deleteRow();
			} else if (action === 'delete-column') {
				table.deleteColumn();
			} else if (action === 'delete-table') {
				table.deleteTable();
			}

			if (action !== 'delete-row') {
				table.balanceTables();
			}

			e.preventDefault();
		});
	}

	is_quill_dirty(source) {
		if (source === 'api') return false;
		let input_value = this.get_input_value();
		return this.value !== input_value;
	}

	get_quill_options() {
		return {
			modules: {
				toolbar: this.get_toolbar_options(),
				table: true,
				imageResize: {},
				magicUrl: true
			},
			theme: 'snow'
		};
	}

	get_toolbar_options() {
		return [
			[{ 'header': [1, 2, 3, false] }],
			['bold', 'italic', 'underline', 'clean'],
			[{ 'color': [] }, { 'background': [] }],
			['blockquote', 'code-block'],
			// Adding Direction tool to give the user the ability to change text direction.
			[{ 'direction': "rtl" }],
			['link', 'image'],
			[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }],
			[{ 'align': [] }],
			[{ 'indent': '-1'}, { 'indent': '+1' }],
			[{'table': [
				'insert-table',
				'insert-row-above',
				'insert-row-below',
				'insert-column-right',
				'insert-column-left',
				'delete-row',
				'delete-column',
				'delete-table',
			]}],
		];
	}

	parse(value) {
		if (value == null) {
			value = "";
		}
		return frappe.dom.remove_script_and_style(value);
	}

	set_formatted_input(value) {
		if (!this.quill) return;
		if (value === this.get_input_value()) return;
		if (!value) {
			// clear contents for falsy values like '', undefined or null
			this.quill.setText('');
			return;
		}

		// set html without triggering a focus
		const delta = this.quill.clipboard.convert({ html: value, text: '' });
		this.quill.setContents(delta);
	}

	get_input_value() {
		let value = this.quill ? this.quill.root.innerHTML : '';
		// hack to retain space sequence.
		value = value.replace(/(\s)(\s)/g, '  ');

		try {
			if (!$(value).find('.ql-editor').length) {
				value = `<div class="ql-editor read-mode">${value}</div>`;
			}
		} catch(e) {
			value = `<div class="ql-editor read-mode">${value}</div>`;
		}

		return value;
	}

	set_focus() {
		this.quill.focus();
	}
};
<commit_msg>refactor: space in code<commit_after>import Quill from 'quill';
import ImageResize from 'quill-image-resize';
import MagicUrl from 'quill-magic-url';

Quill.register('modules/imageResize', ImageResize);
Quill.register('modules/magicUrl', MagicUrl);
const CodeBlockContainer = Quill.import('formats/code-block-container');
CodeBlockContainer.tagName = 'PRE';
Quill.register(CodeBlockContainer, true);

// table
const Table = Quill.import('formats/table-container');
const superCreate = Table.create.bind(Table);
Table.create = (value) => {
	const node = superCreate(value);
	node.classList.add('table');
	node.classList.add('table-bordered');
	return node;
};

Quill.register(Table, true);

// link without href
var Link = Quill.import('formats/link');

class MyLink extends Link {
	static create(value) {
		let node = super.create(value);
		value = this.sanitize(value);
		node.setAttribute('href', value);
		if (value.startsWith('/') || value.indexOf(window.location.host)) {
			// no href if internal link
			node.removeAttribute('target');
		}
		return node;
	}
}

Quill.register(MyLink, true);

// image uploader
const Uploader = Quill.import('modules/uploader');
Uploader.DEFAULTS.mimetypes.push('image/gif');

// inline style
const BackgroundStyle = Quill.import('attributors/style/background');
const ColorStyle = Quill.import('attributors/style/color');
const FontStyle = Quill.import('attributors/style/font');
const AlignStyle = Quill.import('attributors/style/align');
const DirectionStyle = Quill.import('attributors/style/direction');
Quill.register(BackgroundStyle, true);
Quill.register(ColorStyle, true);
Quill.register(FontStyle, true);
Quill.register(AlignStyle, true);
Quill.register(DirectionStyle, true);

// direction class
const DirectionClass = Quill.import('attributors/class/direction');
Quill.register(DirectionClass, true);

// replace font tag with span
const Inline = Quill.import('blots/inline');

class CustomColor extends Inline {
	constructor(domNode, value) {
		super(domNode, value);
		this.domNode.style.color = this.domNode.color;
		domNode.outerHTML = this.domNode.outerHTML.replace(/<font/g, '<span').replace(/<\/font>/g, '');
	}
}

CustomColor.blotName = "customColor";
CustomColor.tagName = "font";

Quill.register(CustomColor, true);

frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.form.ControlCode {
	make_wrapper() {
		super.make_wrapper();
	}

	make_input() {
		this.has_input = true;
		this.make_quill_editor();
	}

	make_quill_editor() {
		if (this.quill) return;
		this.quill_container = $('
').appendTo(this.input_area);
		this.quill = new Quill(this.quill_container[0], this.get_quill_options());
		this.bind_events();
	}

	bind_events() {
		this.quill.on('text-change', frappe.utils.debounce((delta, oldDelta, source) => {
			if (!this.is_quill_dirty(source)) return;

			const input_value = this.get_input_value();
			this.parse_validate_and_set_in_model(input_value);
		}, 300));

		$(this.quill.root).on('keydown', (e) => {
			const key = frappe.ui.keys && frappe.ui.keys.get_key(e);
			if (['ctrl+b', 'meta+b'].includes(key)) {
				e.stopPropagation();
			}
		});

		$(this.quill.root).on('drop', (e) => {
			e.stopPropagation();
		});

		// table commands
		this.$wrapper.on('click', '.ql-table .ql-picker-item', (e) => {
			const $target = $(e.currentTarget);
			const action = $target.data().value;
			e.preventDefault();

			const table = this.quill.getModule('table');
			if (action === 'insert-table') {
				table.insertTable(2, 2);
			} else if (action === 'insert-row-above') {
				table.insertRowAbove();
			} else if (action === 'insert-row-below') {
				table.insertRowBelow();
			} else if (action === 'insert-column-left') {
				table.insertColumnLeft();
			} else if (action === 'insert-column-right') {
				table.insertColumnRight();
			} else if (action === 'delete-row') {
				table.deleteRow();
			} else if (action === 'delete-column') {
				table.deleteColumn();
			} else if (action === 'delete-table') {
				table.deleteTable();
			}

			if (action !== 'delete-row') {
				table.balanceTables();
			}

			e.preventDefault();
		});
	}

	is_quill_dirty(source) {
		if (source === 'api') return false;
		let input_value = this.get_input_value();
		return this.value !== input_value;
	}

	get_quill_options() {
		return {
			modules: {
				toolbar: this.get_toolbar_options(),
				table: true,
				imageResize: {},
				magicUrl: true
			},
			theme: 'snow'
		};
	}

	get_toolbar_options() {
		return [
			[{ 'header': [1, 2, 3, false] }],
			['bold', 'italic', 'underline', 'clean'],
			[{ 'color': [] }, { 'background': [] }],
			['blockquote', 'code-block'],
			// Adding Direction tool to give the user the ability to change text direction.
			[{ 'direction': "rtl" }],
			['link', 'image'],
			[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }],
			[{ 'align': [] }],
			[{ 'indent': '-1'}, { 'indent': '+1' }],
			[{'table': [
				'insert-table',
				'insert-row-above',
				'insert-row-below',
				'insert-column-right',
				'insert-column-left',
				'delete-row',
				'delete-column',
				'delete-table',
			]}],
		];
	}

	parse(value) {
		if (value == null) {
			value = "";
		}
		return frappe.dom.remove_script_and_style(value);
	}

	set_formatted_input(value) {
		if (!this.quill) return;
		if (value === this.get_input_value()) return;
		if (!value) {
			// clear contents for falsy values like '', undefined or null
			this.quill.setText('');
			return;
		}

		// set html without triggering a focus
		const delta = this.quill.clipboard.convert({ html: value, text: '' });
		this.quill.setContents(delta);
	}

	get_input_value() {
		let value = this.quill ? this.quill.root.innerHTML : '';
		// hack to retain space sequence.
		value = value.replace(/(\s)(\s)/g, '  ');

		try {
			if (!$(value).find('.ql-editor').length) {
				value = `<div class="ql-editor read-mode">${value}</div>`;
			}
		} catch(e) {
			value = `<div class="ql-editor read-mode">${value}</div>`;
		}

		return value;
	}

	set_focus() {
		this.quill.focus();
	}
};
<|endoftext|>