<commit_before>'use strict';

const calculator = require('./calculator');
const coloriser = require('./coloriser');
const deferAndDropExcess = require('./deferAndDropExcess');
const displayBlockStore = require('./displayBlockStore');
const scheduler = require('./scheduler');

module.exports = function Renderer(canvas) {
  const self = this;

  self.depth = 500;
  self.canvas = canvas;
  self.ctx = canvas.getContext('2d');
  self.center = { x: -0.75, y: 0 };
  self.width = 8;

  self.coloriser = new coloriser();

  self.calculator = calculator();
  self.displayBlockStore = null;

  self.pixelRatio = window.devicePixelRatio || 1;

  self.scheduler = scheduler(20);

  self.drawIndex = 0;
  self.drawBegin = null;
  self.drawEnd = null;

  self.updateSize = function () {
    canvas.style.width = `${canvas.parentNode.clientWidth}px`;
    canvas.style.height = `${canvas.parentNode.clientHeight}px`;

    canvas.width = self.pixelRatio * canvas.parentNode.clientWidth;
    canvas.height = self.pixelRatio * canvas.parentNode.clientHeight;
  }

  ;(function () {
    self.updateSize();

    canvas.parentNode.addEventListener('keydown', (evt) => {
      if (evt.keyCode === 67) {
        self.coloriser.randomise(!evt.shiftKey ? 1 : -1);
        self.drawBlocksCached(); // TODO: rename to redrawCurrentBlocks?
      }

      if (evt.keyCode === 187 || evt.keyCode === 189) {
        if (evt.shiftKey) {
          self.coloriser.shift(0.05 * (188 - evt.keyCode));
        } else {
          self.coloriser.multiplySpeed(Math.exp(0.05 * (188 - evt.keyCode)));
        }

        self.drawBlocksCached();
      }
    });

    let pressSession = null;

    const pressSessionRef = () => {
      if (!pressSession) {
        return null;
      }

      if (!pressSession.secondary) {
        return pressSession.primary;
      }

      return {
        start: {
          x: 0.5 * (pressSession.primary.start.x + pressSession.secondary.start.x),
          y: 0.5 * (pressSession.primary.start.y + pressSession.secondary.start.y),
        },
        curr: {
          x: 0.5 * (pressSession.primary.curr.x + pressSession.secondary.curr.x),
          y: 0.5 * (pressSession.primary.curr.y + pressSession.secondary.curr.y),
        },
      };
    };

    const onPressStart = (press) => {
      if (!pressSession) {
        pressSession = {
          dragData: (() => {
            const cvs = document.createElement('canvas');
            cvs.setAttribute('width', canvas.width);
            cvs.setAttribute('height', canvas.height);

            const ctx = cvs.getContext('2d');
            const imgData = self.ctx.getImageData(0, 0, canvas.width, canvas.height);
            ctx.putImageData(imgData, 0, 0);

            return cvs;
          })(),
          dragDataIncomplete: self.drawBegin && !self.drawEnd,
        };
      }

      const pressDetails = {
        id: press.id,
        start: { x: press.x, y: press.y },
        curr: { x: press.x, y: press.y },
      };

      if (!pressSession.primary) {
        pressSession.primary = pressDetails;
      } else if (!pressSession.secondary) {
        pressSession.secondary = pressDetails;
      }
    };

    const onPressMove = (press) => {
      if (!pressSession) {
        return;
      }

      let primaryPressChanged = false;
      let secondaryPressChanged = false;

      if (press.id === pressSession.primary.id) {
        pressSession.primary.curr.x = press.x;
        pressSession.primary.curr.y = press.y;

        primaryPressChanged = true;
      }

      if (press.id === (pressSession.secondary && pressSession.secondary.id)) {
        pressSession.secondary.curr.x = press.x;
        pressSession.secondary.curr.y = press.y;

        const sq = x => x * x;
        const dist = (p1, p2) => Math.sqrt(sq(p1.x - p2.x) + sq(p1.y - p2.y));

        pressSession.zoom = (
          dist(pressSession.primary.curr, pressSession.secondary.curr) /
          dist(pressSession.primary.start, pressSession.secondary.start)
        );

        secondaryPressChanged = true;
      }

      if (primaryPressChanged || secondaryPressChanged) {
        const refPress = pressSessionRef();
        const diff = { x: refPress.curr.x - refPress.start.x, y: refPress.curr.y - refPress.start.y };
        self.ctx.clearRect(0, 0, canvas.width, canvas.height);

        const zoom = pressSession.zoom || 1;

        let dx = 0;
        dx += self.pixelRatio * diff.x;
        dx += self.pixelRatio * refPress.start.x * (1 - zoom);

        let dy = 0;
        dy += self.pixelRatio * diff.y;
        dy += self.pixelRatio * refPress.start.y * (1 - zoom);

        self.ctx.drawImage(
          pressSession.dragData,
          dx,
          dy,
          pressSession.dragData.width * zoom,
          pressSession.dragData.height * zoom,
        );
      }
    };

    const onPressCancel = (press) => {
      if (!pressSession) {
        return;
      }

      if (press.id !== pressSession.primary.id) {
        return;
      }

      pressSession = null;
      self.draw(self.center);
    };

    const onPressEnd = (press) => {
      if (!pressSession) {
        return;
      }

      if (press.id !== pressSession.primary.id) {
        return;
      }

      const refPress = pressSessionRef();

      const diff = { x: refPress.curr.x - refPress.start.x, y: refPress.curr.y - refPress.start.y };

      if (diff.x === 0 && diff.y === 0) {
        return;
      }

      const pixelSize = self.width / canvas.width;
      const aspectRatio = canvas.width / canvas.height;

      const alreadyDrawnRect = {
        topLeft: {
          x: self.center.x - 0.5 * self.width,
          y: self.center.y - 0.5 / aspectRatio * self.width,
        },
        bottomRight: {
          x: self.center.x + 0.5 * self.width,
          y: self.center.y + 0.5 / aspectRatio * self.width,
        },
        needsRedraw: pressSession.dragDataIncomplete || pressSession.zoom,
      };

      const pos = {
        x: (
          self.center.x -
          0.5 * self.width +
          refPress.start.x * self.pixelRatio * pixelSize
        ),
        y: (
          self.center.y -
          0.5 / aspectRatio *
          self.width +
          refPress.start.y * self.pixelRatio * pixelSize
        ),
      };

      self.moveCenter({
        x: -diff.x * pixelSize * self.pixelRatio,
        y: -diff.y * pixelSize * self.pixelRatio,
      });

      if (pressSession.zoom) {
        self.scale(1 / pressSession.zoom, pos, alreadyDrawnRect);
      } else {
        self.draw(pos, alreadyDrawnRect);
      }

      pressSession = null;
    };

    let clickCount = 0;
    let clickInProgress = false;

    canvas.addEventListener('mousedown', (e) => {
      if (clickInProgress) {
        return;
      }

      clickInProgress = true;

      onPressStart({
        x: e.clientX,
        y: e.clientY,
        id: `click${clickCount}`,
      });
    });

    canvas.addEventListener('mousemove', (e) => {
      onPressMove({
        x: e.clientX,
        y: e.clientY,
        id: `click${clickCount}`,
      });
    });

    canvas.addEventListener('mouseup', (e) => {
      clickInProgress = false;

      onPressEnd({
        x: e.clientX,
        y: e.clientY,
        id: `click${clickCount}`,
      });

      clickCount++;
    });

    canvas.addEventListener('touchstart', (e) => {
      e.preventDefault();

      Array.from(e.changedTouches).forEach((touch) => {
        onPressStart({
          x: touch.clientX,
          y: touch.clientY,
          id: `touch${touch.identifier}`,
        });
      });
    });

    canvas.addEventListener('touchmove', (e) => {
      e.preventDefault();

      Array.from(e.changedTouches).forEach((touch) => {
        onPressMove({
          x: touch.clientX,
          y: touch.clientY,
          id: `touch${touch.identifier}`,
        });
      });
    });

    canvas.addEventListener('touchcancel', (e) => {
      Array.from(e.changedTouches).forEach((touch) => {
        onPressCancel({
          x: touch.clientX,
          y: touch.clientY,
          id: `touch${touch.identifier}`,
        });
      });
    });

    canvas.addEventListener('touchend', (e) => {
      Array.from(e.changedTouches).forEach((touch) => {
        onPressEnd({
          x: touch.clientX,
          y: touch.clientY,
          id: `touch${touch.identifier}`,
        });
      });
    });

    const zoom = function (dz, pos) {
      const pixelSize = self.width / canvas.width;
      const aspectRatio = canvas.width / canvas.height;

      self.scale(
        dz < 0 ? 2 / 3 : 3 / 2,
        {
          x: (
            self.center.x -
            0.5 * self.width +
            pos.x * self.pixelRatio * pixelSize
          ),
          y: (
            self.center.y -
            0.5 / aspectRatio * self.width +
            self.pixelRatio * pixelSize * pos.y
          ),
        },
      );
    };

    const mousePos = {
      x: 0,
      y: 0,
    };

    canvas.addEventListener('mousemove', (e) => {
      mousePos.x = e.clientX;
      mousePos.y = e.clientY;
    });

    canvas.parentNode.addEventListener('keydown', (e) => {
      if (e.keyCode === 68) {
        self.depth = parseInt(window.prompt('Enter new depth', '500'));
        self.draw();
      } else if (e.keyCode === 65) { // a
        zoom(-1, mousePos);
      } else if (e.keyCode === 90) { // z
        zoom(1, mousePos);
      }
    });
  }());

  self.draw = deferAndDropExcess((pos, alreadyDrawnRect) => { // pos === referencePoint?
    pos = pos || self.center;
    self.scheduler.clear();
    self.drawIndex++;
    const drawIndex = self.drawIndex;

    // self.coloriser.clearCache() // TODO: was this a good idea when it used to work?

    const pixelWidth = self.width / canvas.width;
    const aspectRatio = canvas.width / canvas.height;

    const topLeft = {
      x: self.center.x - 0.5 * self.width,
      y: self.center.y - 0.5 / aspectRatio * self.width,
    };

    const bottomRight = {
      x: topLeft.x + self.width,
      y: topLeft.y + self.width / aspectRatio,
    };

    const rect = { topLeft, bottomRight };

    self.drawBegin = Date.now();
    self.drawEnd = null;

    self.displayBlockStore = new displayBlockStore(
      self.center,
      self.canvas.width,
      self.canvas.height,
      self.width,
      self.width / self.canvas.width * self.canvas.height,
    );

    let incompleteDraw = false;

    const blocks = self.calculator.getBlocksForScreen(
      pos,
      rect,
      alreadyDrawnRect,
      pixelWidth,
      self.depth,
    );

    const jobs = [];
    const jobsLater = [];

    blocks.forEach((block) => {
      if (!block.later) {
        jobs.push(() => block.calculateOnePoint()
          .then((pointValue) => {
            if (drawIndex !== self.drawIndex || pointValue === null) {
              return;
            }

            const scaled = self.displayBlockStore.scalePoint(pointValue, block.depth);
            const colorised = self.coloriser.colorise(scaled);

            if (!self.displayBlockStore.centralPixelPos) {
              self.displayBlockStore.centralPixelPos = self.displayBlockStore.calculateCentralPixelPos(block);
            }

            const pixelPos = {
              x: self.displayBlockStore.centralPixelPos.x + block.size * block.j,
              y: self.displayBlockStore.centralPixelPos.y + block.size * block.i,
            };

            self.ctx.fillStyle = `rgba(${Math.floor(colorised.r)},${Math.floor(colorised.g)},${Math.floor(colorised.b)},${Math.floor(colorised.a)}`;
            self.ctx.fillRect(pixelPos.x, pixelPos.y, block.size, block.size);
          }),
        );
      }

      jobsLater.push(() => block.calculate().then((calc) => {
        if (!calc) {
          incompleteDraw = true;
          return null;
        }

        if (drawIndex !== self.drawIndex) {
          return Promise.resolve();
        }

        const scaledBlock = self.displayBlockStore.scaleBlock(block);
        self.displayBlockStore.add(scaledBlock);

        return self.drawBlock(scaledBlock).then((drawn) => {
          if (!drawn) {
            incompleteDraw = true;
          }

          return scaledBlock;
        });
      }));
    });

    const promises = [];
    promises.push(...jobs.map(job => job()));

    const scaledBlocksPromise = Promise.all(jobsLater.map(job => job()))
      .then((scaledBlocks) => {
        if (!incompleteDraw) {
          self.drawEnd = Date.now();
          console.log(self.drawEnd - self.drawBegin);
        }

        self.coloriser.updateReferenceColor(scaledBlocks);
      })
    ;

    promises.push(scaledBlocksPromise);

    return Promise.all(promises);
  });

  self.drawBlock = self.scheduler((block) => {
    const pix = self.ctx.createImageData(block.size, block.size);

    // TODO: this belongs in the coloriser
    self.coloriser.blockDataToPixelData(block.data, pix);

    self.ctx.putImageData(
      pix,
      block.pixelPos.x,
      block.pixelPos.y,
    );

    return true;
  });

  self.drawBlocksCached = deferAndDropExcess(() => {
    if (!self.displayBlockStore) {
      self.draw();
      return;
    }

    self.scheduler.clear();
    // self.displayBlockStore.blocks.forEach(self.scheduler(self.drawBlock))
    self.displayBlockStore.blocks.forEach(self.drawBlock);
  });

  self.scale = function (factor, pos, alreadyDrawnRect) {
    pos = (pos || self.center);

    self.center = {
      x: factor * self.center.x + (1 - factor) * pos.x,
      y: factor * self.center.y + (1 - factor) * pos.y,
    };

    self.width *= factor;

    self.draw(pos, alreadyDrawnRect);
  };

  self.moveCenter = function (p) {
    self.center.x += p.x;
    self.center.y += p.y;
  };
};
<commit_msg>Fix more stale draws<commit_after>'use strict';

