<commit_before>idrinth.core = {
  /**
     *
     * @param {string} str
     * @returns {string}
     */
  escapeRegExp: function(str) {
    return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
  },
  /**
     *
     * @param {object} parent
     * @param {string} field
     * @param {Boolean} allowObjects
     * @returns {Boolean}
     */
  fieldIsSetting: function(parent, field, allowObjects) {
    return (
      parent &&
      typeof parent === "object" &&
      field &&
      parent.hasOwnProperty(field) &&
      (parent[field] === null ||
        typeof parent[field] !== "object" ||
        allowObjects) &&
      typeof parent[field] !== "function"
    );
  },
  /**
     *
     * @type {object}
     */
  ajax: {
    /**
         *
         * @type {object}
         */
    active: {},
    /**
         *
         * @param {string} url
         * @param {function} success
         * @param {function} failure
         * @param {function} timeout
         * @param {string} additionalHeader
         * @param {Boolean} [false] isStatic
         * @returns {undefined}
         */
    runHome: function(
      url,
      success,
      failure,
      timeout,
      additionalHeader,
      isStatic
    ) {
      var server = isStatic
        ? "static"
        : (idrinth.settings.get("isWorldServer") ? "world-" : "") +
            idrinth.platform;
      var homeUrl =
        "https://dotd.idrinth.de/" + server + ("/" + url).replace(/\/\//, "/");
      idrinth.core.ajax.run(
        homeUrl,
        success,
        failure,
        timeout,
        additionalHeader
      );
    },
    /**
         *
         * @param {string} url
         * @param {function} success
         * @param {function} failure
         * @param {function} timeout
         * @param {string} additionalHeader
         * @param {Boolean} [false] isStatic
         * @returns {undefined}
         */
    run: function(url, success, failure, timeout, additionalHeader) {
      if(!idrinth.start && !idrinth.settings.get("server")) {
          return;
      }
      var requestHandler = new XMLHttpRequest();
      requestHandler.onreadystatechange = function(event) {
        var request = (event || window.event).target;
        var call = function(func, value) {
          if (typeof func !== "function") {
            return;
          }
          try {
            return func(value);
          } catch (e) {
            idrinth.core.log(
              typeof e.getMessage === "function"
                ? e.getMessage()
                : e.message ? e.message : e
            );
            return null;
          }
        };
        if (request.readyState === 4) {
          var status =
            (request.status > 199 && request.status < 300) ||
            request.status === 0;
          call(
            status ? success : failure,
            status ? request.responseText : request
          );
          delete idrinth.core.ajax.active[request._url];
        }
      };
      requestHandler.timeout = 30000;
      requestHandler.ontimeout = function(event) {
        var request = (event || window.event).target;
        if (typeof timeout === "function") {
          timeout.bind(request);
        }
        delete idrinth.core.ajax.active[request._url];
        idrinth.core.log("Request to " + request._url + " failed.");
      };
      var error = function(event) {
        delete idrinth.core.ajax.active[(event || window.event).target._url];
        idrinth.core.log(
          "Request to " + (event || window.event).target._url + " failed."
        );
      };
      requestHandler.onerror = error;
      requestHandler.onabort = error;
      requestHandler._url = url;
      requestHandler.open("GET", url, true);
      if (url.match("/dotd\.idrinth\.de/")) {
        if (additionalHeader) {
          requestHandler.setRequestHeader("Idrinth-Addition", additionalHeader);
        }
        requestHandler.withCredentials = true;
      }
      idrinth.core.ajax.active[url] = requestHandler;
      idrinth.core.ajax.active[url].send();
    }
  },
  /**
     *
     * @type {object}
     */
  copyToClipboard: {
    /**
         *
         * @param {string} text
         * @returns {Boolean}
         */
    text: function(text) {
      var success;
      try {
        var textAreaElement = idrinth.ui.buildElement({
          type: "textarea",
          id: "idrinth-copy-helper"
        });
        textAreaElement.value = text;
        idrinth.ui.base.appendChild(textAreaElement);
        textAreaElement.select();
        success = document.execCommand("copy");
      } catch (exception) {
        idrinth.core.log(exception.getMessage());
        success = false;
      }
      idrinth.ui.removeElement("idrinth-copy-helper");
      return success;
    },
    /**
         *
         * @param {HTMLElement} element
         * @returns {string}
         */
    element: function(element) {
      if (element.hasAttribute("data-clipboard-text")) {
        return idrinth.core.copyToClipboard.text(
          element.getAttribute("data-clipboard-text")
        );
      }
      if (element.value) {
        return idrinth.core.copyToClipboard.text(element.value);
      }
      return idrinth.core.copyToClipboard.text(element.innerHTML);
    }
  },
  /**
     *
     * @param {string} title
     * @param {string|HTMLElement} content
     * @returns {Boolean|window.Notification}
     */
  sendNotification: function(title, content) {
    if (!("Notification" in window)) {
      return false;
    }
    if (window.Notification.permission === "default") {
      window.Notification.requestPermission();
    }
    if (window.Notification.permission === "denied") {
      return false;
    }
    var data = {};
    if (idrinth.settings.get("notification#image")) {
      data.icon = "https://dotd.idrinth.de/Resources/Images/logo.png";
    }
    if (idrinth.settings.get("notification#content")) {
      data.body = content;
    }
    return new window.Notification(title, data);
  },
  /**
     *
     * @type {object}
     */
  timeouts: {
    /**
         *
         * @type {object}
         */
    next: null,
    /**
         *
         * @type {object}
         */
    list: {},
    /**
         * makes sure the timeout is removed when called next
         * @param {string} identifier
         * @returns {undefined}
         */
    remove: function(identifier) {
      if (idrinth.core.timeouts.list[identifier] !== undefined) {
        idrinth.core.timeouts.list[identifier].repeats = 1;
        idrinth.core.timeouts.list[identifier].func = function() {};
      }
    },
    /**
         *
         * @param {string} identifier
         * @param {function} func
         * @param {int} time in milliseconds
         * @param {Number} [1] maxRepeats -1 will repeat until the end of time
         * @returns {undefined}
         */
    add: function(identifier, func, time, maxRepeats) {
      var date = new Date();
      idrinth.core.timeouts.list[identifier] = {
        func: func,
        next: date.getTime() + date.getMilliseconds() / 1000 + time / 1000,
        duration: time,
        repeats: maxRepeats ? maxRepeats : 1
      };
      if (!idrinth.core.timeouts.next) {
        idrinth.core.timeouts.next = window.setTimeout(
          idrinth.core.timeouts.process,
          1000
        );
      }
    },
    /**
         * activates all relevant timeouts and intervals
         * @returns {undefined}
         */
    process: function() {
      var date = new Date().getTime() + new Date().getMilliseconds() / 1000;
      var min = 10;
      /**
             *
             * @param {Number} min
             * @param {Number} property
             * @returns {Number}
             */
      var check = function(min, property, date) {
        /**
                 *
                 * @param {Number} durationLeft
                 * @param {string} minDuration
                 * @returns {Number}
                 */
        var getVal = function(durationLeft, minDuration) {
          if (durationLeft < 0.1) {
            return 0.1;
          }
          return durationLeft < minDuration ? durationLeft : minDuration;
        };
        /**
                 *
                 * @param {string} property
                 * @returns {undefined}
                 */
        var handle = function(property, min) {
          idrinth.core.timeouts.list[property].func();
          idrinth.core.timeouts.list[property].repeats = Math.max(
            -1,
            idrinth.core.timeouts.list[property].repeats - 1
          );
          if (idrinth.core.timeouts.list[property].repeats) {
            min = getVal(idrinth.core.timeouts.list[property].duration, min);
            idrinth.core.timeouts.list[property].next =
              date + idrinth.core.timeouts.list[property].duration / 1000;
          } else {
            delete idrinth.core.timeouts.list[property];
          }
          return min;
        };
        if (date < idrinth.core.timeouts.list[property].next) {
          return getVal(idrinth.core.timeouts.list[property].next - date, min);
        }
        try {
          return handle(property, min);
        } catch (e) {
          idrinth.core.log(e.message ? e.message : e.getMessage());
          return min;
        }
      };
      for (var property in idrinth.core.timeouts.list) {
        if (idrinth.core.timeouts.list.hasOwnProperty(property)) {
          min = check(min, property, date);
        }
      }
      if (Object.keys(idrinth.core.timeouts.list).length) {
        idrinth.core.timeouts.next = window.setTimeout(
          idrinth.core.timeouts.process,
          Math.ceil(min * 1000)
        );
      }
    }
  },
  /**
     *
     * @param {string} string
     * @returns {undefined}
     */
  log: function(string) {
    console.log("[IDotDS] " + string);
  },
  /**
     *
     * @param {string} text
     * @returns {undefined}
     */
  alert: function(text) {
    if (!idrinth.ui.base) {
      window.alert(text);
      return;
    }
    idrinth.ui.buildModal("Info", text);
  },
  /**
     *
     * @param {string} text
     * @param {function} callback
     * @returns {undefined}
     */
  confirm: function(text, callback) {
    idrinth.ui.buildModal("Do you?", text, callback);
  },
  /**
     * runs the inWorker function in the worker and let's the resultHandler handle the result
     * @param {Function} inWorker
     * @param {Function} resultHandler
     * @param {object} values
     * @returns {undefined}
     */
  addWorker: function(inWorker, resultHandler, values) {
    if (!window.Worker) {
      return resultHandler(inWorker(values));
    }
    var blobURL = window.URL.createObjectURL(
      new Blob([
        "self.onmessage = function(message) {var work=" +
          inWorker.toString() +
          ";self.postMessage(work(message.data));self.close();}"
      ])
    );
    var worker = new Worker(blobURL);
    worker.onmessage = function(message) {
      message.target.resultHandler(message.data);
    };
    worker.resultHandler = resultHandler;
    worker.postMessage(values);
    window.URL.revokeObjectURL(blobURL);
  },
  /**
     *
     * @type {object}
     */
  multibind: {
    /**
         *
         * @type {object}
         */
    events: {},
    /**
         *
         * @param {string} event
         * @param {string} selector
         * @param {function} method
         * @returns {undefined}
         */
    add: function(event, selector, method) {
      var bind = function(event, selector, method) {
        idrinth.core.multibind.events[event] = idrinth.core.multibind.events[
          event
        ]
          ? idrinth.core.multibind.events[event]
          : {};
        idrinth.core.multibind.events[event][selector] = idrinth.core.multibind
          .events[event][selector]
          ? idrinth.core.multibind.events[event][selector]
          : [];
        idrinth.core.multibind.events[event][selector].push(method);
      };
      if (!idrinth.core.multibind.events[event]) {
        document
          .getElementsByTagName("body")[0]
          .addEventListener(event, idrinth.core.multibind.triggered);
      }
      bind(event, selector, method);
    },
    /**
         *
         * @param {Event} [window.event] event
         * @returns {undefined}
         */
    triggered: function(event) {
      /**
             *
             * @param {HTMLElement} el
             * @param {string} event
             * @param {string} selector
             * @returns {undefined}
             */
      var handleElement = function(el, event, selector) {
        if (!el) {
          return;
        }
        for (
          var pos = 0;
          pos < idrinth.core.multibind.events[event][selector].length;
          pos++
        ) {
          try {
            idrinth.core.multibind.events[event][selector][pos](el, event);
          } catch (exception) {
            idrinth.core.log(
              exception.message ? exception.message : exception.getMessage()
            );
          }
        }
      };
      event = event || window.event;
      if (idrinth.core.multibind.events[event.type]) {
        for (var selector in idrinth.core.multibind.events[event.type]) {
          if (
            idrinth.core.multibind.events[event.type].hasOwnProperty(selector)
          ) {
            handleElement(
              idrinth.ui.matchesCss(event.target, selector),
              event.type,
              selector
            );
          }
        }
      }
    }
  }
};
<commit_msg>fixes #311 add key to be replaced(server side change pending)<commit_after>idrinth.core = {
  /**
     *
     * @param {string} str
     * @returns {string}
     */
  escapeRegExp: function(str) {
    return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
  },
  /**
     *
     * @param {object} parent
     * @param {string} field
     * @param {Boolean} allowObjects
     * @returns {Boolean}
     */
  fieldIsSetting: function(parent, field, allowObjects) {
    return (
      parent &&
      typeof parent === "object" &&
      field &&
      parent.hasOwnProperty(field) &&
      (parent[field] === null ||
        typeof parent[field] !== "object" ||
        allowObjects) &&
      typeof parent[field] !== "function"
    );
  },
  /**
     *
     * @type {object}
     */
  ajax: {
    /**
         *
         * @type {object}
         */
    active: {},
    /**
         *
         * @param {string} url
         * @param {function} success
         * @param {function} failure
         * @param {function} timeout
         * @param {string} additionalHeader
         * @param {Boolean} [false] isStatic
         * @returns {undefined}
         */
    runHome: function(
      url,
      success,
      failure,
      timeout,
      additionalHeader,
      isStatic
    ) {
      var server = isStatic
        ? "static"
        : (idrinth.settings.get("isWorldServer") ? "world-" : "") +
            idrinth.platform;
      var homeUrl =
        "https://dotd.idrinth.de/" + server + ("/" + url).replace(/\/\//, "/");
      idrinth.core.ajax.run(
        homeUrl,
        success,
        failure,
        timeout,
        additionalHeader
      );
    },
    /**
         *
         * @param {string} url
         * @param {function} success
         * @param {function} failure
         * @param {function} timeout
         * @param {string} additionalHeader
         * @param {Boolean} [false] isStatic
         * @returns {undefined}
         */
    run: function(url, success, failure, timeout, additionalHeader) {
      if(!idrinth.start && !idrinth.settings.get("server")) {
          return;
      }
      var requestHandler = new XMLHttpRequest();
      requestHandler.onreadystatechange = function(event) {
        var request = (event || window.event).target;
        var call = function(func, value) {
          if (typeof func !== "function") {
            return;
          }
          try {
            return func(value);
          } catch (e) {
            idrinth.core.log(
              typeof e.getMessage === "function"
                ? e.getMessage()
                : e.message ? e.message : e
            );
            return null;
          }
        };
        if (request.readyState === 4) {
          var status =
            (request.status > 199 && request.status < 300) ||
            request.status === 0;
          call(
            status ? success : failure,
            status ? request.responseText : request
          );
          delete idrinth.core.ajax.active[request._url];
        }
      };
      requestHandler.timeout = 30000;
      requestHandler.ontimeout = function(event) {
        var request = (event || window.event).target;
        if (typeof timeout === "function") {
          timeout.bind(request);
        }
        delete idrinth.core.ajax.active[request._url];
        idrinth.core.log("Request to " + request._url + " failed.");
      };
      var error = function(event) {
        delete idrinth.core.ajax.active[(event || window.event).target._url];
        idrinth.core.log(
          "Request to " + (event || window.event).target._url + " failed."
        );
      };
      requestHandler.onerror = error;
      requestHandler.onabort = error;
      requestHandler._url = url;
      requestHandler.open("GET", url, true);
      if (url.match("/dotd\.idrinth\.de/")) {
        if (additionalHeader) {
          requestHandler.setRequestHeader("Idrinth-Addition", additionalHeader);
        }
        requestHandler.withCredentials = true;
      }
      idrinth.core.ajax.active[url] = requestHandler;
      idrinth.core.ajax.active[url].send();
    }
  },
  /**
     *
     * @type {object}
     */
  copyToClipboard: {
    /**
         *
         * @param {string} text
         * @returns {Boolean}
         */
    text: function(text) {
      var success;
      try {
        var textAreaElement = idrinth.ui.buildElement({
          type: "textarea",
          id: "idrinth-copy-helper"
        });
        textAreaElement.value = text;
        idrinth.ui.base.appendChild(textAreaElement);
        textAreaElement.select();
        success = document.execCommand("copy");
      } catch (exception) {
        idrinth.core.log(exception.getMessage());
        success = false;
      }
      idrinth.ui.removeElement("idrinth-copy-helper");
      return success;
    },
    /**
         *
         * @param {HTMLElement} element
         * @returns {string}
         */
    element: function(element) {
      if (element.hasAttribute("data-clipboard-text")) {
        return idrinth.core.copyToClipboard.text(
          element.getAttribute("data-clipboard-text")
        );
      }
      if (element.value) {
        return idrinth.core.copyToClipboard.text(element.value);
      }
      return idrinth.core.copyToClipboard.text(element.innerHTML);
    }
  },
  /**
     *
     * @param {string} title
     * @param {string|HTMLElement} content
     * @returns {Boolean|window.Notification}
     */
  sendNotification: function(title, content) {
    if (!("Notification" in window)) {
      return false;
    }
    if (window.Notification.permission === "default") {
      window.Notification.requestPermission();
    }
    if (window.Notification.permission === "denied") {
      return false;
    }
    var data = {};
    if (idrinth.settings.get("notification#image")) {
      data.icon = "https://dotd.idrinth.de/Resources/Images/logo.png";
    }
    if (idrinth.settings.get("notification#content")) {
      data.body = content;
    }
    return new window.Notification(title, data);
  },
  /**
     *
     * @type {object}
     */
  timeouts: {
    /**
         *
         * @type {object}
         */
    next: null,
    /**
         *
         * @type {object}
         */
    list: {},
    /**
         * makes sure the timeout is removed when called next
         * @param {string} identifier
         * @returns {undefined}
         */
    remove: function(identifier) {
      if (idrinth.core.timeouts.list[identifier] !== undefined) {
        idrinth.core.timeouts.list[identifier].repeats = 1;
        idrinth.core.timeouts.list[identifier].func = function() {};
      }
    },
    /**
         *
         * @param {string} identifier
         * @param {function} func
         * @param {int} time in milliseconds
         * @param {Number} [1] maxRepeats -1 will repeat until the end of time
         * @returns {undefined}
         */
    add: function(identifier, func, time, maxRepeats) {
      var date = new Date();
      idrinth.core.timeouts.list[identifier] = {
        func: func,
        next: date.getTime() + date.getMilliseconds() / 1000 + time / 1000,
        duration: time,
        repeats: maxRepeats ? maxRepeats : 1
      };
      if (!idrinth.core.timeouts.next) {
        idrinth.core.timeouts.next = window.setTimeout(
          idrinth.core.timeouts.process,
          1000
        );
      }
    },
    /**
         * activates all relevant timeouts and intervals
         * @returns {undefined}
         */
    process: function() {
      var date = new Date().getTime() + new Date().getMilliseconds() / 1000;
      var min = 10;
      /**
             *
             * @param {Number} min
             * @param {Number} property
             * @returns {Number}
             */
      var check = function(min, property, date) {
        /**
                 *
                 * @param {Number} durationLeft
                 * @param {string} minDuration
                 * @returns {Number}
                 */
        var getVal = function(durationLeft, minDuration) {
          if (durationLeft < 0.1) {
            return 0.1;
          }
          return durationLeft < minDuration ? durationLeft : minDuration;
        };
        /**
                 *
                 * @param {string} property
                 * @returns {undefined}
                 */
        var handle = function(property, min) {
          idrinth.core.timeouts.list[property].func();
          idrinth.core.timeouts.list[property].repeats = Math.max(
            -1,
            idrinth.core.timeouts.list[property].repeats - 1
          );
          if (idrinth.core.timeouts.list[property].repeats) {
            min = getVal(idrinth.core.timeouts.list[property].duration, min);
            idrinth.core.timeouts.list[property].next =
              date + idrinth.core.timeouts.list[property].duration / 1000;
          } else {
            delete idrinth.core.timeouts.list[property];
          }
          return min;
        };
        if (date < idrinth.core.timeouts.list[property].next) {
          return getVal(idrinth.core.timeouts.list[property].next - date, min);
        }
        try {
          return handle(property, min);
        } catch (e) {
          idrinth.core.log(e.message ? e.message : e.getMessage());
          return min;
        }
      };
      for (var property in idrinth.core.timeouts.list) {
        if (idrinth.core.timeouts.list.hasOwnProperty(property)) {
          min = check(min, property, date);
        }
      }
      if (Object.keys(idrinth.core.timeouts.list).length) {
        idrinth.core.timeouts.next = window.setTimeout(
          idrinth.core.timeouts.process,
          Math.ceil(min * 1000)
        );
      }
    }
  },
  /**
     *
     * @param {string} string
     * @returns {undefined}
     */
  log: function(string) {
    console.log("[IDotDS] " + string);
  },
  /**
     *
     * @param {string} text
     * @returns {undefined}
     */
  alert: function(text) {
    if (!idrinth.ui.base) {
      window.alert(text);
      return;
    }
    idrinth.ui.buildModal("Info", text);
  },
  /**
     *
     * @param {string} text
     * @param {function} callback
     * @returns {undefined}
     */
  confirm: function(text, callback) {
    idrinth.ui.buildModal("Do you?", text, callback);
  },
  /**
     * runs the inWorker function in the worker and let's the resultHandler handle the result
     * @param {Function} inWorker
     * @param {Function} resultHandler
     * @param {object} values
     * @returns {undefined}
     */
  addWorker: function(inWorker, resultHandler, values) {
    if (!window.Worker) {
      return resultHandler(inWorker(values));
    }
    var blobURL = window.URL.createObjectURL(
      new Blob([
        "/*js:big*/" +//replaced server-side from libs/[name].js
        "self.onmessage = function(message) {var work=" +
          inWorker.toString() +
          ";self.postMessage(work(message.data));self.close();}"
      ])
    );
    var worker = new Worker(blobURL);
    worker.onmessage = function(message) {
      message.target.resultHandler(message.data);
    };
    worker.resultHandler = resultHandler;
    worker.postMessage(values);
    window.URL.revokeObjectURL(blobURL);
  },
  /**
     *
     * @type {object}
     */
  multibind: {
    /**
         *
         * @type {object}
         */
    events: {},
    /**
         *
         * @param {string} event
         * @param {string} selector
         * @param {function} method
         * @returns {undefined}
         */
    add: function(event, selector, method) {
      var bind = function(event, selector, method) {
        idrinth.core.multibind.events[event] = idrinth.core.multibind.events[
          event
        ]
          ? idrinth.core.multibind.events[event]
          : {};
        idrinth.core.multibind.events[event][selector] = idrinth.core.multibind
          .events[event][selector]
          ? idrinth.core.multibind.events[event][selector]
          : [];
        idrinth.core.multibind.events[event][selector].push(method);
      };
      if (!idrinth.core.multibind.events[event]) {
        document
          .getElementsByTagName("body")[0]
          .addEventListener(event, idrinth.core.multibind.triggered);
      }
      bind(event, selector, method);
    },
    /**
         *
         * @param {Event} [window.event] event
         * @returns {undefined}
         */
    triggered: function(event) {
      /**
             *
             * @param {HTMLElement} el
             * @param {string} event
             * @param {string} selector
             * @returns {undefined}
             */
      var handleElement = function(el, event, selector) {
        if (!el) {
          return;
        }
        for (
          var pos = 0;
          pos < idrinth.core.multibind.events[event][selector].length;
          pos++
        ) {
          try {
            idrinth.core.multibind.events[event][selector][pos](el, event);
          } catch (exception) {
            idrinth.core.log(
              exception.message ? exception.message : exception.getMessage()
            );
          }
        }
      };
      event = event || window.event;
      if (idrinth.core.multibind.events[event.type]) {
        for (var selector in idrinth.core.multibind.events[event.type]) {
          if (
            idrinth.core.multibind.events[event.type].hasOwnProperty(selector)
          ) {
            handleElement(
              idrinth.ui.matchesCss(event.target, selector),
              event.type,
              selector
            );
          }
        }
      }
    }
  }
};
<|endoftext|>


<commit_before>/* -*- mode: espresso; espresso-indent-level: 4; indent-tabs-mode: nil -*- */
/* vim: set softtabstop=4 shiftwidth=4 tabstop=4 expandtab: */

var ReviewSystem = new function()
{
    var projectID, skeletonID;
    var self = this;
    self.skeleton_segments = null;
    self.current_segment = null;
    self.current_segment_index = 0;

    this.init = function() {
        projectID = project.id;
    };

    this.validSegment = function() {
        if(self.current_segment !== null) {
            return true;
        } else {
            return false;
        }
    };

    this.endReview = function() {
        self.skeleton_segments = null;
        self.current_segment = null;
        self.current_segment_index = 0;
        if( $('#review_segment_table').length > 0 )
            $('#review_segment_table').remove();
            $('#reviewing_skeleton').text( '' );
    };

    this.initReviewSegment = function( id ) {
        self.current_segment = self.skeleton_segments[id];
        self.current_segment_index = 0;
        self.goToNodeIndexOfSegmentSequence( 0 );
    };

    this.goToNodeIndexOfSegmentSequence = function( idx ) {
        if (self.skeleton_segments===null)
            return;
        var node = self.current_segment['sequence'][idx];
        project.moveTo(node.z, node.y, node.x, undefined,
         function () {
            SkeletonAnnotations.staticSelectNode( node.id, skeletonID );
         });
    };

    this.moveNodeInSegmentBackward = function() {
        if (self.skeleton_segments===null)
            return;
        if( self.current_segment_index == 0 ) {
            self.markAsReviewed( self.current_segment['sequence'][self.current_segment_index], self.startSkeletonToReview );
            return;
        }
        self.markAsReviewed( self.current_segment['sequence'][self.current_segment_index] );
        self.current_segment_index--;
        self.goToNodeIndexOfSegmentSequence( self.current_segment_index );
    };

    this.moveNodeInSegmentForward = function(evt) {
        if (self.skeleton_segments===null)
            return;
        if( self.current_segment_index === self.current_segment['sequence'].length - 1  ) {
            self.markAsReviewed( self.current_segment['sequence'][self.current_segment_index], self.startSkeletonToReview );
            return;
        }

        self.markAsReviewed( self.current_segment['sequence'][self.current_segment_index] );
        self.current_segment_index++;

        if (evt.shiftKey) {
            // Advance current_segment_index to the first node that is not reviewed
            // which is a node with rid (reviewer id) of -1.
            var i = self.current_segment_index;
            var seq = self.current_segment['sequence'];
            var len = seq.length;
            while (i < len) {
                if (-1 === seq[i].rid) {
                    self.current_segment_index = i;
                    break;
                }
                i += 1;
            }
        }

        if (self.current_segment_index < self.current_segment['sequence'].length -1) {
            // Check if the remainder of the segment was complete at an earlier time
            // and perhaps now the whole segment is done:
            var i = self.current_segment_index;
            var seq = self.current_segment['sequence'];
            var len = seq.length;
            while (i < len && -1 !== seq[i].rid) {
                i += 1;
            }
            if (i === len) {
                growlAlert('DONE', 'Segment fully reviewed: ' + self.current_segment['nr_nodes'] + ' nodes');
                var cell = $('#rev-status-cell-' + self.current_segment['id']);
                cell.text('100.00%');
                cell.css('background-color', '#6fff5c');
                // Don't startSkeletonToReview, because self.current_segment_index
                // would be lost, losing state for q/w navigation.
            }
        }

        self.goToNodeIndexOfSegmentSequence( self.current_segment_index );
    };

    this.markAsReviewed = function( node_ob, funct ) {
        requestQueue.register(
            "dj/"+projectID+"/node/" + node_ob['id'] + "/reviewed",
            "POST",
            {},
            function (status, text) {
                var json = $.parseJSON(text);
                if (json.error) {
                    alert( json.error );
                } else {
                    // Mark locally as reviewed
                    node_ob['rid'] = json.reviewer_id;
                    // Execute continuation if any
                    if( funct )
                        funct();
                }
            });
    };

    /** Clears the #review_segment_table prior to adding rows to it. */
    this.createReviewSkeletonTable = function( skeleton_data ) {
        self.skeleton_segments = skeleton_data;
        var butt, table, tbody, row;
        if( $('#review_segment_table').length > 0 ) {
            $('#review_segment_table').remove();
        }
        $('#reviewing_skeleton').text( 'Skeleton ID under review: ' + skeletonID );
        table = $('
').attr('cellpadding', '3').attr('cellspacing', '0').attr('id', 'review_segment_table').attr('border', '0');
        // create header
        thead = $('
');
        table.append( thead );
        row = $('
')
        row.append( $('').text("") );
        row.append( $('').text("Status") );
        row.append( $('').text("# nodes") );
        thead.append( row );
        tbody = $('
');
        table.append( tbody );
        // create a row
        for(var e in skeleton_data ) {
            row = $('
');
            row.append( $('').text( skeleton_data[e]['id'] ) );
            var status = $('').text( skeleton_data[e]['status']+'%' );
            row.append( status );
            row.append( $('').text( skeleton_data[e]['nr_nodes'] ) );
            if( parseInt( skeleton_data[e]['status']) === 0 ) {
                status.css('background-color', '#ff8c8c');
            } else if( parseInt( skeleton_data[e]['status']) === 100 ) {
                status.css('background-color', '#6fff5c');
            } else {
                status.css('background-color', '#ffc71d');
            }
            butt = $('<button />').text( "Review" );
            butt.attr( 'id', 'reviewbutton_'+skeleton_data[e]['id'] );
            butt.click( function() {
                self.initReviewSegment( this.id.replace("reviewbutton_", "") );
            });
            row.append( butt );
            tbody.append( row );
        }
        // empty row
        row = $('
');
        tbody.append( row );
        table.append( $('

') );
        $("#project_review_widget").append( table );

    };

    var checkSkeletonID = function() {
        if (!skeletonID) {
            $('#growl-alert').growlAlert({
                autoShow: true,
                content: 'You need to activate a skeleton to review.',
                title: 'BEWARE',
                position: 'top-right',
                delayTime: 2500,
                onComplete: function() { g.remove(); }
            });
            return false;
        }
        return true;
    };

    this.startSkeletonToReview = function( ) {
        skeletonID = SkeletonAnnotations.getActiveSkeletonId();
        if (!checkSkeletonID()) {
            return;
        }
        requestQueue.replace(
            "dj/"+projectID+"/skeleton/" + skeletonID + "/review",
            "POST",
            {},
            function (status, text) {
                if (200 !== status) { return; }
                var skeleton_data = $.parseJSON(text);
                if (skeleton_data.error) {
                    if ("REPLACED" === skeleton_data.error) { return; }
                    alert( skeleton_data.error );
                } else {
                    self.createReviewSkeletonTable( skeleton_data );
                }
            },
            "start_review_skeleton");
    };

    var resetFn = function(fnName) {
        if (!checkSkeletonID()) {
            return;
        }
        if (!confirm("Are you sure you want to alter the review state of skeleton #" + skeletonID + " with '" + fnName + "' ?")) {
            return;
        }
        requestQueue.replace(
            "dj/"+projectID+"/skeleton/" + skeletonID + "/review/" + fnName,
            "POST",
            {},
            function (status, text) {
                if (200 !== status) { return; }
                var json = $.parseJSON(text);
                if (json.error) {
                    if ("REPLACED" === json.error) { return; }
                    alert(json.error);
                    return;
                }
                self.startSkeletonToReview();
            },
            "review_" + fnName);
    };

    this.resetAllRevisions = function() {
        resetFn("reset-all");
    };

    this.resetOwnRevisions = function() {
        resetFn("reset-own");
    };

    this.resetRevisionsByOthers = function() {
        resetFn("reset-others");
    };
};
<commit_msg>Review widget now knows who reviewed how many nodes and shows it.<commit_after>/* -*- mode: espresso; espresso-indent-level: 4; indent-tabs-mode: nil -*- */
/* vim: set softtabstop=4 shiftwidth=4 tabstop=4 expandtab: */

var ReviewSystem = new function()
{
    var projectID, skeletonID;
    var self = this;
    self.skeleton_segments = null;
    self.current_segment = null;
    self.current_segment_index = 0;

    this.init = function() {
        projectID = project.id;
    };

    this.validSegment = function() {
        if(self.current_segment !== null) {
            return true;
        } else {
            return false;
        }
    };

    this.endReview = function() {
        self.skeleton_segments = null;
        self.current_segment = null;
        self.current_segment_index = 0;
        if( $('#review_segment_table').length > 0 )
            $('#review_segment_table').remove();
            $('#reviewing_skeleton').text( '' );
    };

    this.initReviewSegment = function( id ) {
        self.current_segment = self.skeleton_segments[id];
        self.current_segment_index = 0;
        self.goToNodeIndexOfSegmentSequence( 0 );
    };

    this.goToNodeIndexOfSegmentSequence = function( idx ) {
        if (self.skeleton_segments===null)
            return;
        var node = self.current_segment['sequence'][idx];
        project.moveTo(node.z, node.y, node.x, undefined,
         function () {
            SkeletonAnnotations.staticSelectNode( node.id, skeletonID );
         });
    };

    this.moveNodeInSegmentBackward = function() {
        if (self.skeleton_segments===null)
            return;
        if( self.current_segment_index == 0 ) {
            self.markAsReviewed( self.current_segment['sequence'][self.current_segment_index], self.startSkeletonToReview );
            return;
        }
        self.markAsReviewed( self.current_segment['sequence'][self.current_segment_index] );
        self.current_segment_index--;
        self.goToNodeIndexOfSegmentSequence( self.current_segment_index );
    };

    this.moveNodeInSegmentForward = function(evt) {
        if (self.skeleton_segments===null)
            return;
        if( self.current_segment_index === self.current_segment['sequence'].length - 1  ) {
            self.markAsReviewed( self.current_segment['sequence'][self.current_segment_index], self.startSkeletonToReview );
            return;
        }

        self.markAsReviewed( self.current_segment['sequence'][self.current_segment_index] );
        self.current_segment_index++;

        if (evt.shiftKey) {
            // Advance current_segment_index to the first node that is not reviewed
            // which is a node with rid (reviewer id) of -1.
            var i = self.current_segment_index;
            var seq = self.current_segment['sequence'];
            var len = seq.length;
            while (i < len) {
                if (-1 === seq[i].rid) {
                    self.current_segment_index = i;
                    break;
                }
                i += 1;
            }
        }

        if (self.current_segment_index < self.current_segment['sequence'].length -1) {
            // Check if the remainder of the segment was complete at an earlier time
            // and perhaps now the whole segment is done:
            var i = self.current_segment_index;
            var seq = self.current_segment['sequence'];
            var len = seq.length;
            while (i < len && -1 !== seq[i].rid) {
                i += 1;
            }
            if (i === len) {
                growlAlert('DONE', 'Segment fully reviewed: ' + self.current_segment['nr_nodes'] + ' nodes');
                var cell = $('#rev-status-cell-' + self.current_segment['id']);
                cell.text('100.00%');
                cell.css('background-color', '#6fff5c');
                // Don't startSkeletonToReview, because self.current_segment_index
                // would be lost, losing state for q/w navigation.
            }
        }

        self.goToNodeIndexOfSegmentSequence( self.current_segment_index );
    };

    this.markAsReviewed = function( node_ob, funct ) {
        requestQueue.register(
            "dj/"+projectID+"/node/" + node_ob['id'] + "/reviewed",
            "POST",
            {},
            function (status, text) {
                var json = $.parseJSON(text);
                if (json.error) {
                    alert( json.error );
                } else {
                    // Mark locally as reviewed
                    node_ob['rid'] = json.reviewer_id;
                    // Execute continuation if any
                    if( funct )
                        funct();
                }
            });
    };

    /** Clears the #review_segment_table prior to adding rows to it. */
    this.createReviewSkeletonTable = function( skeleton_data, users ) {
        self.skeleton_segments = skeleton_data;
        var butt, table, tbody, row;
        if( $('#review_segment_table').length > 0 ) {
            $('#review_segment_table').remove();
        }

        // Count which user reviewed how many nodes
        // Map of user ID vs object containing name and count:
        var users = users.reduce(function(map, u) {
            map[u[0]] = {name: u[1], count: 0};
            return map;
        }, {});
        users[-1] = {name: 'unreviewed', count: 0};
        // Fill in the users count:
        skeleton_data.forEach(function(segment) {
            segment['sequence'].forEach(function(node) {
                users[node['rid']].count += 1;
            });
        }, skeleton_data);
        // Create string with user's reviewed counts:
        var user_revisions = Object.keys(users).reduce(function(s, u) {
            u = users[u];
            if (u.count > 0) { s += u.name + ": " + u.count + "; "; }
            return s;
        }, "");

        $('#reviewing_skeleton').text( 'Skeleton ID under review: ' + skeletonID + " -- " + user_revisions );
        table = $('
').attr('cellpadding', '3').attr('cellspacing', '0').attr('id', 'review_segment_table').attr('border', '0');
        // create header
        thead = $('
');
        table.append( thead );
        row = $('
')
        row.append( $('').text("") );
        row.append( $('').text("Status") );
        row.append( $('').text("# nodes") );
        thead.append( row );
        tbody = $('
');
        table.append( tbody );
        // create a row
        for(var e in skeleton_data ) {
            row = $('
');
            row.append( $('').text( skeleton_data[e]['id'] ) );
            var status = $('').text( skeleton_data[e]['status']+'%' );
            row.append( status );
            row.append( $('').text( skeleton_data[e]['nr_nodes'] ) );
            if( parseInt( skeleton_data[e]['status']) === 0 ) {
                status.css('background-color', '#ff8c8c');
            } else if( parseInt( skeleton_data[e]['status']) === 100 ) {
                status.css('background-color', '#6fff5c');
            } else {
                status.css('background-color', '#ffc71d');
            }
            butt = $('<button />').text( "Review" );
            butt.attr( 'id', 'reviewbutton_'+skeleton_data[e]['id'] );
            butt.click( function() {
                self.initReviewSegment( this.id.replace("reviewbutton_", "") );
            });
            row.append( butt );
            tbody.append( row );
        }
        // empty row
        row = $('
');
        tbody.append( row );
        table.append( $('

') );
        $("#project_review_widget").append( table );

    };

    var checkSkeletonID = function() {
        if (!skeletonID) {
            $('#growl-alert').growlAlert({
                autoShow: true,
                content: 'You need to activate a skeleton to review.',
                title: 'BEWARE',
                position: 'top-right',
                delayTime: 2500,
                onComplete: function() { g.remove(); }
            });
            return false;
        }
        return true;
    };

    this.startSkeletonToReview = function( ) {
        skeletonID = SkeletonAnnotations.getActiveSkeletonId();
        if (!checkSkeletonID()) {
            return;
        }
        requestQueue.replace(
            "dj/"+projectID+"/skeleton/" + skeletonID + "/review",
            "POST",
            {},
            function (status, text) {
                if (200 !== status) { return; }
                var skeleton_data = $.parseJSON(text);
                if (skeleton_data.error) {
                    if ("REPLACED" === skeleton_data.error) { return; }
                    alert( skeleton_data.error );
                } else {
                    requestQueue.register("dj" + "/accounts/" + projectID + "/all-usernames", "POST", {},
                        function(status, text) {
                            if (200 !== status) { return; }
                            var usernames = $.parseJSON(text);
                            if (usernames.error) {
                                alert(usernames.error);
                            } else {
                                self.createReviewSkeletonTable( skeleton_data, usernames );
                            }
                        });
                }
            },
            "start_review_skeleton");
    };

    var resetFn = function(fnName) {
        if (!checkSkeletonID()) {
            return;
        }
        if (!confirm("Are you sure you want to alter the review state of skeleton #" + skeletonID + " with '" + fnName + "' ?")) {
            return;
        }
        requestQueue.replace(
            "dj/"+projectID+"/skeleton/" + skeletonID + "/review/" + fnName,
            "POST",
            {},
            function (status, text) {
                if (200 !== status) { return; }
                var json = $.parseJSON(text);
                if (json.error) {
                    if ("REPLACED" === json.error) { return; }
                    alert(json.error);
                    return;
                }
                self.startSkeletonToReview();
            },
            "review_" + fnName);
    };

    this.resetAllRevisions = function() {
        resetFn("reset-all");
    };

    this.resetOwnRevisions = function() {
        resetFn("reset-own");
    };

    this.resetRevisionsByOthers = function() {
        resetFn("reset-others");
    };
};
<|endoftext|>


<commit_before>/**
 * @file EditableSelect component
 * @author sunday(sunday.wei@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {findDOMNode} from 'react-dom';
import classNames from 'classnames';

import TextField from '../TextField';
import Popup from '../Popup';
import List from '../List';
import DynamicRenderList from '../DynamicRenderList';
import GroupList from '../GroupList';
import Theme from '../Theme';

import Position from '../_statics/Position';

import Util from '../_vendors/Util';
import DropdownCalculation from '../_vendors/DropdownCalculation';

class EditableSelect extends Component {

    static Theme = Theme;
    static Position = Position;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            value: props.value,
            listValue: props.value,
            filter: '',
            popupVisible: false,
            isAbove: false
        };

    }

    onChangeValue = value => {
        const {useFilter} = this.props;
        if (useFilter) {
            this.setState({
                value,
                filter: value,
                listValue: ''
            }, () => {
                const {onChange} = this.props;
                onChange && onChange(value);
            });
        } else {
            this.setState({
                value,
                listValue: ''
            }, () => {
                const {onChange} = this.props;
                onChange && onChange(value);
            });
        }

    };

    showPopup = () => {
        this.setState({
            popupVisible: true
        }, () => {
            this.props.onTriggerClick && this.props.onTriggerClick(this.state.popupVisible);
            this.props.onFocus && this.props.onFocus();
        });
    };

    closePopup = () => {
        this.setState({
            popupVisible: false
        }, () => {
            this.props.onTriggerClick && this.props.onTriggerClick(this.state.popupVisible);
            this.props.onPopupClosed && this.props.onPopupClosed();
        });
    };

    triggerHandler = (el, triggerEl, popupEl, currentVisible, isBlurClose) => {

        if (!triggerEl) {
            return true;
        }

        while (el) {
            if (el == popupEl) {
                return currentVisible;
            } else if (el == triggerEl) {
                return true;
            }
            el = el.parentNode;
        }

        return isBlurClose ? false : currentVisible;

    };

    filterData = (filter = this.state.filter, data = this.props.data) => {

        if (!filter) {
            return data;
        }

        const {displayField, isGrouped} = this.props,
            filterFunc = (originData) => {
                return originData.filter(item => typeof item === 'object' && !!item[displayField] ?
                    item[displayField].toString().toUpperCase().includes(filter.toUpperCase())
                    :
                    item.toString().toUpperCase().includes(filter.toUpperCase()));
            };

        if (isGrouped) {

            let result = Object.assign(data);

            for (let i = 0, len = result.length; i < len; i++) {
                let group = result[i];
                group.children = filterFunc(group.children);
                if (group.children.length < 1) {
                    result.splice(i, 1);
                    i--;
                }
            }

        }

        return filterFunc(data);

    };

    popupRenderHandle = popupEl => {

        const isAbove = DropdownCalculation.isAbove(this.editabledSelectEl, this.triggerEl, findDOMNode(popupEl));

        if (isAbove !== this.state.isAbove) {
            this.setState({
                isAbove
            });
        }

    };

    changeHandle = value => {
        const {valueField, renderer} = this.props;
        let itemValue = renderer ? renderer(value) : (typeof value == 'object' ? value[valueField] : value);
        const {autoClose} = this.props,
            state = {
                value: itemValue,
                listValue: value
            };

        if (autoClose) {
            state.popupVisible = false;
        }

        this.setState(state, () => {
            const {onChange} = this.props;
            onChange && onChange(itemValue);
        });

    };

    componentDidMount() {
        this.editabledSelectEl = this.refs.editabledSelect;
        this.triggerEl = findDOMNode(this.refs.trigger);
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.value !== this.props.value) {
            this.setState({
                value: nextProps.value
            });
        }
    }

    render() {

        const {
                className, popupClassName, style, popupStyle, name, placeholder,
                disabled, valueField, descriptionField, position, rightIconCls, resetPopPositionWait,
                triggerTheme, isGrouped, onItemClick, renderer, noMatchedMsg, useDynamicRenderList,
                onMouseOver, onMouseOut, parentEl
            } = this.props,
            {value, listValue, popupVisible, isAbove} = this.state,

            isAboveFinally = position === Position.TOP || position === Position.TOP_LEFT
                || position === Position.TOP_RIGHT || (!position && isAbove),

            triggerClassName = classNames('editable-select-trigger', isAboveFinally ? 'above' : 'blow', {
                activated: popupVisible,
                empty: !value
            }),

            editableSelectPopupClassName = classNames('editable-select-popup', isAboveFinally ? 'above' : 'blow', {
                [popupClassName]: popupClassName
            }),

            editablePopupStyle = Object.assign({
                width: this.triggerEl && getComputedStyle(this.triggerEl).width
            }, popupStyle),

            listData = this.filterData();

        return (

                {
                    name ?
                        <input type="hidden"
                               name={name}
                               value={value}/>
                        :
                        null
                }

                <TextField ref="trigger"
                           className={triggerClassName}
                           value={value}
                           rightIconCls={`${rightIconCls} editable-select-trigger-icon`}
                           placeholder={placeholder}
                           disabled={disabled}
                           theme={triggerTheme}
                           onMouseOver={onMouseOver}
                           onMouseOut={onMouseOut}
                           onChange={this.onChangeValue}
                           onFocus={this.showPopup}/>

                <Popup ref="popup"
                       className={editableSelectPopupClassName}
                       style={editablePopupStyle}
                       visible={popupVisible}
                       triggerEl={this.triggerEl}
                       triggerHandler={this.triggerHandler}
                       parentEl={parentEl}
                       hasTriangle={false}
                       position={position ? position : (isAbove ? Position.TOP_LEFT : Position.BOTTOM_LEFT)}
                       resetPositionWait={resetPopPositionWait}
                       onRender={this.popupRenderHandle}
                       onRequestClose={this.closePopup}>

                    {
                        listData.length < 1 ?

                                {
                                    noMatchedMsg ?
                                        noMatchedMsg
                                        :

                                            No matched value.

                                }

                            :

                            isGrouped ?
                                <GroupList className="editable-select-list"
                                           data={listData}
                                           value={listValue}
                                           valueField={valueField}
                                           displayField={valueField}
                                           descriptionField={descriptionField}
                                           renderer={renderer}
                                           onItemClick={this.onItemClick}
                                           onChange={this.changeHandler}/>
                                :

                                (
                                    useDynamicRenderList ?
                                        <DynamicRenderList className="editable-select-list"
                                                           data={listData}
                                                           value={listValue}
                                                           valueField={valueField}
                                                           displayField={valueField}
                                                           descriptionField={descriptionField}
                                                           renderer={renderer}
                                                           onItemClick={onItemClick}
                                                           onChange={this.changeHandle}/>
                                        :
                                        <List className="editable-select-list"
                                              data={listData}
                                              valueField={valueField}
                                              value={listValue}
                                              displayField={valueField}
                                              descriptionField={descriptionField}
                                              renderer={renderer}
                                              onItemClick={onItemClick}
                                              onChange={this.changeHandle}/>
                                )

                    }

                </Popup>

        );

    }
}

EditableSelect.propTypes = {

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

    /**
     * The class name of the popup element.
     */
    popupClassName: PropTypes.string,

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

    rightIconCls: PropTypes.string,

    /**
     * Override the styles of the popup element.
     */
    popupStyle: PropTypes.object,

    position: PropTypes.oneOf(Util.enumerateValue(Position)),

    /**
     * The name of the editableSelect.
     */
    name: PropTypes.string,

    /**
     * The value of the editableSelect.
     */
    value: PropTypes.any,

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

    /**
     * The placeholder of the editableSelect.
     */
    placeholder: PropTypes.string,

    /**
     * The options data.
     */
    data: PropTypes.oneOfType([

        // not grouped
        PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({

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

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

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

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

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

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

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

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

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

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

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

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

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

        // grouped
        PropTypes.array

    ]).isRequired,

    /**
     * The invalid message of editableSelect.
     */
    invalidMsg: PropTypes.string,

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

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

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

    /**
     * The message of the editableSelect.
     */
    infoMsg: PropTypes.string,

    /**
     * Use this function to format the options's text.
     */
    textFormat: PropTypes.func,

    /**
     * If true,the drop-down box automatically closed after selection.
     */
    autoClose: PropTypes.bool,

    /**
     * If true,the drop-down box will have search input.
     */
    useFilter: PropTypes.bool,

    /**
     * The theme of editableSelect.Can be primary,highlight,success,warning,error.
     */
    triggerTheme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * If true,the drop-down box will be have group selection.
     */
    isGrouped: PropTypes.bool,

    resetPopPositionWait: PropTypes.number,

    /**
     * Callback function fired when the button is touch-tapped.
     */
    onItemClick: PropTypes.func,

    /**
     * Callback function fired when a menu item is selected.
     */
    onChange: PropTypes.func,

    onMouseOver: PropTypes.func,
    onMouseOut: PropTypes.func

};

EditableSelect.defaultProps = {

    name: '',
    value: '',
    placeholder: 'Please select ...',
    rightIconCls: 'fas fa-angle-down',
    data: [],
    invalidMsg: '',
    disabled: false,

    displayField: 'text',
    descriptionField: 'desc',

    infoMsg: '',
    autoClose: true,
    useFilter: false,
    useDynamicRenderList: false,
    triggerTheme: Theme.DEFAULT,
    isGrouped: false,
    resetPopPositionWait: 250

};

export default EditableSelect;
<commit_msg>update EditableSelect<commit_after>/**
 * @file EditableSelect component
 * @author sunday(sunday.wei@derbysoft.com)
 */

import React, {Component, createRef} from 'react';
import PropTypes from 'prop-types';
import {findDOMNode} from 'react-dom';
import classNames from 'classnames';

import TextField from '../TextField';
import Popup from '../Popup';
import List from '../List';
import DynamicRenderList from '../DynamicRenderList';
import GroupList from '../GroupList';

import Theme from '../Theme';
import Position from '../_statics/Position';

import Util from '../_vendors/Util';
import DropdownCalculation from '../_vendors/DropdownCalculation';
import ComponentUtil from '../_vendors/ComponentUtil';

class EditableSelect extends Component {

    static Theme = Theme;
    static Position = Position;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.wrapper = createRef();
        this.wrapperEl = null;
        this.trigger = createRef();
        this.triggerEl = null;

        this.state = {
            value: props.value,
            listValue: props.value,
            filter: '',
            popupVisible: false,
            isAbove: false
        };

    }

    handleChangeValue = value => {
        const {useFilter} = this.props;
        if (useFilter) {
            this.setState({
                value,
                filter: value,
                listValue: ''
            }, () => {
                const {onChange} = this.props;
                onChange && onChange(value);
            });
        } else {
            this.setState({
                value,
                listValue: ''
            }, () => {
                const {onChange} = this.props;
                onChange && onChange(value);
            });
        }
    };

    showPopup = () => {
        this.setState({
            popupVisible: true
        }, () => {
            this.props.onTriggerClick && this.props.onTriggerClick(this.state.popupVisible);
            this.props.onFocus && this.props.onFocus();
        });
    };

    closePopup = () => {
        this.setState({
            popupVisible: false
        }, () => {
            this.props.onTriggerClick && this.props.onTriggerClick(this.state.popupVisible);
            this.props.onPopupClosed && this.props.onPopupClosed();
        });
    };

    triggerHandler = (el, triggerEl, popupEl, currentVisible, isBlurClose) => {

        if (!triggerEl) {
            return true;
        }

        while (el) {
            if (el == popupEl) {
                return currentVisible;
            } else if (el == triggerEl) {
                return true;
            }
            el = el.parentNode;
        }

        return isBlurClose ? false : currentVisible;

    };

    filterData = (filter = this.state.filter, data = this.props.data) => {

        if (!filter) {
            return data;
        }

        const {displayField, isGrouped} = this.props,
            filterFunc = originData => {
                return originData.filter(item => typeof item === 'object' && !!item[displayField] ?
                    item[displayField].toString().toUpperCase().includes(filter.toUpperCase())
                    :
                    item.toString().toUpperCase().includes(filter.toUpperCase()));
            };

        if (isGrouped) {

            let result = Object.assign(data);

            for (let i = 0, len = result.length; i < len; i++) {
                let group = result[i];
                group.children = filterFunc(group.children);
                if (group.children.length < 1) {
                    result.splice(i, 1);
                    i--;
                }
            }

        }

        return filterFunc(data);

    };

    handlePopupRender = popupEl => {
        const isAbove = DropdownCalculation.isAbove(this.wrapperEl, this.triggerEl, findDOMNode(popupEl));
        if (isAbove !== this.state.isAbove) {
            this.setState({
                isAbove
            });
        }
    };

    handleChange = value => {

        const {valueField, renderer} = this.props;
        let itemValue = renderer ? renderer(value) : (typeof value == 'object' ? value[valueField] : value);
        const {autoClose} = this.props,
            state = {
                value: itemValue,
                listValue: value
            };

        if (autoClose) {
            state.popupVisible = false;
        }

        this.setState(state, () => {
            const {onChange} = this.props;
            onChange && onChange(itemValue);
        });

    };

    componentDidMount() {
        this.wrapperEl = this.wrapper && this.wrapper.current;
        this.triggerEl = this.trigger && this.trigger.current && findDOMNode(this.trigger.current);
    }

    static getDerivedStateFromProps(props, state) {
        return {
            prevProps: props,
            value: ComponentUtil.getDerivedState(props, state, 'value')
        };
    }

    render() {

        const {
                className, popupClassName, style, popupStyle, name, placeholder,
                disabled, valueField, descriptionField, position, rightIconCls, resetPopPositionWait,
                triggerTheme, isGrouped, onItemClick, renderer, noMatchedMsg, useDynamicRenderList,
                onMouseOver, onMouseOut, parentEl
            } = this.props,
            {value, listValue, popupVisible, isAbove} = this.state,

            isAboveFinally = position === Position.TOP || position === Position.TOP_LEFT
                || position === Position.TOP_RIGHT || (!position && isAbove),
            listData = this.filterData();

        return (

                {
                    name ?
                        <input type="hidden"
                               name={name}
                               value={value}/>
                        :
                        null
                }

                <TextField ref={this.trigger}
                           className={classNames('editable-select-trigger', isAboveFinally ? 'above' : 'blow', {
                               activated: popupVisible,
                               empty: !value
                           })}
                           value={value}
                           rightIconCls={`${rightIconCls} editable-select-trigger-icon`}
                           placeholder={placeholder}
                           disabled={disabled}
                           theme={triggerTheme}
                           onMouseOver={onMouseOver}
                           onMouseOut={onMouseOut}
                           onChange={this.handleChangeValue}
                           onFocus={this.showPopup}/>

                <Popup className={classNames('editable-select-popup', isAboveFinally ? 'above' : 'blow', {
                    [popupClassName]: popupClassName
                })}
                       style={Object.assign({
                           width: this.triggerEl && getComputedStyle(this.triggerEl).width
                       }, popupStyle)}
                       visible={popupVisible}
                       triggerEl={this.triggerEl}
                       triggerHandler={this.triggerHandler}
                       parentEl={parentEl}
                       hasTriangle={false}
                       position={position ? position : (isAbove ? Position.TOP_LEFT : Position.BOTTOM_LEFT)}
                       resetPositionWait={resetPopPositionWait}
                       onRender={this.handlePopupRender}
                       onRequestClose={this.closePopup}>

                    {
                        listData.length < 1 ?

                                {
                                    noMatchedMsg ?
                                        noMatchedMsg
                                        :

                                            No matched value.

                                }

                            :
                            isGrouped ?
                                <GroupList className="editable-select-list"
                                           data={listData}
                                           value={listValue}
                                           valueField={valueField}
                                           displayField={valueField}
                                           descriptionField={descriptionField}
                                           renderer={renderer}
                                           onItemClick={this.onItemClick}
                                           onChange={this.changeHandler}/>
                                :
                                useDynamicRenderList ?
                                    <DynamicRenderList className="editable-select-list"
                                                       data={listData}
                                                       value={listValue}
                                                       valueField={valueField}
                                                       displayField={valueField}
                                                       descriptionField={descriptionField}
                                                       renderer={renderer}
                                                       onItemClick={onItemClick}
                                                       onChange={this.handleChange}/>
                                    :
                                    <List className="editable-select-list"
                                          data={listData}
                                          valueField={valueField}
                                          value={listValue}
                                          displayField={valueField}
                                          descriptionField={descriptionField}
                                          renderer={renderer}
                                          onItemClick={onItemClick}
                                          onChange={this.handleChange}/>
                    }

                </Popup>

        );

    }
}

EditableSelect.propTypes = {

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

    /**
     * The class name of the popup element.
     */
    popupClassName: PropTypes.string,

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

    rightIconCls: PropTypes.string,

    /**
     * Override the styles of the popup element.
     */
    popupStyle: PropTypes.object,

    position: PropTypes.oneOf(Util.enumerateValue(Position)),

    /**
     * The name of the editableSelect.
     */
    name: PropTypes.string,

    /**
     * The value of the editableSelect.
     */
    value: PropTypes.any,

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

    /**
     * The placeholder of the editableSelect.
     */
    placeholder: PropTypes.string,

    /**
     * The options data.
     */
    data: PropTypes.oneOfType([

        // not grouped
        PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({

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

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

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

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

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

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

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

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

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

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

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

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

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

        // grouped
        PropTypes.array

    ]).isRequired,

    /**
     * The invalid message of editableSelect.
     */
    invalidMsg: PropTypes.string,

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

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

    displayField: PropTypes.string,

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

    /**
     * The message of the editableSelect.
     */
    infoMsg: PropTypes.string,

    /**
     * Use this function to format the options's text.
     */
    textFormat: PropTypes.func,

    /**
     * If true,the drop-down box automatically closed after selection.
     */
    autoClose: PropTypes.bool,

    /**
     * If true,the drop-down box will have search input.
     */
    useFilter: PropTypes.bool,

    /**
     * The theme of editableSelect.Can be primary,highlight,success,warning,error.
     */
    triggerTheme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * If true,the drop-down box will be have group selection.
     */
    isGrouped: PropTypes.bool,

    resetPopPositionWait: PropTypes.number,
    noMatchedMsg: PropTypes.string,
    useDynamicRenderList: PropTypes.bool,
    parentEl: PropTypes.object,

    /**
     * Callback function fired when the button is touch-tapped.
     */
    onItemClick: PropTypes.func,

    /**
     * Callback function fired when a menu item is selected.
     */
    onChange: PropTypes.func,

    onMouseOver: PropTypes.func,
    onMouseOut: PropTypes.func,
    onTriggerClick: PropTypes.func,
    onFocus: PropTypes.func,
    onPopupClosed: PropTypes.func

};

EditableSelect.defaultProps = {

    name: '',
    value: '',
    placeholder: 'Please select ...',
    rightIconCls: 'fas fa-angle-down',
    data: [],
    invalidMsg: '',
    disabled: false,

    displayField: 'text',
    descriptionField: 'desc',

    infoMsg: '',
    autoClose: true,
    useFilter: false,
    useDynamicRenderList: false,
    triggerTheme: Theme.DEFAULT,
    isGrouped: false,
    resetPopPositionWait: 250

};

export default EditableSelect;
<|endoftext|>