const calculator = require('./calculator');
const coloriser = require('./coloriser');
const deferAndDropExcess = require('./deferAndDropExcess');
const displayBlockStore = require('./displayBlockStore');
const scheduler = require('./scheduler');

module.exports = function Renderer(canvas) {
  const self = this;

  self.depth = 500;
  self.canvas = canvas;
  self.ctx = canvas.getContext('2d');
  self.center = { x: -0.75, y: 0 };
  self.width = 8;

  self.coloriser = new coloriser();

  self.calculator = calculator();
  self.displayBlockStore = null;

  self.pixelRatio = window.devicePixelRatio || 1;

  self.scheduler = scheduler(20);

  self.drawIndex = 0;
  self.drawBegin = null;
  self.drawEnd = null;

  self.updateSize = function () {
    canvas.style.width = `${canvas.parentNode.clientWidth}px`;
    canvas.style.height = `${canvas.parentNode.clientHeight}px`;

    canvas.width = self.pixelRatio * canvas.parentNode.clientWidth;
    canvas.height = self.pixelRatio * canvas.parentNode.clientHeight;
  }

  ;(function () {
    self.updateSize();

    canvas.parentNode.addEventListener('keydown', (evt) => {
      if (evt.keyCode === 67) {
        self.coloriser.randomise(!evt.shiftKey ? 1 : -1);
        self.drawBlocksCached(); // TODO: rename to redrawCurrentBlocks?
      }

      if (evt.keyCode === 187 || evt.keyCode === 189) {
        if (evt.shiftKey) {
          self.coloriser.shift(0.05 * (188 - evt.keyCode));
        } else {
          self.coloriser.multiplySpeed(Math.exp(0.05 * (188 - evt.keyCode)));
        }

        self.drawBlocksCached();
      }
    });

    let pressSession = null;

    const pressSessionRef = () => {
      if (!pressSession) {
        return null;
      }

      if (!pressSession.secondary) {
        return pressSession.primary;
      }

      return {
        start: {
          x: 0.5 * (pressSession.primary.start.x + pressSession.secondary.start.x),
          y: 0.5 * (pressSession.primary.start.y + pressSession.secondary.start.y),
        },
        curr: {
          x: 0.5 * (pressSession.primary.curr.x + pressSession.secondary.curr.x),
          y: 0.5 * (pressSession.primary.curr.y + pressSession.secondary.curr.y),
        },
      };
    };

    const onPressStart = (press) => {
      if (!pressSession) {
        pressSession = {
          dragData: (() => {
            const cvs = document.createElement('canvas');
            cvs.setAttribute('width', canvas.width);
            cvs.setAttribute('height', canvas.height);

            const ctx = cvs.getContext('2d');
            const imgData = self.ctx.getImageData(0, 0, canvas.width, canvas.height);
            ctx.putImageData(imgData, 0, 0);

            return cvs;
          })(),
          dragDataIncomplete: self.drawBegin && !self.drawEnd,
        };

        self.drawIndex++;
      }

      const pressDetails = {
        id: press.id,
        start: { x: press.x, y: press.y },
        curr: { x: press.x, y: press.y },
      };

      if (!pressSession.primary) {
        pressSession.primary = pressDetails;
      } else if (!pressSession.secondary) {
        pressSession.secondary = pressDetails;
      }
    };

    const onPressMove = (press) => {
      if (!pressSession) {
        return;
      }

      let primaryPressChanged = false;
      let secondaryPressChanged = false;

      if (press.id === pressSession.primary.id) {
        pressSession.primary.curr.x = press.x;
        pressSession.primary.curr.y = press.y;

        primaryPressChanged = true;
      }

      if (press.id === (pressSession.secondary && pressSession.secondary.id)) {
        pressSession.secondary.curr.x = press.x;
        pressSession.secondary.curr.y = press.y;

        const sq = x => x * x;
        const dist = (p1, p2) => Math.sqrt(sq(p1.x - p2.x) + sq(p1.y - p2.y));

        pressSession.zoom = (
          dist(pressSession.primary.curr, pressSession.secondary.curr) /
          dist(pressSession.primary.start, pressSession.secondary.start)
        );

        secondaryPressChanged = true;
      }

      if (primaryPressChanged || secondaryPressChanged) {
        const refPress = pressSessionRef();
        const diff = { x: refPress.curr.x - refPress.start.x, y: refPress.curr.y - refPress.start.y };
        self.ctx.clearRect(0, 0, canvas.width, canvas.height);

        const zoom = pressSession.zoom || 1;

        let dx = 0;
        dx += self.pixelRatio * diff.x;
        dx += self.pixelRatio * refPress.start.x * (1 - zoom);

        let dy = 0;
        dy += self.pixelRatio * diff.y;
        dy += self.pixelRatio * refPress.start.y * (1 - zoom);

        self.ctx.drawImage(
          pressSession.dragData,
          dx,
          dy,
          pressSession.dragData.width * zoom,
          pressSession.dragData.height * zoom,
        );
      }
    };

    const onPressCancel = (press) => {
      if (!pressSession) {
        return;
      }

      if (press.id !== pressSession.primary.id) {
        return;
      }

      pressSession = null;
      self.draw(self.center);
    };

    const onPressEnd = (press) => {
      if (!pressSession) {
        return;
      }

      if (press.id !== pressSession.primary.id) {
        return;
      }

      const refPress = pressSessionRef();

      const diff = { x: refPress.curr.x - refPress.start.x, y: refPress.curr.y - refPress.start.y };

      if (diff.x === 0 && diff.y === 0) {
        return;
      }

      const pixelSize = self.width / canvas.width;
      const aspectRatio = canvas.width / canvas.height;

      const alreadyDrawnRect = {
        topLeft: {
          x: self.center.x - 0.5 * self.width,
          y: self.center.y - 0.5 / aspectRatio * self.width,
        },
        bottomRight: {
          x: self.center.x + 0.5 * self.width,
          y: self.center.y + 0.5 / aspectRatio * self.width,
        },
        needsRedraw: pressSession.dragDataIncomplete || pressSession.zoom,
      };

      const pos = {
        x: (
          self.center.x -
          0.5 * self.width +
          refPress.start.x * self.pixelRatio * pixelSize
        ),
        y: (
          self.center.y -
          0.5 / aspectRatio *
          self.width +
          refPress.start.y * self.pixelRatio * pixelSize
        ),
      };

      self.moveCenter({
        x: -diff.x * pixelSize * self.pixelRatio,
        y: -diff.y * pixelSize * self.pixelRatio,
      });

      if (pressSession.zoom) {
        self.scale(1 / pressSession.zoom, pos, alreadyDrawnRect);
      } else {
        self.draw(pos, alreadyDrawnRect);
      }

      pressSession = null;
    };

    let clickCount = 0;
    let clickInProgress = false;

    canvas.addEventListener('mousedown', (e) => {
      if (clickInProgress) {
        return;
      }

      clickInProgress = true;

      onPressStart({
        x: e.clientX,
        y: e.clientY,
        id: `click${clickCount}`,
      });
    });

    canvas.addEventListener('mousemove', (e) => {
      onPressMove({
        x: e.clientX,
        y: e.clientY,
        id: `click${clickCount}`,
      });
    });

    canvas.addEventListener('mouseup', (e) => {
      clickInProgress = false;

      onPressEnd({
        x: e.clientX,
        y: e.clientY,
        id: `click${clickCount}`,
      });

      clickCount++;
    });

    canvas.addEventListener('touchstart', (e) => {
      e.preventDefault();

      Array.from(e.changedTouches).forEach((touch) => {
        onPressStart({
          x: touch.clientX,
          y: touch.clientY,
          id: `touch${touch.identifier}`,
        });
      });
    });

    canvas.addEventListener('touchmove', (e) => {
      e.preventDefault();

      Array.from(e.changedTouches).forEach((touch) => {
        onPressMove({
          x: touch.clientX,
          y: touch.clientY,
          id: `touch${touch.identifier}`,
        });
      });
    });

    canvas.addEventListener('touchcancel', (e) => {
      Array.from(e.changedTouches).forEach((touch) => {
        onPressCancel({
          x: touch.clientX,
          y: touch.clientY,
          id: `touch${touch.identifier}`,
        });
      });
    });

    canvas.addEventListener('touchend', (e) => {
      Array.from(e.changedTouches).forEach((touch) => {
        onPressEnd({
          x: touch.clientX,
          y: touch.clientY,
          id: `touch${touch.identifier}`,
        });
      });
    });

    const zoom = function (dz, pos) {
      const pixelSize = self.width / canvas.width;
      const aspectRatio = canvas.width / canvas.height;

      self.scale(
        dz < 0 ? 2 / 3 : 3 / 2,
        {
          x: (
            self.center.x -
            0.5 * self.width +
            pos.x * self.pixelRatio * pixelSize
          ),
          y: (
            self.center.y -
            0.5 / aspectRatio * self.width +
            self.pixelRatio * pixelSize * pos.y
          ),
        },
      );
    };

    const mousePos = {
      x: 0,
      y: 0,
    };

    canvas.addEventListener('mousemove', (e) => {
      mousePos.x = e.clientX;
      mousePos.y = e.clientY;
    });

    canvas.parentNode.addEventListener('keydown', (e) => {
      if (e.keyCode === 68) {
        self.depth = parseInt(window.prompt('Enter new depth', '500'));
        self.draw();
      } else if (e.keyCode === 65) { // a
        zoom(-1, mousePos);
      } else if (e.keyCode === 90) { // z
        zoom(1, mousePos);
      }
    });
  }());

  self.draw = deferAndDropExcess((pos, alreadyDrawnRect) => { // pos === referencePoint?
    pos = pos || self.center;
    self.scheduler.clear();
    self.drawIndex++;
    const drawIndex = self.drawIndex;

    // self.coloriser.clearCache() // TODO: was this a good idea when it used to work?

    const pixelWidth = self.width / canvas.width;
    const aspectRatio = canvas.width / canvas.height;

    const topLeft = {
      x: self.center.x - 0.5 * self.width,
      y: self.center.y - 0.5 / aspectRatio * self.width,
    };

    const bottomRight = {
      x: topLeft.x + self.width,
      y: topLeft.y + self.width / aspectRatio,
    };

    const rect = { topLeft, bottomRight };

    self.drawBegin = Date.now();
    self.drawEnd = null;

    self.displayBlockStore = new displayBlockStore(
      self.center,
      self.canvas.width,
      self.canvas.height,
      self.width,
      self.width / self.canvas.width * self.canvas.height,
    );

    let incompleteDraw = false;

    const blocks = self.calculator.getBlocksForScreen(
      pos,
      rect,
      alreadyDrawnRect,
      pixelWidth,
      self.depth,
    );

    const jobs = [];
    const jobsLater = [];

    blocks.forEach((block) => {
      if (!block.later) {
        jobs.push(() => block.calculateOnePoint()
          .then((pointValue) => {
            if (drawIndex !== self.drawIndex || pointValue === null) {
              return;
            }

            const scaled = self.displayBlockStore.scalePoint(pointValue, block.depth);
            const colorised = self.coloriser.colorise(scaled);

            if (!self.displayBlockStore.centralPixelPos) {
              self.displayBlockStore.centralPixelPos = self.displayBlockStore.calculateCentralPixelPos(block);
            }

            const pixelPos = {
              x: self.displayBlockStore.centralPixelPos.x + block.size * block.j,
              y: self.displayBlockStore.centralPixelPos.y + block.size * block.i,
            };

            self.ctx.fillStyle = `rgba(${Math.floor(colorised.r)},${Math.floor(colorised.g)},${Math.floor(colorised.b)},${Math.floor(colorised.a)}`;
            self.ctx.fillRect(pixelPos.x, pixelPos.y, block.size, block.size);
          }),
        );
      }

      jobsLater.push(() => block.calculate().then((calc) => {
        if (!calc) {
          incompleteDraw = true;
          return null;
        }

        if (drawIndex !== self.drawIndex) {
          return Promise.resolve();
        }

        const scaledBlock = self.displayBlockStore.scaleBlock(block);
        self.displayBlockStore.add(scaledBlock);

        return self.drawBlock(scaledBlock, drawIndex).then((drawn) => {
          if (!drawn) {
            incompleteDraw = true;
          }

          return scaledBlock;
        });
      }));
    });

    const promises = [];
    promises.push(...jobs.map(job => job()));

    const scaledBlocksPromise = Promise.all(jobsLater.map(job => job()))
      .then((scaledBlocks) => {
        if (!incompleteDraw) {
          self.drawEnd = Date.now();
          console.log(self.drawEnd - self.drawBegin);
        }

        self.coloriser.updateReferenceColor(scaledBlocks);
      })
    ;

    promises.push(scaledBlocksPromise);

    return Promise.all(promises);
  });

  self.drawBlock = self.scheduler((block, drawIndex) => {
    if (drawIndex !== self.drawIndex) {
      return true;
    }

    const pix = self.ctx.createImageData(block.size, block.size);

    // TODO: this belongs in the coloriser
    self.coloriser.blockDataToPixelData(block.data, pix);

    self.ctx.putImageData(
      pix,
      block.pixelPos.x,
      block.pixelPos.y,
    );

    return true;
  });

  self.drawBlocksCached = deferAndDropExcess(() => {
    if (!self.displayBlockStore) {
      self.draw();
      return;
    }

    self.scheduler.clear();
    // self.displayBlockStore.blocks.forEach(self.scheduler(self.drawBlock))
    self.displayBlockStore.blocks.forEach(self.drawBlock);
  });

  self.scale = function (factor, pos, alreadyDrawnRect) {
    pos = (pos || self.center);

    self.center = {
      x: factor * self.center.x + (1 - factor) * pos.x,
      y: factor * self.center.y + (1 - factor) * pos.y,
    };

    self.width *= factor;

    self.draw(pos, alreadyDrawnRect);
  };

  self.moveCenter = function (p) {
    self.center.x += p.x;
    self.center.y += p.y;
  };
};
<|endoftext|>


<commit_before>window.cls = window.cls || (window.cls = {});

/**
 * @constructor
 * @extends ViewBase
 */

cls.ReplView = function(id, name, container_class, html, default_handler) {
  this._resolver = new cls.PropertyFinder();
  this._data = new cls.ReplData(this);
  this._service = new cls.ReplService(this, this._data);
  this._linelist = null;
  this._textarea = null;
  this._lastupdate = null;
  this._current_input = "";
  this._current_scroll = null;
  this._container = null;
  this._backlog_index = -1;
  this._input_row_height = null;

  this.ondestroy = function()
  {
    this._lastupdate = 0;
    this._backlog_index = -1;
    this._current_input = this._textarea.value;
  };

  this.createView = function(container)
  {
    var switched_to_view = false;
    if (!this._lastupdate)
    {
      container.clearAndRender(templates.repl_main());
      this._linelist = container.querySelector("ol");
      this._textarea = container.querySelector("textarea");
      this._textarea.value = this._current_input;
      this._container = container;
      this._input_row_height = this._textarea.scrollHeight;
      switched_to_view = true;
      // note: events are bound to handlers at the bottom of this class
    }

    this._update();

    if (switched_to_view)
    {
      // defer adding listeners until after update
      this._container.addEventListener("scroll", this._save_scroll_bound, false);
      this._container.addEventListener("DOMAttrModified", this._update_scroll_bound, false);
      this._container.addEventListener("DOMNodeInserted", this._update_scroll_bound, false);

      if(this._current_scroll === null)
      {
        this._container.scrollTop = 999999;
        // timer works around issue where element is unfocusable when created
        window.setTimeout(function() {this._textarea.focus();}.bind(this), 0);
      }
      else
      {
        this._container.scrollTop = this._current_scroll;
      }
    }

  };

  this.clear = function()
  {
    this.ondestroy();
  };

  this._update_input_height_bound = function()
  {
    this._textarea.rows = Math.max(1, Math.ceil(this._textarea.scrollHeight / this._input_row_height));
  }.bind(this);

  this._save_scroll_bound = function()
  {
    var at_bottom = (this._container.scrollTop + this._container.offsetHeight >= this._container.scrollHeight);
    this._current_scroll = at_bottom ? null : this._container.scrollTop;
  }.bind(this);

  this._update_scroll_bound = function()
  {
    if (this._current_scroll === null)
    {
      this._container.scrollTop = 9999999;
    }
  }.bind(this);

  /**
   * Pulls all the available, non-rendered, events from the data
   * object and renders them
   */
  this._update = function()
  {
    var now = new Date().getTime();
    var entries = this._data.get_log(this._lastupdate);
    this._lastupdate = now;

    for (var n=0, e; e=entries[n]; n++)
    {
      switch(e.type) {
        case "input":
          this._render_input(e.data);
          break;
        case "string":
          this._render_string(e.data);
          break;
        case "exception":
          this._render_error(e.data);
          break;
        case "iobj":
          this._render_inspectable_object(e.data);
          break;
        case "iele":
          this._render_inspectable_element(e.data);
          break;
        case "pobj":
          this._render_pointer_to_object(e.data);
          break;
        case "valuelist":
          this._render_value_list(e.data);
          break;
        case "trace":
          this._render_trace(e.data);
          break;
        case "groupstart":
          this._render_groupstart(e.data);
          break;
        case "groupend":
          this._render_groupend();
          break;
        case "count":
        this._render_count(e.data);
          break;
      default:
          this._render_string("unknown");
      }
    }
  };

  this._render_count = function(data)
  {
    this._render_string((data.label ? data.label + ": " : "") + data.count);
  };

  this._render_groupstart = function(data)
  {
    this._add_line([["button", "", "class", "folder-key"+(data.collapsed ? "" : " open" ),
                                   "handler", "repl-toggle-group"
                    ],
                    data.name]);
    var ol = document.createElement("ol");
    ol.className="repl-lines";
    this._add_line(ol);
    if (data.collapsed) {
      ol.parentNode.style.display = "none";
    }

    this._linelist = ol;
  };

  this._render_groupend = function()
  {
    if (this._linelist.parentNode.parentNode.nodeName.toLowerCase() == "ol")
    {
      this._linelist = this._linelist.parentNode.parentNode;
    }
  };

  this._render_pointer_to_object = function(data)
  {
    this._add_line(templates.repl_output_pobj(data));
  };

  this._render_inspectable_element = function(data)
  {
    if (!data.view) {
      var rt_id = data.rt_id, obj_id=data.obj_id, name=data.name;
      data.view = new cls.InspectableDomNodeView(rt_id, obj_id, name, false);
    }

    if (data.view && !data.view.expanded)
    {
      // re-enter once we have the data.
      data.view.expand(this._render_inspectable_element.bind(this, data));
      return;
    }

    this._add_line(data.view.render());
  };

  this._render_inspectable_object = function(data)
  {
    if (!data.view) {
      var rt_id = data.rt_id, obj_id=data.obj_id, name=data.name;
      data.view = new cls.InspectableObjectView(rt_id, obj_id, name, false);
    }

    if (data.view && !data.view.expanded)
    {
      // re-enter once we have the data.
      data.view.expand(this._render_inspectable_object.bind(this, data));
      return;
    }

    this._add_line(data.view.render());
  };

  this._render_error = function(data)
  {
    this._render_string(data.message, data.stacktrace);
  };

  this._render_trace = function(data)
  {
    this._add_line(templates.repl_output_trace(data));
  };

  this._render_value_list = function(values) {
    var tpl = values.map(templates.repl_output_native_or_pobj);
    var separated = [];
    separated.push(tpl.shift());
    while (tpl.length)
    {
      separated.push(["span", ", "]);
      separated.push(tpl.shift());
    }
    this._add_line(separated);
  };

  /**
   * Render an arbitrary numver of string arguments
   */
  this._render_string = function()
  {
    for (var n=0; n<arguments.length; n++)
    {
      this._add_line(templates.repl_output_native(arguments[n]));
    }
  };

  this._render_input = function(str)
  {
    this._render_string(">>> " + str);
  };

  this.set_current_input = function(str)
  {
    this._textarea.textContent = str;
  };

  this._add_line = function(elem_or_template)
  {
    var line = document.createElement("li");

    if (elem_or_template.nodeType === undefined)
    {
      line.render(elem_or_template);
    }
    else
    {
      line.appendChild(elem_or_template);
    }
    this._linelist.appendChild(line);
  };

  this._handle_keypress_bound = function(evt)
  {
    // opera.postError("" + evt.keyCode + " " + evt.which );
    switch (evt.keyCode) {
      case 9: // tab
      {
        evt.preventDefault();
        if (this._multiediting) {
          var pos = this._textarea.selectionStart;
          this._textarea.value = this._textarea.value.slice(0, pos) + "\t" + this._textarea.value.slice(pos);

          break; // tab should be off when in a multiline box.
        }

        this._resolver.find_props(this._handle_completer.bind(this),
                                  this._textarea.value,
                                  window.stop_at.getSelectedFrame());
        break;
      }
      case 13: // enter
      {
        if (evt.ctrlKey)
        {
          this._multiediting = true;
          this._textarea.rows = this._textarea.rows + 1;
          this._textarea.value = this._textarea.value + "\n";
        }
        else
        {
          // stop it from adding a newline just before processing. Looks strange

          evt.preventDefault();

          var input = this._textarea.value;
          input = input.trim();
          this._textarea.value = "";
          this._multiediting = false;
          this._backlog_index = -1;
          this._current_input = "";

          if (input == "") {
            this._render_input("");
            return;
          }
          this._service.handle_input(input);
        }
        break;
      }
      case 38: // up and down. maybe. See DSK-246193
      case 40:
      {
        if (this._multiediting) {
          break; // history editing should be off when in a multiline box.
        }
        // workaround as long as we don't have support for keyIdentifier
        // event.which is 0 in a keypress event for function keys
        if( !evt.which )
        {
          evt.preventDefault();
          this._handle_backlog(evt.keyCode == 38 ? 1 : -1);
        }
        break;
      }
      case 97: // a key. ctrl-a == move to start of line
      {
        if (evt.ctrlKey) {
          evt.preventDefault();
          this._textarea.selectionStart = 0;
          this._textarea.selectionEnd = 0;
        }
        break;
      }
      case 107: // k key. ctrl-k == kill to en of line
      {
        if (evt.ctrlKey) {
          evt.preventDefault();
          var pos = this._textarea.selectionStart;
          this._textarea.value = this._textarea.value.slice(0, pos);
        }
        break;
      }
      case 108: // l key
      {
        if (evt.ctrlKey) {
          evt.preventDefault();
          this.clear();
          this._data.clear();
        }
        break;
      }
      case 119: // w key. ctrl-w == kill word backwards
      {
        if (evt.ctrlKey) {
          evt.preventDefault();
          var s = this._textarea.value.slice(0, this._textarea.selectionStart-1);
          var pos = s.lastIndexOf(" ");
          pos++;

          this._textarea.value = this._textarea.value.slice(0, pos) + this._textarea.value.slice(this._textarea.selectionStart);
          this._textarea.selectionStart = pos;
          this._textarea.selectionEnd = pos;
        }
        break;
      }

    }

    // timeout makes sure we do this after all events have fired to update box
    window.setTimeout(this._update_input_height_bound, 0);

  }.bind(this);

  this._handle_backlog = function(delta)
  {
    this._set_input_from_backlog(this._backlog_index + delta);
  };

  this._set_input_from_backlog = function(index)
  {
    if (index <= -1)
    {
      this._backlog_index = -1;
      this._textarea.value = this._current_input;
      return;
    }

    if (this._backlog_index == -1)
    {
      this._current_input = this._textarea.value;
    }

    var log = this._data.get_typed_history();
    this._backlog_index = Math.min(index, log.length-1);
    var entry = log[this._backlog_index];

    if (entry != undefined)
    {
      this._textarea.value = entry;
    }
  };

  this._handle_completer = function(props)
  {
    var localpart = props.identifier;

    var matches = props.props.filter(function(e) {
      return e.indexOf(localpart) == 0;
    });

    if (! matches.length) {
      return;
    }

    var match = this._longest_common_prefix(matches.slice(0));
    if (match.length > localpart.length)
    {
      var pos = this._textarea.value.lastIndexOf(localpart);
      this._textarea.value = this._textarea.value.slice(0, pos) + match;
    }
    else
    {
      this._render_input(this._textarea.value);
      this._render_string(matches.sort().join(", "));
    }

  };

  /**
   * Return the longest common prefix of all the strings in the array
   * of strings. For example ["foobar", "foobaz", "foomatic"] -> "foo"
   */
  this._longest_common_prefix = function(strings)
  {
    if (strings.length == 0)
    {
      return "";
    }
    else if (strings.length == 1)
    {
      return strings[0];
    }
    else
    {
      var sorted = strings.slice(0).sort();
      var first = sorted.shift();
      var last = sorted.pop();

      for (var n=last.length; n; n--)
      {
        if (first.indexOf(last.slice(0,n)) == 0) { return last.slice(0, n); }
      }
    }
    return "";
  };

  this._handle_repl_frame_select_bound = function(event, target)
  {
    var sourceview = window.views.js_source;
    sourceview.highlight(parseInt(event.srcElement.getAttribute("script-id")),
                         parseInt(event.srcElement.getAttribute("line-number")));

    messages.post("trace-frame-selected", {rt_id: parseInt(target.getAttribute("runtime-id")),
                                           obj_id: parseInt(event.srcElement.getAttribute("scope-variable-object-id")),
                                           this_id: parseInt(event.srcElement.getAttribute("this-object-id")),
                                           arg_id: parseInt(event.srcElement.getAttribute("arguments-object-id"))
                                          }
                 );
  }.bind(this);

  this._handle_repl_toggle_group = function(event, target)
  {
    var li = target.parentNode;
    if (target.hasClass("open"))
    {
      target.removeClass("open");
      li.nextSibling.style.display = "none";
    }
    else
    {
      target.addClass("open");
      li.nextSibling.style.display = "";
    }
  };

  this._handle_option_change_bound = function(event, target)
  {
    settings.command_line.set('max-typed-history-length', target.value);
    messages.post("setting-changed", {id: "repl", key: "max-typed-history-length"});
  }.bind(this);

  var eh = window.eventHandlers;
  eh.click["repl-toggle-group"] = this._handle_repl_toggle_group;
  eh.click["select-trace-frame"] = this._handle_repl_frame_select_bound;
  eh.keypress['repl-textarea'] = this._handle_keypress_bound;
  eh.change['set-typed-history-length'] = this._handle_option_change_bound;

  this.init(id, name, container_class, html, default_handler);
};
cls.ReplView.prototype = ViewBase;

cls.ReplView.create_ui_widgets = function()
{

  new Settings(
    'command_line',
    { // key/value
      'max-typed-history-length': 8,
      'typed-history': []
    },
    { // key/label
      'max-typed-history-length': "Max items in typed history to remember"
    },
    { // settings map
      customSettings:
      [
        'max-typed-history-length'
      ]
    },
    {  // custom templates
      'max-typed-history-length':
      function(setting)
      {
        return (
        [
          'setting-composite',
          ['label',
           setting.label_map['max-typed-history-length'] + ': ',
           ['input',
            'type', 'number',
            'handler', 'set-typed-history-length',
            'max', '1000',
            'min', '0',
            'value', setting.get('max-typed-history-length')
           ]
          ]
        ]);
      }
    }
  );

  new ToolbarConfig
  (
    'command_line',
    null,
    null,
    null,
    [
      {
        handler: 'select-window',
        title: ui_strings.S_BUTTON_LABEL_SELECT_WINDOW,
        type: 'dropdown',
        class: 'window-select-dropdown',
        template: window['cst-selects']['cmd-runtime-select'].getTemplate()
      }
    ]
  );

};
<commit_msg>Added some separating text between traces<commit_after>window.cls = window.cls || (window.cls = {});

/**
 * @constructor
 * @extends ViewBase
 */

cls.ReplView = function(id, name, container_class, html, default_handler) {
  this._resolver = new cls.PropertyFinder();
  this._data = new cls.ReplData(this);
  this._service = new cls.ReplService(this, this._data);
  this._linelist = null;
  this._textarea = null;
  this._lastupdate = null;
  this._current_input = "";
  this._current_scroll = null;
  this._container = null;
  this._backlog_index = -1;
  this._input_row_height = null;

  this.ondestroy = function()
  {
    this._lastupdate = 0;
    this._backlog_index = -1;
    this._current_input = this._textarea.value;
  };

  this.createView = function(container)
  {
    var switched_to_view = false;
    if (!this._lastupdate)
    {
      container.clearAndRender(templates.repl_main());
      this._linelist = container.querySelector("ol");
      this._textarea = container.querySelector("textarea");
      this._textarea.value = this._current_input;
      this._container = container;
      this._input_row_height = this._textarea.scrollHeight;
      switched_to_view = true;
      // note: events are bound to handlers at the bottom of this class
    }

    this._update();

    if (switched_to_view)
    {
      // defer adding listeners until after update
      this._container.addEventListener("scroll", this._save_scroll_bound, false);
      this._container.addEventListener("DOMAttrModified", this._update_scroll_bound, false);
      this._container.addEventListener("DOMNodeInserted", this._update_scroll_bound, false);

      if(this._current_scroll === null)
      {
        this._container.scrollTop = 999999;
        // timer works around issue where element is unfocusable when created
        window.setTimeout(function() {this._textarea.focus();}.bind(this), 0);
      }
      else
      {
        this._container.scrollTop = this._current_scroll;
      }
    }

  };

  this.clear = function()
  {
    this.ondestroy();
  };

  this._update_input_height_bound = function()
  {
    this._textarea.rows = Math.max(1, Math.ceil(this._textarea.scrollHeight / this._input_row_height));
  }.bind(this);

  this._save_scroll_bound = function()
  {
    var at_bottom = (this._container.scrollTop + this._container.offsetHeight >= this._container.scrollHeight);
    this._current_scroll = at_bottom ? null : this._container.scrollTop;
  }.bind(this);

  this._update_scroll_bound = function()
  {
    if (this._current_scroll === null)
    {
      this._container.scrollTop = 9999999;
    }
  }.bind(this);

  /**
   * Pulls all the available, non-rendered, events from the data
   * object and renders them
   */
  this._update = function()
  {
    var now = new Date().getTime();
    var entries = this._data.get_log(this._lastupdate);
    this._lastupdate = now;

    for (var n=0, e; e=entries[n]; n++)
    {
      switch(e.type) {
        case "input":
          this._render_input(e.data);
          break;
        case "string":
          this._render_string(e.data);
          break;
        case "exception":
          this._render_error(e.data);
          break;
        case "iobj":
          this._render_inspectable_object(e.data);
          break;
        case "iele":
          this._render_inspectable_element(e.data);
          break;
        case "pobj":
          this._render_pointer_to_object(e.data);
          break;
        case "valuelist":
          this._render_value_list(e.data);
          break;
        case "trace":
          this._render_trace(e.data);
          break;
        case "groupstart":
          this._render_groupstart(e.data);
          break;
        case "groupend":
          this._render_groupend();
          break;
        case "count":
        this._render_count(e.data);
          break;
      default:
          this._render_string("unknown");
      }
    }
  };

  this._render_count = function(data)
  {
    this._render_string((data.label ? data.label + ": " : "") + data.count);
  };

  this._render_groupstart = function(data)
  {
    this._add_line([["button", "", "class", "folder-key"+(data.collapsed ? "" : " open" ),
                                   "handler", "repl-toggle-group"
                    ],
                    data.name]);
    var ol = document.createElement("ol");
    ol.className="repl-lines";
    this._add_line(ol);
    if (data.collapsed) {
      ol.parentNode.style.display = "none";
    }

    this._linelist = ol;
  };

  this._render_groupend = function()
  {
    if (this._linelist.parentNode.parentNode.nodeName.toLowerCase() == "ol")
    {
      this._linelist = this._linelist.parentNode.parentNode;
    }
  };

  this._render_pointer_to_object = function(data)
  {
    this._add_line(templates.repl_output_pobj(data));
  };

  this._render_inspectable_element = function(data)
  {
    if (!data.view) {
      var rt_id = data.rt_id, obj_id=data.obj_id, name=data.name;
      data.view = new cls.InspectableDomNodeView(rt_id, obj_id, name, false);
    }

    if (data.view && !data.view.expanded)
    {
      // re-enter once we have the data.
      data.view.expand(this._render_inspectable_element.bind(this, data));
      return;
    }

    this._add_line(data.view.render());
  };

  this._render_inspectable_object = function(data)
  {
    if (!data.view) {
      var rt_id = data.rt_id, obj_id=data.obj_id, name=data.name;
      data.view = new cls.InspectableObjectView(rt_id, obj_id, name, false);
    }

    if (data.view && !data.view.expanded)
    {
      // re-enter once we have the data.
      data.view.expand(this._render_inspectable_object.bind(this, data));
      return;
    }

    this._add_line(data.view.render());
  };

  this._render_error = function(data)
  {
    this._render_string(data.message, data.stacktrace);
  };

  this._render_trace = function(data)
  {
    this._add_line("console.trace:");
    this._add_line(templates.repl_output_trace(data));
  };

  this._render_value_list = function(values) {
    var tpl = values.map(templates.repl_output_native_or_pobj);
    var separated = [];
    separated.push(tpl.shift());
    while (tpl.length)
    {
      separated.push(["span", ", "]);
      separated.push(tpl.shift());
    }
    this._add_line(separated);
  };

  /**
   * Render an arbitrary numver of string arguments
   */
  this._render_string = function()
  {
    for (var n=0; n<arguments.length; n++)
    {
      this._add_line(templates.repl_output_native(arguments[n]));
    }
  };

  this._render_input = function(str)
  {
    this._render_string(">>> " + str);
  };

  this.set_current_input = function(str)
  {
    this._textarea.textContent = str;
  };

  this._add_line = function(elem_or_template)
  {
    var line = document.createElement("li");

    if (elem_or_template.nodeType === undefined)
    {
      line.render(elem_or_template);
    }
    else
    {
      line.appendChild(elem_or_template);
    }
    this._linelist.appendChild(line);
  };

  this._handle_keypress_bound = function(evt)
  {
    // opera.postError("" + evt.keyCode + " " + evt.which );
    switch (evt.keyCode) {
      case 9: // tab
      {
        evt.preventDefault();
        if (this._multiediting) {
          var pos = this._textarea.selectionStart;
          this._textarea.value = this._textarea.value.slice(0, pos) + "\t" + this._textarea.value.slice(pos);

          break; // tab should be off when in a multiline box.
        }

        this._resolver.find_props(this._handle_completer.bind(this),
                                  this._textarea.value,
                                  window.stop_at.getSelectedFrame());
        break;
      }
      case 13: // enter
      {
        if (evt.ctrlKey)
        {
          this._multiediting = true;
          this._textarea.rows = this._textarea.rows + 1;
          this._textarea.value = this._textarea.value + "\n";
        }
        else
        {
          // stop it from adding a newline just before processing. Looks strange

          evt.preventDefault();

          var input = this._textarea.value;
          input = input.trim();
          this._textarea.value = "";
          this._multiediting = false;
          this._backlog_index = -1;
          this._current_input = "";

          if (input == "") {
            this._render_input("");
            return;
          }
          this._service.handle_input(input);
        }
        break;
      }
      case 38: // up and down. maybe. See DSK-246193
      case 40:
      {
        if (this._multiediting) {
          break; // history editing should be off when in a multiline box.
        }
        // workaround as long as we don't have support for keyIdentifier
        // event.which is 0 in a keypress event for function keys
        if( !evt.which )
        {
          evt.preventDefault();
          this._handle_backlog(evt.keyCode == 38 ? 1 : -1);
        }
        break;
      }
      case 97: // a key. ctrl-a == move to start of line
      {
        if (evt.ctrlKey) {
          evt.preventDefault();
          this._textarea.selectionStart = 0;
          this._textarea.selectionEnd = 0;
        }
        break;
      }
      case 107: // k key. ctrl-k == kill to en of line
      {
        if (evt.ctrlKey) {
          evt.preventDefault();
          var pos = this._textarea.selectionStart;
          this._textarea.value = this._textarea.value.slice(0, pos);
        }
        break;
      }
      case 108: // l key
      {
        if (evt.ctrlKey) {
          evt.preventDefault();
          this.clear();
          this._data.clear();
        }
        break;
      }
      case 119: // w key. ctrl-w == kill word backwards
      {
        if (evt.ctrlKey) {
          evt.preventDefault();
          var s = this._textarea.value.slice(0, this._textarea.selectionStart-1);
          var pos = s.lastIndexOf(" ");
          pos++;

          this._textarea.value = this._textarea.value.slice(0, pos) + this._textarea.value.slice(this._textarea.selectionStart);
          this._textarea.selectionStart = pos;
          this._textarea.selectionEnd = pos;
        }
        break;
      }

    }

    // timeout makes sure we do this after all events have fired to update box
    window.setTimeout(this._update_input_height_bound, 0);

  }.bind(this);

  this._handle_backlog = function(delta)
  {
    this._set_input_from_backlog(this._backlog_index + delta);
  };

  this._set_input_from_backlog = function(index)
  {
    if (index <= -1)
    {
      this._backlog_index = -1;
      this._textarea.value = this._current_input;
      return;
    }

    if (this._backlog_index == -1)
    {
      this._current_input = this._textarea.value;
    }

    var log = this._data.get_typed_history();
    this._backlog_index = Math.min(index, log.length-1);
    var entry = log[this._backlog_index];

    if (entry != undefined)
    {
      this._textarea.value = entry;
    }
  };

  this._handle_completer = function(props)
  {
    var localpart = props.identifier;

    var matches = props.props.filter(function(e) {
      return e.indexOf(localpart) == 0;
    });

    if (! matches.length) {
      return;
    }

    var match = this._longest_common_prefix(matches.slice(0));
    if (match.length > localpart.length)
    {
      var pos = this._textarea.value.lastIndexOf(localpart);
      this._textarea.value = this._textarea.value.slice(0, pos) + match;
    }
    else
    {
      this._render_input(this._textarea.value);
      this._render_string(matches.sort().join(", "));
    }

  };

  /**
   * Return the longest common prefix of all the strings in the array
   * of strings. For example ["foobar", "foobaz", "foomatic"] -> "foo"
   */
  this._longest_common_prefix = function(strings)
  {
    if (strings.length == 0)
    {
      return "";
    }
    else if (strings.length == 1)
    {
      return strings[0];
    }
    else
    {
      var sorted = strings.slice(0).sort();
      var first = sorted.shift();
      var last = sorted.pop();

      for (var n=last.length; n; n--)
      {
        if (first.indexOf(last.slice(0,n)) == 0) { return last.slice(0, n); }
      }
    }
    return "";
  };

  this._handle_repl_frame_select_bound = function(event, target)
  {
    var sourceview = window.views.js_source;
    sourceview.highlight(parseInt(event.srcElement.getAttribute("script-id")),
                         parseInt(event.srcElement.getAttribute("line-number")));

    messages.post("trace-frame-selected", {rt_id: parseInt(target.getAttribute("runtime-id")),
                                           obj_id: parseInt(event.srcElement.getAttribute("scope-variable-object-id")),
                                           this_id: parseInt(event.srcElement.getAttribute("this-object-id")),
                                           arg_id: parseInt(event.srcElement.getAttribute("arguments-object-id"))
                                          }
                 );
  }.bind(this);

  this._handle_repl_toggle_group = function(event, target)
  {
    var li = target.parentNode;
    if (target.hasClass("open"))
    {
      target.removeClass("open");
      li.nextSibling.style.display = "none";
    }
    else
    {
      target.addClass("open");
      li.nextSibling.style.display = "";
    }
  };

  this._handle_option_change_bound = function(event, target)
  {
    settings.command_line.set('max-typed-history-length', target.value);
    messages.post("setting-changed", {id: "repl", key: "max-typed-history-length"});
  }.bind(this);

  var eh = window.eventHandlers;
  eh.click["repl-toggle-group"] = this._handle_repl_toggle_group;
  eh.click["select-trace-frame"] = this._handle_repl_frame_select_bound;
  eh.keypress['repl-textarea'] = this._handle_keypress_bound;
  eh.change['set-typed-history-length'] = this._handle_option_change_bound;

  this.init(id, name, container_class, html, default_handler);
};
cls.ReplView.prototype = ViewBase;

cls.ReplView.create_ui_widgets = function()
{

  new Settings(
    'command_line',
    { // key/value
      'max-typed-history-length': 8,
      'typed-history': []
    },
    { // key/label
      'max-typed-history-length': "Max items in typed history to remember"
    },
    { // settings map
      customSettings:
      [
        'max-typed-history-length'
      ]
    },
    {  // custom templates
      'max-typed-history-length':
      function(setting)
      {
        return (
        [
          'setting-composite',
          ['label',
           setting.label_map['max-typed-history-length'] + ': ',
           ['input',
            'type', 'number',
            'handler', 'set-typed-history-length',
            'max', '1000',
            'min', '0',
            'value', setting.get('max-typed-history-length')
           ]
          ]
        ]);
      }
    }
  );

  new ToolbarConfig
  (
    'command_line',
    null,
    null,
    null,
    [
      {
        handler: 'select-window',
        title: ui_strings.S_BUTTON_LABEL_SELECT_WINDOW,
        type: 'dropdown',
        class: 'window-select-dropdown',
        template: window['cst-selects']['cmd-runtime-select'].getTemplate()
      }
    ]
  );

};
<|endoftext|>


<commit_before>(function () {
	var _options = JSON.parse(document.querySelector('body').dataset.options);
	var _oidToken, _uploadUrl, _contentsCss;

	window.addEventListener('load', replaceEditor);
	document.addEventListener('voe.openLightBox', replaceEditor);
	document.addEventListener('voe.refresh', replaceEditor);

	function replaceEditor() {
		if (!window.tinyMCE || !tinyMCE.editors.length) return;

		_oidToken = tinyMCE.settings.oidToken;
		_uploadUrl = tinyMCE.settings.postUrl;
		_contentsCss = tinyMCE.settings.content_css;

		if (window.CKEDITOR) {
			replace();
			return;
		}

		window.CKEDITOR_BASEPATH = _options.baseUrl + 'ckeditor4/';
		injectScript(_options.baseUrl + 'ckeditor4/ckeditor.js', function () {
			CKEDITOR.disableAutoInline = true;

			var interval = setInterval(function () {
				if (CKEDITOR.status === 'loaded') {
					clearInterval(interval);
					replace();
				}
			}, 100);
		});
	}

	function replace() {
		for (var i = 0, l = tinyMCE.editors.length; i < l; i++) {
			(function () {
				var textarea = tinyMCE.editors[0].getElement();
				var textareaContainer = textarea.parentNode;

				tinyMCE.editors[0].destroy();

				if (textareaContainer.classList.contains('inline-edit-content')) {
					var button = addButtonForEditing(textarea.id);

					textarea.setAttribute('contenteditable', 'false');

					button.addEventListener('click', function() {
						textarea.setAttribute('contenteditable', 'true');
						initCKEditor(textarea, {
							blur: function (e) {
								var id = this.element.$.id;

								$('.main-panel-scroller').off('scroll');

								textarea.setAttribute('contenteditable', 'false');
								e.editor.destroy();

								V1.Topics.Publish('RichText/Blur', {
									target: {id: id}
								});
							},
							key: function (event) {
								V1.Topics.Publish('RichText/Keyup', event.data.domEvent.$);
							},
							instanceReady: function (e) {
								setupFloatPanel(e);

								textarea.focus();

								e.editor.on('simpleuploads.startUpload', function (ev) {
									ev.data.extraFields = {
										oidToken: oidToken
									};
								});

								e.editor.on('simpleuploads.serverResponse', function (ev) {
									ev.data.url = ev.data.xhr.responseText.match(/"Url":"([^"]*)/i)[1];
								});
							}
						}, true);
					});
				} else {
					initCKEditor(textarea, {
						change: function () {
							this.updateElement();
							V1.Html.Event.Fire(this.element.$, "change");
						},
						instanceReady: function (e) {
							e.editor.on('simpleuploads.startUpload', function (ev) {
								ev.data.extraFields = {
									oidToken: _oidToken
								};
							});

							window.editor = e.editor;

							e.editor.on('simpleuploads.serverResponse', function (ev) {
								ev.data.url = ev.data.xhr.responseText.match(/"Url":"([^"]*)/i)[1];
							});

							e.editor.on('simpleuploads.finishedUpload', function () {
								this.updateElement();
								V1.Html.Event.Fire(this.element.$, "change");
							});
						}
					});
				}
			}());
		}
	}

	function setupFloatPanel(e) {
		var $editor = $(e.editor.element.$);
		var $panel = $(document.getElementById('cke_' + $editor.attr('id')));

		$panel.css('maxWidth', $editor.outerWidth() + 'px');

		var $scroller = $('.main-panel-scroller');
		var panelHeight = $panel.outerHeight();
		var editorHeight = $editor.outerHeight();
		var scrollerOffset = $scroller.offset().top;

		$panel.offset({
			top: $editor.offset().top - $panel.outerHeight(),
			left: $editor.offset().left
		});

		setPanelPosition();
		$scroller.on('scroll', function () {
			debouncedSetPanelPosition();
		});

		function debounce(f, ms) {

			var timer = null;

			return function (...args) {
				const onComplete = () => {
					f.apply(this, args);
					timer = null;
				}

				if (timer) {
					clearTimeout(timer);
				}

				timer = setTimeout(onComplete, ms);
			};
		}

		var debouncedSetPanelPosition = debounce(setPanelPosition, 50);

		function setPanelPosition() {
			var editorOffset = $editor.offset().top;
			var editorOffsetBottom = editorOffset + editorHeight;
			var panelOffset = editorOffset - panelHeight;

			if (panelOffset <= scrollerOffset && editorOffsetBottom >= scrollerOffset) {
				$panel.animate({top: scrollerOffset, left: $editor.offset().left, opacity: 1}, 150);
			} else if (editorOffsetBottom < scrollerOffset) {
				$panel.animate({top: scrollerOffset, left: $editor.offset().left, opacity: 0}, 150);
			} else {
				$panel.animate({top: panelOffset, left: $editor.offset().left, opacity: 1}, 150);
			}
		}
	}

	function addButtonForEditing(id) {
		var wrapper = document.createElement('div');
		wrapper.classList.add('voe-inline-editor-button-wrapper');
		wrapper.innerHTML = '<button class="voe-inline-editor-button">Do you wanna edit this shit?</button>';
		document.querySelector('label[for="' + id + '"]').parentNode.prepend(wrapper);

		return wrapper.querySelector('button');
	}

	function initCKEditor(element, on, isInline) {
		var config = {
			height: _options.minHeightTinyMCE,
			autoGrow_minHeight: _options.minHeightTinyMCE,
			autoGrow_maxHeight: _options.maxHeightTinyMCE,
			filebrowserUploadUrl: _uploadUrl,
			filebrowserImageUploadUrl: _uploadUrl,
			coreStyles_strike: {
				element: 'span',
				attributes: {'style': 'text-decoration: line-through;'},
				overrides: 'strike'
			},
			stylesSet: [
				{name: 'Red text', element: 'span', styles: {'color': 'red'}},
				{name: 'Blue text', element: 'span', styles: {'color': 'blue'}},
				{name: 'Green text', element: 'span', styles: {'color': 'green'}},
				{name: 'Yellow highlight', element: 'span', styles: {'background-color': 'yellow'}},
				{name: 'Light green highlight', element: 'span', styles: {'background-color': 'lightgreen'}},
				{name: 'Light blue highlight', element: 'span', styles: {'background-color': 'lightblue'}}
			],
			extraPlugins: 'simpleuploads,justify,autogrow,keystrokes,notification',
			simpleuploads_inputname: 'image',
			toolbarGroups: [
				{name: 'clipboard', groups: ['clipboard', 'undo']},
				{name: 'basicstyles', groups: ['basicstyles', 'cleanup']},
				{name: 'paragraph', groups: ['list', 'indent', 'blocks', 'align', 'bidi', 'paragraph']},
				{name: 'editing', groups: ['find', 'selection', 'spellchecker', 'editing']},
				{name: 'links', groups: ['links']},
				{name: 'insert', groups: ['insert', 'addImage']},
				{name: 'forms', groups: ['forms']},
				{name: 'document', groups: ['mode', 'document', 'doctools']},
				{name: 'others', groups: ['others']},
				{name: 'styles', groups: ['styles']},
				{name: 'colors', groups: ['colors']},
				{name: 'about', groups: ['about']},
				{name: 'tools', groups: ['tools']}
			],
			removeButtons: 'Subscript,Superscript,Image,Source,Blockquote,About,addFile',
			on: on || {}
		};

		if (isInline) {
			CKEDITOR.inline(element, config);
		} else {
			config.contentsCss = [_options.baseUrl + 'proximaNova.css', _options.baseUrl + 'typography.css'];
			CKEDITOR.replace(element, config);
		}
	}
})();
<commit_msg>fix small bug<commit_after>(function () {
	var _options = JSON.parse(document.querySelector('body').dataset.options);
	var _oidToken, _uploadUrl, _contentsCss;

	window.addEventListener('load', replaceEditor);
	document.addEventListener('voe.openLightBox', replaceEditor);
	document.addEventListener('voe.refresh', replaceEditor);

	function replaceEditor() {
		if (!window.tinyMCE || !tinyMCE.editors.length) return;

		_oidToken = tinyMCE.settings.oidToken;
		_uploadUrl = tinyMCE.settings.postUrl;
		_contentsCss = tinyMCE.settings.content_css;

		if (window.CKEDITOR) {
			replace();
			return;
		}

		window.CKEDITOR_BASEPATH = _options.baseUrl + 'ckeditor4/';
		injectScript(_options.baseUrl + 'ckeditor4/ckeditor.js', function () {
			CKEDITOR.disableAutoInline = true;

			var interval = setInterval(function () {
				if (CKEDITOR.status === 'loaded') {
					clearInterval(interval);
					replace();
				}
			}, 100);
		});
	}

	function replace() {
		for (var i = 0, l = tinyMCE.editors.length; i < l; i++) {
			(function () {
				var textarea = tinyMCE.editors[0].getElement();
				var textareaContainer = textarea.parentNode;

				tinyMCE.editors[0].destroy();

				if (textareaContainer.classList.contains('inline-edit-content')) {
					var button = addButtonForEditing(textarea.id);

					textarea.setAttribute('contenteditable', 'false');

					button.addEventListener('click', function() {
						textarea.setAttribute('contenteditable', 'true');
						initCKEditor(textarea, {
							blur: function (e) {
								var id = this.element.$.id;

								$('.main-panel-scroller').off('scroll');

								textarea.setAttribute('contenteditable', 'false');
								e.editor.destroy();

								V1.Topics.Publish('RichText/Blur', {
									target: {id: id}
								});
							},
							key: function (event) {
								V1.Topics.Publish('RichText/Keyup', event.data.domEvent.$);
							},
							instanceReady: function (e) {
								setupFloatPanel(e);

								textarea.focus();

								e.editor.on('simpleuploads.startUpload', function (ev) {
									ev.data.extraFields = {
										oidToken: _oidToken
									};
								});

								e.editor.on('simpleuploads.serverResponse', function (ev) {
									ev.data.url = ev.data.xhr.responseText.match(/"Url":"([^"]*)/i)[1];
								});
							}
						}, true);
					});
				} else {
					initCKEditor(textarea, {
						change: function () {
							this.updateElement();
							V1.Html.Event.Fire(this.element.$, "change");
						},
						instanceReady: function (e) {
							e.editor.on('simpleuploads.startUpload', function (ev) {
								ev.data.extraFields = {
									oidToken: _oidToken
								};
							});

							window.editor = e.editor;

							e.editor.on('simpleuploads.serverResponse', function (ev) {
								ev.data.url = ev.data.xhr.responseText.match(/"Url":"([^"]*)/i)[1];
							});

							e.editor.on('simpleuploads.finishedUpload', function () {
								this.updateElement();
								V1.Html.Event.Fire(this.element.$, "change");
							});
						}
					});
				}
			}());
		}
	}

	function setupFloatPanel(e) {
		var $editor = $(e.editor.element.$);
		var $panel = $(document.getElementById('cke_' + $editor.attr('id')));

		$panel.css('maxWidth', $editor.outerWidth() + 'px');

		var $scroller = $('.main-panel-scroller');
		var panelHeight = $panel.outerHeight();
		var editorHeight = $editor.outerHeight();
		var scrollerOffset = $scroller.offset().top;

		$panel.offset({
			top: $editor.offset().top - $panel.outerHeight(),
			left: $editor.offset().left
		});

		setPanelPosition();
		$scroller.on('scroll', function () {
			debouncedSetPanelPosition();
		});

		function debounce(f, ms) {

			var timer = null;

			return function (...args) {
				const onComplete = () => {
					f.apply(this, args);
					timer = null;
				}

				if (timer) {
					clearTimeout(timer);
				}

				timer = setTimeout(onComplete, ms);
			};
		}

		var debouncedSetPanelPosition = debounce(setPanelPosition, 50);

		function setPanelPosition() {
			var editorOffset = $editor.offset().top;
			var editorOffsetBottom = editorOffset + editorHeight;
			var panelOffset = editorOffset - panelHeight;

			if (panelOffset <= scrollerOffset && editorOffsetBottom >= scrollerOffset) {
				$panel.animate({top: scrollerOffset, left: $editor.offset().left, opacity: 1}, 150);
			} else if (editorOffsetBottom < scrollerOffset) {
				$panel.animate({top: scrollerOffset, left: $editor.offset().left, opacity: 0}, 150);
			} else {
				$panel.animate({top: panelOffset, left: $editor.offset().left, opacity: 1}, 150);
			}
		}
	}

	function addButtonForEditing(id) {
		var wrapper = document.createElement('div');
		wrapper.classList.add('voe-inline-editor-button-wrapper');
		wrapper.innerHTML = '<button class="voe-inline-editor-button">Do you wanna edit this shit?</button>';
		document.querySelector('label[for="' + id + '"]').parentNode.prepend(wrapper);

		return wrapper.querySelector('button');
	}

	function initCKEditor(element, on, isInline) {
		var config = {
			height: _options.minHeightTinyMCE,
			autoGrow_minHeight: _options.minHeightTinyMCE,
			autoGrow_maxHeight: _options.maxHeightTinyMCE,
			filebrowserUploadUrl: _uploadUrl,
			filebrowserImageUploadUrl: _uploadUrl,
			coreStyles_strike: {
				element: 'span',
				attributes: {'style': 'text-decoration: line-through;'},
				overrides: 'strike'
			},
			stylesSet: [
				{name: 'Red text', element: 'span', styles: {'color': 'red'}},
				{name: 'Blue text', element: 'span', styles: {'color': 'blue'}},
				{name: 'Green text', element: 'span', styles: {'color': 'green'}},
				{name: 'Yellow highlight', element: 'span', styles: {'background-color': 'yellow'}},
				{name: 'Light green highlight', element: 'span', styles: {'background-color': 'lightgreen'}},
				{name: 'Light blue highlight', element: 'span', styles: {'background-color': 'lightblue'}}
			],
			extraPlugins: 'simpleuploads,justify,autogrow,keystrokes,notification',
			simpleuploads_inputname: 'image',
			toolbarGroups: [
				{name: 'clipboard', groups: ['clipboard', 'undo']},
				{name: 'basicstyles', groups: ['basicstyles', 'cleanup']},
				{name: 'paragraph', groups: ['list', 'indent', 'blocks', 'align', 'bidi', 'paragraph']},
				{name: 'editing', groups: ['find', 'selection', 'spellchecker', 'editing']},
				{name: 'links', groups: ['links']},
				{name: 'insert', groups: ['insert', 'addImage']},
				{name: 'forms', groups: ['forms']},
				{name: 'document', groups: ['mode', 'document', 'doctools']},
				{name: 'others', groups: ['others']},
				{name: 'styles', groups: ['styles']},
				{name: 'colors', groups: ['colors']},
				{name: 'about', groups: ['about']},
				{name: 'tools', groups: ['tools']}
			],
			removeButtons: 'Subscript,Superscript,Image,Source,Blockquote,About,addFile',
			on: on || {}
		};

		if (isInline) {
			CKEDITOR.inline(element, config);
		} else {
			config.contentsCss = [_options.baseUrl + 'proximaNova.css', _options.baseUrl + 'typography.css'];
			CKEDITOR.replace(element, config);
		}
	}
})();
<|endoftext|>


<commit_before>import Resource from './Resource'
import md5 from 'md5-hex'
import moment from 'moment'
import random from 'rndm'
import isArray from 'lodash/isArray'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import isPlainObject from 'lodash/isPlainObject'

export default class Cart extends Resource {
  constructor (api, data) {
    super(api)
    this.update(data)
    this.possibleVouchers_ = {}
  }

  /**
   * Update
   *
   * @param object data
   * @return Cart
   */
  update (data) {
    this.data = data || {}
    this.data.spaces = this.data.spaces || {}
    return this
  }

  /**
   * Create a new shopping cart instance.
   *
   * @return Cart
   */
  newInstance () {
    return new this.constructor(this.api)
  }

  setGroup (groupId) {
    this.data.group = groupId
    return this
  }

  getGroup () {
    return this.data.group || null
  }

  setVoucherCode (code) {
    this.data.voucherCode = code
    return this
  }

  getVoucherCode () {
    return this.data.voucherCode
  }

  possibleVouchers () {
    return this.possibleVouchers_
  }

  spaces () {
    return this.data.spaces
  }

  addSpace (space) {
    space = new Space(this, space)
    this.data.spaces[space.hash()] = space
    return this
  }

  getSpace (hash) {
    return this.data.spaces[hash] || null
  }

  removeSpace (hash) {
    delete this.data.spaces[hash]
    return this
  }

  batchCheckData () {
    const batch = []
    let bookingIndex = 0

    // Spaces
    forEach(this.spaces(), (space) => {
      // Add space check
      batch.push({ bookingIndex, ...space.checkData() })

      // Add space products
      forEach(space.products(), (product) =>
        batch.push({ bookingIndex, ...product.checkData() })
      )

      ++bookingIndex
    })

    return batch
  }

  async refresh (ignoreBooking = null) {
    const batch = this.batchCheckData()
    const { voucherCodes, results } = await this.request({
      method: 'POST',
      path: 'booking/batchCheck',
      params: {
        group: this.getGroup(),
        voucherCode: this.getVoucherCode(),
        ignoreBooking: ignoreBooking && ignoreBooking.id || null,
        action: ignoreBooking && ignoreBooking.action || null,
        batch
      }
    })

    // Vouchers
    this.possibleVouchers_ = {}
    for (let i = 0; i < voucherCodes.length; ++i) {
      this.possibleVouchers_[`c${voucherCodes[i].code}`] = voucherCodes[i]
    }
    if (!this.getVoucherCode() && voucherCodes.length > 0) {
      this.setVoucherCode(voucherCodes[0].code)

      // Refresh with first voucher
      await this.refresh(ignoreBooking)
      return
    }

    // Update items
    forEach(this.spaces(), (space) => {
      // Update space
      if (space.hash() in results) {
        space.update({
          ...space.data,
          ...results[space.hash()]
        })
      }

      forEach(space.products(), (product) => {
        // Update product
        if (product.hash() in results) {
          product.update({
            ...product.data,
            ...results[product.hash()]
          })
        }
      })
    })
  }
}

export class Item {
  constructor (cart, data) {
    this.cart = cart
    this.update(data)
  }

  static generateHash () {
    return `h${md5(`${random(64)}-${new Date().getTime()}`)}`
  }

  hash () {
    return this.data.hash
  }

  update (data) {
    // Set data
    if (data instanceof Item) data = data.data
    this.data = data || {}

    // Make sure it has an unique hash
    if (!this.data.hash) this.data.hash = this.constructor.generateHash()

    // Make sure it has a quantity
    if (!this.data.quantity) this.data.quantity = 1

    // Normalize start/end
    this.data.start = normalizeDate(this.data.start)
    this.data.end = normalizeDate(this.data.end)

    return this
  }

  meta () {
    return this.data.meta || {}
  }

  start () {
    return this.data.start || null
  }

  end () {
    return this.data.end || null
  }

  checkData () {
    return {
      id: this.data.hash,
      quantity: this.data.quantity,
      start: unix(this.data.start),
      end: unix(this.data.end)
    }
  }

  export () {
    return {
      ...this.data,
      start: unix(this.data.start),
      end: unix(this.data.end)
    }
  }

  available () {
    return (
      this.data.ranges &&
      this.data.ranges.length > 0 &&
      this.start() &&
      this.end() &&
      this.data.ranges[0].start <= unix(this.start()) &&
      this.data.ranges[0].end >= unix(this.end()) &&
      this.data.ranges[0].quantity >= this.data.quantity
    )
  }
}

export class Product extends Item {
  checkData () {
    return {
      ...super.checkData(),
      productId: this.data.id,
      price: this.data.override || null
    }
  }
}

export class SpaceProduct extends Product {
  constructor (space, data) {
    super(space.cart, data)
    this.space = space
  }

  update (...args) {
    super.update(...args)

    // Copy start / end from space
    this.data.start = this.space.data.start
    this.data.end = this.space.data.end

    return this
  }
}

export class Space extends Item {
  products () {
    return this.data.products
  }

  addProduct (product) {
    product = new SpaceProduct(this, product)
    this.data.products[product.hash()] = product
    return this
  }

  getProduct (hash) {
    return this.data.products[hash] || null
  }

  removeProduct (hash) {
    delete this.data.products[hash]
    return this
  }

  setProducts (products) {
    // Normalize the products object
    this.data.products = {}
    if (isArray(products) || isPlainObject(products)) {
      forEach(products, (product) => {
        product = new SpaceProduct(this, product)
        this.data.products[product.hash()] = product
      })
    }

    return this
  }

  update (...args) {
    super.update(...args)

    // Normalize products
    this.setProducts(this.data.products)

    // Normalize workplace
    if (this.data.workplace) this.data.meta = this.data.workplace
    delete this.data.workplace

    // Normalize people
    const { people, minimum_capacity: min, maximum_capacity: max } = this.data
    if (min && max) this.data.people = Math.max(min, Math.min(max, people))

    return this
  }

  checkData () {
    return {
      ...super.checkData(),
      workplaceId: this.data.id,
      people: this.data.people,
      price: this.data.override || null,
      recurring: !!(this.data.recurringData && this.data.recurringData.enabled)
    }
  }

  export () {
    return {
      ...super.export(),
      products: map(this.products(), (product) => product.export())
    }
  }
}

export const normalizeDate = (date) => date ? moment(date) : null

export const unix = (date) => {
  const normalized = normalizeDate(date)
  return normalized && normalized.unix() || null
}
<commit_msg>Add available helper to cart<commit_after>import Resource from './Resource'
import md5 from 'md5-hex'
import moment from 'moment'
import random from 'rndm'
import isArray from 'lodash/isArray'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import isPlainObject from 'lodash/isPlainObject'

export default class Cart extends Resource {
  constructor (api, data) {
    super(api)
    this.update(data)
    this.possibleVouchers_ = {}
  }

  update (data) {
    this.data = data || {}
    this.data.spaces = this.data.spaces || {}
    return this
  }

  newInstance () {
    return new this.constructor(this.api)
  }

  setGroup (groupId) {
    this.data.group = groupId
    return this
  }

  getGroup () {
    return this.data.group || null
  }

  setVoucherCode (code) {
    this.data.voucherCode = code
    return this
  }

  getVoucherCode () {
    return this.data.voucherCode
  }

  possibleVouchers () {
    return this.possibleVouchers_
  }

  items () {
    const items = []
    forEach(this.data.spaces, (space) => {
      items.push(space)
      forEach(space.products(), (product) => {
        items.push(product)
      })
    })
    return items
  }

  spaces () {
    return this.data.spaces
  }

  addSpace (space) {
    space = new Space(this, space)
    this.data.spaces[space.hash()] = space
    return this
  }

  getSpace (hash) {
    return this.data.spaces[hash] || null
  }

  removeSpace (hash) {
    delete this.data.spaces[hash]
    return this
  }

  batchCheckData () {
    const batch = []
    let bookingIndex = 0

    // Spaces
    forEach(this.spaces(), (space) => {
      // Add space check
      batch.push({ bookingIndex, ...space.checkData() })

      // Add space products
      forEach(space.products(), (product) =>
        batch.push({ bookingIndex, ...product.checkData() })
      )

      ++bookingIndex
    })

    return batch
  }

  async refresh (ignoreBooking = null) {
    const batch = this.batchCheckData()
    const { voucherCodes, results } = await this.request({
      method: 'POST',
      path: 'booking/batchCheck',
      params: {
        group: this.getGroup(),
        voucherCode: this.getVoucherCode(),
        ignoreBooking: ignoreBooking && ignoreBooking.id || null,
        action: ignoreBooking && ignoreBooking.action || null,
        batch
      }
    })

    // Vouchers
    this.possibleVouchers_ = {}
    for (let i = 0; i < voucherCodes.length; ++i) {
      this.possibleVouchers_[`c${voucherCodes[i].code}`] = voucherCodes[i]
    }
    if (!this.getVoucherCode() && voucherCodes.length > 0) {
      this.setVoucherCode(voucherCodes[0].code)

      // Refresh with first voucher
      await this.refresh(ignoreBooking)
      return
    }

    // Update items
    forEach(this.spaces(), (space) => {
      // Update space
      if (space.hash() in results) {
        space.update({
          ...space.data,
          ...results[space.hash()]
        })
      }

      forEach(space.products(), (product) => {
        // Update product
        if (product.hash() in results) {
          product.update({
            ...product.data,
            ...results[product.hash()]
          })
        }
      })
    })
  }

  available () {
    let available = true
    forEach(this.items(), (item) => {
      if (!item.available()) {
        available = false
        return false
      }
    })
    return available
  }
}

export class Item {
  constructor (cart, data) {
    this.cart = cart
    this.update(data)
  }

  static generateHash () {
    return `h${md5(`${random(64)}-${new Date().getTime()}`)}`
  }

  hash () {
    return this.data.hash
  }

  update (data) {
    // Set data
    if (data instanceof Item) data = data.data
    this.data = data || {}

    // Make sure it has an unique hash
    if (!this.data.hash) this.data.hash = this.constructor.generateHash()

    // Make sure it has a quantity
    if (!this.data.quantity) this.data.quantity = 1

    // Normalize start/end
    this.data.start = normalizeDate(this.data.start)
    this.data.end = normalizeDate(this.data.end)

    return this
  }

  meta () {
    return this.data.meta || {}
  }

  start () {
    return this.data.start || null
  }

  end () {
    return this.data.end || null
  }

  checkData () {
    return {
      id: this.data.hash,
      quantity: this.data.quantity,
      start: unix(this.data.start),
      end: unix(this.data.end)
    }
  }

  export () {
    return {
      ...this.data,
      start: unix(this.data.start),
      end: unix(this.data.end)
    }
  }

  available () {
    return (
      this.data.ranges &&
      this.data.ranges.length > 0 &&
      this.start() &&
      this.end() &&
      this.data.ranges[0].start <= unix(this.start()) &&
      this.data.ranges[0].end >= unix(this.end()) &&
      this.data.ranges[0].quantity >= this.data.quantity
    )
  }
}

export class Product extends Item {
  checkData () {
    return {
      ...super.checkData(),
      productId: this.data.id,
      price: this.data.override || null
    }
  }
}

export class SpaceProduct extends Product {
  constructor (space, data) {
    super(space.cart, data)
    this.space = space
  }

  update (...args) {
    super.update(...args)

    // Copy start / end from space
    this.data.start = this.space.data.start
    this.data.end = this.space.data.end

    return this
  }
}

export class Space extends Item {
  products () {
    return this.data.products
  }

  addProduct (product) {
    product = new SpaceProduct(this, product)
    this.data.products[product.hash()] = product
    return this
  }

  getProduct (hash) {
    return this.data.products[hash] || null
  }

  removeProduct (hash) {
    delete this.data.products[hash]
    return this
  }

  setProducts (products) {
    // Normalize the products object
    this.data.products = {}
    if (isArray(products) || isPlainObject(products)) {
      forEach(products, (product) => {
        product = new SpaceProduct(this, product)
        this.data.products[product.hash()] = product
      })
    }

    return this
  }

  update (...args) {
    super.update(...args)

    // Normalize products
    this.setProducts(this.data.products)

    // Normalize workplace
    if (this.data.workplace) this.data.meta = this.data.workplace
    delete this.data.workplace

    // Normalize people
    const { people, minimum_capacity: min, maximum_capacity: max } = this.data
    if (min && max) this.data.people = Math.max(min, Math.min(max, people))

    return this
  }

  checkData () {
    return {
      ...super.checkData(),
      workplaceId: this.data.id,
      people: this.data.people,
      price: this.data.override || null,
      recurring: !!(this.data.recurringData && this.data.recurringData.enabled)
    }
  }

  export () {
    return {
      ...super.export(),
      products: map(this.products(), (product) => product.export())
    }
  }
}

export const normalizeDate = (date) => date ? moment(date) : null

export const unix = (date) => {
  const normalized = normalizeDate(date)
  return normalized && normalized.unix() || null
}
<|endoftext|>


<commit_before>var express = require('express');
var fs = require('fs');
var router = express.Router();
var nodemailer = require('nodemailer');
var jade = require('jade');
var main = require('../../index');
var resumeData = require('../data/resume.json');
var candidateName = resumeData.firstName + ' ' + resumeData.lastName;

/* POST request from contact page by clicking send button to send the name, email and message to gmail account */
router.post('/send', function (req, res) {
    var transporter = nodemailer.createTransport ({ // nodemailer has its own API
        // providing gmail service credential
        service : 'Gmail',
        auth : {
            user : 'sajjad@withpulp.com',
            pass : '****'
        }
    });
    fs.readFile(main.dist + '/views/mail.jade', 'utf8', function (error, data) {
        if (error) {
            throw error;
        }
        var fn = jade.compile(data);
        var html = fn({
            senderName: req.body.name,
            message: req.body.message,
            senderEmail: req.body.email,
            name: candidateName
        });
        // setup e-mail data with unicode symbols
        var mailOptions = {
            from: 'sajjad@withpulp.com',
            to: req.body.email,
            bcc: 'sajjad@withpulp.com',
            subject: 'Website Submission to ' + candidateName,
            text: 'Name: ' + req.body.name + ' Email: ' + req.body.email + ' Message: ' + req.body.message,
            html: html
        };
        transporter.sendMail(mailOptions, function(error) {
            if (error) {
                throw error;
            }
        });
    });
    res.redirect('back');
});

module.exports = router;
<commit_msg>app requires gmail.json<commit_after>var express = require('express');
var fs = require('fs');
var router = express.Router();
var nodemailer = require('nodemailer');
var jade = require('jade');
var main = require('../../index');
var gmail = require('../../gmail.json');
var resumeData = require('../data/resume.json');
var candidateName = resumeData.firstName + ' ' + resumeData.lastName;

/* POST request from contact page by clicking send button to send the name, email and message to gmail account */
router.post('/send', function (req, res) {
    var transporter = nodemailer.createTransport ({ // nodemailer has its own API
        // providing gmail service credential
        service : 'Gmail',
        auth : {
            user : gmail.auth.user,
            pass : gmail.auth.pass
        }
    });
    fs.readFile(main.dist + '/views/mail.jade', 'utf8', function (error, data) {
        if (error) {
            throw error;
        }
        var fn = jade.compile(data);
        var html = fn({
            senderName: req.body.name,
            message: req.body.message,
            senderEmail: req.body.email,
            name: candidateName
        });
        // setup e-mail data with unicode symbols
        var mailOptions = {
            from: gmail.auth.user,
            to: req.body.email,
            bcc: gmail.auth.user,
            subject: 'Website Submission to ' + candidateName,
            text: 'Name: ' + req.body.name + ' Email: ' + req.body.email + ' Message: ' + req.body.message,
            html: html
        };
        transporter.sendMail(mailOptions, function(error) {
            if (error) {
                throw error;
            }
        });
    });
    res.redirect('back');
});

module.exports = router;
<|endoftext|>


<commit_before>// @flow

import url from "url";
import request from "retry-request";
import mixin from "merge-descriptors";
import wildcard from "wildcard-named";
import setPrototypeOf from "setprototypeof";

export default {
  /**
   * Registered callbacks.
   *
   * @type    {object}
   * @access  protected
   */
  callbacks: {},

  /**
   * Add a handler for a specific uri. Accepts wildcards.
   *
   * @param   {string}    uri
   * @return  {Promise}
   * @access  public
   */
  when(uri: string): Promise<*> {
    return new Promise(resolve => {
      this.callbacks[url.resolve(this.base, uri)] = resolve;
    });
  },

  /**
   * Requests a single uri.
   *
   * @param   {string}    uri
   * @return  {Promise}
   * @access  public
   */
  get(uri: string): Promise<*> {
    let link = uri;

    if (!uri.startsWith(this.base)) {
      link = url.resolve(this.base, uri);
    }

    return new Promise((resolve, reject) => {
      request(link, (error, response) => {
        if (error || response.statusCode != 200) {
          reject(error || link);
        }

        const req: Object = {};
        const res: Object = response;

        // Set circular references:
        res.req = req;
        req.res = res;

        // Alter the prototypes:
        setPrototypeOf(req, this.req);
        setPrototypeOf(res, this.res);

        resolve({ req, res, link });
      });
    });
  },

  /**
   * Checks whenever an uri matches the registered callbacks and eventually
   * executes the callback.
   *
   * @param   {string}    uri
   * @param   {Request}   req
   * @param   {Response}  res
   * @return  {void}
   * @access  protected
   */
  check(uri: string, req: Object, res: Object): void {
    for (const index in this.callbacks) {
      if (!this.callbacks.hasOwnPrototpye(index)) {
        continue;
      }

      const requested: Object = wildcard(uri, index);
      const callback: Function = this.callbacks[index];

      if (uri === index || requested) {
        // Merge request parameters with wildcard output:
        mixin(req.params, requested || {});

        callback({ req, res, uri });
      }
    }
  }
};
<commit_msg>Fix typo in hasOwnProperty<commit_after>// @flow

import url from "url";
import request from "retry-request";
import mixin from "merge-descriptors";
import wildcard from "wildcard-named";
import setPrototypeOf from "setprototypeof";

export default {
  /**
   * Registered callbacks.
   *
   * @type    {object}
   * @access  protected
   */
  callbacks: {},

  /**
   * Add a handler for a specific uri. Accepts wildcards.
   *
   * @param   {string}    uri
   * @return  {Promise}
   * @access  public
   */
  when(uri: string): Promise<*> {
    return new Promise(resolve => {
      this.callbacks[url.resolve(this.base, uri)] = resolve;
    });
  },

  /**
   * Requests a single uri.
   *
   * @param   {string}    uri
   * @return  {Promise}
   * @access  public
   */
  get(uri: string): Promise<*> {
    let link = uri;

    if (!uri.startsWith(this.base)) {
      link = url.resolve(this.base, uri);
    }

    return new Promise((resolve, reject) => {
      request(link, (error, response) => {
        if (error || response.statusCode != 200) {
          reject(error || link);
        }

        const req: Object = {};
        const res: Object = response;

        // Set circular references:
        res.req = req;
        req.res = res;

        // Alter the prototypes:
        setPrototypeOf(req, this.req);
        setPrototypeOf(res, this.res);

        resolve({ req, res, link });
      });
    });
  },

  /**
   * Checks whenever an uri matches the registered callbacks and eventually
   * executes the callback.
   *
   * @param   {string}    uri
   * @param   {Request}   req
   * @param   {Response}  res
   * @return  {void}
   * @access  protected
   */
  check(uri: string, req: Object, res: Object): void {
    for (const index in this.callbacks) {
      if (!this.callbacks.hasOwnProperty(index)) {
        continue;
      }

      const requested: Object = wildcard(uri, index);
      const callback: Function = this.callbacks[index];

      if (uri === index || requested) {
        // Merge request parameters with wildcard output:
        mixin(req.params, requested || {});

        callback({ req, res, uri });
      }
    }
  }
};
<|endoftext|>