<commit_before>import { Rectangle, pt } from "lively.graphics";
import { arr } from "lively.lang";
import { inspect } from "lively.morphic";
import { Range } from "../text/range.js";

function todo(name) { throw new Error("not yet implemented " + name)}

export default class TextLayout {

  constructor() {
    this.reset();
  }

  reset() {
    this.resetLineCharBoundsCache();
  }

  resetLineCharBoundsCache(morph) {
    this.lineCharBoundsCache = new WeakMap();
    if (morph) {
      this.estimateLineHeights(morph, true);
      morph.makeDirty();
    }
  }

  resetLineCharBoundsCacheOfLine(line) {
    if (!line) return;
    this.lineCharBoundsCache.set(line, null);
  }

  resetLineCharBoundsCacheOfRow(morph, row) {
    let doc = morph.document;
    doc && this.resetLineCharBoundsCacheOfLine(doc.getLine(row));
  }

  resetLineCharBoundsCacheOfRange(morph, range) {
    for (let row = range.start.row; row <= range.end.row; row++)
      this.resetLineCharBoundsCacheOfRow(morph, row);
  }

  estimateLineHeights(morph, force = false) {

    let {
      viewState,
      defaultTextStyle,
      lineWrapping: wraps,
      width,
      document: {lines},
      fontMetric
    } = morph;

    for (let i = 0; i < lines.length; i++) {
      let line = lines[i];
      if (!force && line.height > 0) continue;

      let textAttributes = line.textAttributes,
          styles = [];

      // find all styles that apply to line
      if (!textAttributes || !textAttributes.length) styles.push(defaultTextStyle);
      else for (let j = 0; j < textAttributes.length; j++)
        styles.push({...defaultTextStyle, ...textAttributes[j]});

      // measure default char widths and heights
      let charWidthN = 0, charWidthSum = 0, charHeight = 0;
      for (let h = 0; h < styles.length; h++) {
        let {width, height} = fontMetric.defaultCharExtent({
          defaultTextStyle: styles[h],
          cssClassName: "newtext-text-layer"
        });
        charHeight = Math.max(height, charHeight);
        if (wraps) { charWidthSum += width; charWidthN++; }
      }

      let estimatedHeight = charHeight,
          charCount = wraps && line.text.length;
      if (charCount) {
        let charWidth = (charWidthSum/charWidthN),
            charsPerline = Math.max(3, width / charWidth),
            wrappedLineCount = Math.ceil(charCount / charsPerline) || 1;
        estimatedHeight = wrappedLineCount * charHeight;
      }
      line.changeHeight(estimatedHeight, true);
    }

    viewState._textLayoutStale = false;
  }

  textBounds(morph) {
    let {document: doc, textLayout: tl, lineWrapping, padding} = morph,
        pL = padding.left(),
        pT = padding.top(),
        rowY = pT, rowX = pL,
        {rowCount} = doc,
        textWidth = 0, textHeight = 0,
        lastCharBounds;
    if (rowCount === 0) return new Rectangle(pL, pT, 0, 0)

    if (morph.isClip()) {
      this.estimateLineHeights(morph, false);
      let w = Math.max(morph.viewState.textWidth, morph.width),
          h = doc.height;
      return new Rectangle(pL, pT, w - padding.right(), h);
    }

    // for each line check the char bounds for max width...
    for (let row = 0; row < rowCount; row++) {
      let charBounds = tl.charBoundsOfRow(morph, row);
      if (charBounds.length) lastCharBounds = charBounds;

      // ...if we have lineWrapping enabled we need to check every char
      if (lineWrapping) {
        for (let column = 0; column < charBounds.length; column++) {
          let {x, width} = charBounds[column];
          textWidth = Math.max(textWidth, x + width + rowX);
        }

      // ...otherwise only the last
      } else {
        let {x, width} = charBounds[charBounds.length-1];
        textWidth = Math.max(textWidth, x + width + rowX);
      }
    }

    // We get the height of the last row
    let {y, height} = lastCharBounds ? arr.last(lastCharBounds) : {height: 0, y: pT};
    textHeight = y + height;

    return new Rectangle(pL, pT, textWidth-pL, doc.height-pT);
  }

  isFirstLineVisible(morph) {
    return morph.viewState.firstVisibleRow <= 0;
  }
  isLastLineVisible(morph) {
    return morph.viewState.lastVisibleRow >= morph.lineCount() - 1;
  }

  isLineVisible(morph, row) {
    return row >= morph.viewState.firstVisibleRow && row <= morph.viewState.lastVisibleRow;
  }

  isLineFullyVisible(morph, row) {
    // FIXME!!!!!
    return this.isLineVisible(morph, row)
  }

  whatsVisible(morph) {
    var startRow = morph.viewState.firstVisibleRow,
        endRow = morph.viewState.lastVisibleRow,
        lines = morph.document.lines.slice(startRow, endRow);
    return {lines, startRow, endRow};
  }

  screenLineRange(morph, textPos, ignoreLeadingWhitespace) {
    // find the range that includes textPos whose start and end chars are in a
    // vertical line. Normally all chars of a line are positioned vertically
    // next to each other, unless the line is wrapped. This is for figuring
    // that out

    let {row, column} = textPos;
    column = Math.max(0, column);
    row = Math.min(morph.lineCount()-1, Math.max(0, row));

    let charBounds = this.charBoundsOfRow(morph, row);
    if (column > charBounds.length-1) column = charBounds.length-1;
    let bounds = charBounds[column];

    let firstIndex = column, lastIndex = column;

    for (let i = column+1; i < charBounds.length; i++) {
      if (charBounds[i].y+charBounds[i].height > bounds.y+bounds.height) break;
      lastIndex = i;
    }
    lastIndex++;

    for (let i = column-1; i >= 0; i--) {
      if (charBounds[i].y+charBounds[i].height < bounds.y+bounds.height) break;
      firstIndex = i;
    }

    if (ignoreLeadingWhitespace) {
      let rangeString = morph.getLine(row).slice(firstIndex, lastIndex),
          skip = rangeString.match(/^\s*/)[0].length;
      firstIndex += skip;
    }

    return new Range({start: {row, column: firstIndex}, end: {row, column: lastIndex}});
  }

  rangesOfWrappedLine(morph, row) {
    let lineLength = morph.getLine(row).length,
        column = 0, ranges = [];
    while (true) {
      let range = this.screenLineRange(morph, {row, column}, false);
      ranges.push(range);
      if (range.end.column >= lineLength) break;
      if (range.end.column <= column) throw new Error("should not happen");
      column = range.end.column;
    }
    return ranges;
  }

  chunkAtPos(morph, pos) { todo("chunkAtPos"); }

  charBoundsOfRow(morph, row) {
    let doc = morph.document,
        line = doc.getLine(row);

    let cached = this.lineCharBoundsCache.get(line);
    if (cached) return cached;

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // find char bounds via rendered nodes
    let {fontMetric, viewState: {dom_nodeFirstRow, dom_nodes}, padding} = morph,
        paddingLeft = padding.left(),
        paddingRight = padding.right(),
        paddingTop = padding.top(),
        paddingBottom = padding.bottom(),
        lineNode = dom_nodes[row - dom_nodeFirstRow],
        charBounds;

    if (false && lineNode) { // compute using rendered text node
      let {x: offsetX, y: offsetY} = morph.globalPosition;
      offsetX = offsetX - paddingLeft;
      offsetY = offsetY - paddingTop;
      charBounds = charBoundsOfLine(lineNode, line.text.length, offsetX, offsetY);

    } else {
      let {
        defaultTextStyle,
        extent: {x: width, y: height},
        lineWrapping, clipMode, textAlign
      } = morph;

      charBounds = fontMetric.manuallyComputeCharBoundsOfLine(
        line, 0, 0, {
          defaultTextStyle,
          paddingLeft,paddingRight, paddingTop, paddingBottom,
          width, height, lineWrapping, clipMode, textAlign,
          cssClassName: "newtext-text-layer"
        },
      );
    }

    // if ((!line.height || line.hasEstimatedHeight) && charBounds.length) {
    //   // FIXME: when computing height via charbounds... we need to consider
    //   // line margings/paddings, custom line heights....!
    //   let baseY = charBounds[0].y, lineHeight = 0;
    //   for (let i = 0; i < charBounds.length; i++) {
    //     let {y: charBoundsY, height: charBoundsHeight} = charBounds[i];
    //     lineHeight = Math.max(lineHeight, (charBoundsY - baseY) + charBoundsHeight+1/*????*/);
    //   }
    //   line.changeHeight(lineHeight, false/*not estimated*/)
    // }

    this.lineCharBoundsCache.set(line, charBounds);
    return charBounds;
  }

  boundsFor(morph, docPos) {
    let {row, column} = docPos;
    column = Math.max(0, column);
    row = Math.min(morph.lineCount()-1, Math.max(0, row));

    let charBounds = this.charBoundsOfRow(morph, row),
        bounds;

    if (charBounds.length > 0) {
      if (column >= charBounds.length) {
        let {x, y, width, height} = charBounds[charBounds.length-1];
        bounds = new Rectangle(x+width, y, 0, height);
      } else bounds = Rectangle.fromLiteral(charBounds[column]);
    }

    if (!bounds) {
      // throw new Error(`Cannot compute bounds for line ${row}/${column}`);
      console.warn(`Cannot compute bounds for line ${row}/${column}`);
      bounds = new Rectangle(0, 0, 0, 0);
    }

    let {document: doc, padding} = morph;
    bounds.x = bounds.x + padding.left();
    bounds.y = bounds.y + padding.top() + doc.computeVerticalOffsetOf(row);

    return bounds;
  }

  pixelPositionFor(morph, docPos) {
    var { x, y } = this.boundsFor(morph, docPos);
    return pt(x, y);
  }

  textPositionFromPoint(morph, point) {
    this.estimateLineHeights(morph);

    let {x, y} = point,
        {document: doc, padding} = morph,
        padL = padding.left(),
        padT = padding.top(),
        found = doc.findLineByVerticalOffset(y-padT);

    if (!found) {
      if (y >= doc.height+padT) found = {line: {row: doc.rowCount-1}}
      else if (y <= padL) found = {line: {row: 0}}
      else return {row: 0, column: 0};/*????*/
    }

    let {line: {row}} = found,
        charBounds = this.charBoundsOfRow(morph, row),
        nChars = charBounds.length,
        first = charBounds[0],
        last = charBounds[nChars-1],
        result = {row, column: 0};

    // charBounds are in local line coordinates. Translate points x, y so that it fits
    x = x - padL;
    y = y - (padT + doc.computeVerticalOffsetOf(row));

    // everything to the left of the first char + half its width is col 0
    if (nChars === 0 || (x <= first.x + Math.round(first.width/2)
                      && y >= first.y && y <= first.y + first.height)) return result;

    if (x > last.x + Math.round(last.width/2) && y >= last.y && y <= last.y + last.height) {
      result.column = nChars;
      return result;
    }

    // find col so that x between right side of char[col-1] and left side of char[col]
    // consider wrapped lines, i.e. charBounds.y <= y <= charBounds.y+charBounds.height
    let wrappedCharBoundsWithMatchingVerticalPos = [];
    for (var i = nChars-1; i >= 0; i--) {
      let cb = charBounds[i],
          {x: cbX, width: cbWidth, y: cbY, height: cbHeight} = cb;
      if (y < cbY || y > cbY + cbHeight) continue;
      wrappedCharBoundsWithMatchingVerticalPos.push(cb);
      if (x >= cbX + Math.round(cbWidth/2)) {

        let clickedAtEndOfWrappedLine = wrappedCharBoundsWithMatchingVerticalPos.length === 1;
        result.column = clickedAtEndOfWrappedLine ? i/*pos in front of line break*/ : i+1;
        return result;
      }
    }

    // we counted backwards, so the last bounds in
    // wrappedCharBoundsWithMatchingVerticalPos is actually the first in the
    // wrapped line. Since we are left of its center, we just take it
    if (wrappedCharBoundsWithMatchingVerticalPos.length) {
      result.column = charBounds.indexOf(arr.last(wrappedCharBoundsWithMatchingVerticalPos));
      return result;
    }

    // if still not found we go by proximity...
    let minDist = Infinity, minIndex = -1;
    for (let i = 0; i < charBounds.length; i++) {
      let cb = charBounds[i],
          {x: cbX, width: cbWidth, y: cbY, height: cbHeight} = cb,
          dist = point.distSquared(pt(cbX+cbWidth/2, cbY+cbHeight/2));
      if (dist >= minDist) continue;
      minDist = dist;
      minIndex = i;
    }

    result.column = minIndex;
    return result;
  }
}<commit_msg>text2: rangesOfWrappedLine - only go to line end position for inner wrapped lines might be causing issues with up/down goal column, investigate!<commit_after>import { Rectangle, pt } from "lively.graphics";
import { arr } from "lively.lang";
import { inspect } from "lively.morphic";
import { Range } from "../text/range.js";

function todo(name) { throw new Error("not yet implemented " + name)}

export default class TextLayout {

  constructor() {
    this.reset();
  }

  reset() {
    this.resetLineCharBoundsCache();
  }

  resetLineCharBoundsCache(morph) {
    this.lineCharBoundsCache = new WeakMap();
    if (morph) {
      this.estimateLineHeights(morph, true);
      morph.makeDirty();
    }
  }

  resetLineCharBoundsCacheOfLine(line) {
    if (!line) return;
    this.lineCharBoundsCache.set(line, null);
  }

  resetLineCharBoundsCacheOfRow(morph, row) {
    let doc = morph.document;
    doc && this.resetLineCharBoundsCacheOfLine(doc.getLine(row));
  }

  resetLineCharBoundsCacheOfRange(morph, range) {
    for (let row = range.start.row; row <= range.end.row; row++)
      this.resetLineCharBoundsCacheOfRow(morph, row);
  }

  estimateLineHeights(morph, force = false) {

    let {
      viewState,
      defaultTextStyle,
      lineWrapping: wraps,
      width,
      document: {lines},
      fontMetric
    } = morph;

    for (let i = 0; i < lines.length; i++) {
      let line = lines[i];
      if (!force && line.height > 0) continue;

      let textAttributes = line.textAttributes,
          styles = [];

      // find all styles that apply to line
      if (!textAttributes || !textAttributes.length) styles.push(defaultTextStyle);
      else for (let j = 0; j < textAttributes.length; j++)
        styles.push({...defaultTextStyle, ...textAttributes[j]});

      // measure default char widths and heights
      let charWidthN = 0, charWidthSum = 0, charHeight = 0;
      for (let h = 0; h < styles.length; h++) {
        let {width, height} = fontMetric.defaultCharExtent({
          defaultTextStyle: styles[h],
          cssClassName: "newtext-text-layer"
        });
        charHeight = Math.max(height, charHeight);
        if (wraps) { charWidthSum += width; charWidthN++; }
      }

      let estimatedHeight = charHeight,
          charCount = wraps && line.text.length;
      if (charCount) {
        let charWidth = (charWidthSum/charWidthN),
            charsPerline = Math.max(3, width / charWidth),
            wrappedLineCount = Math.ceil(charCount / charsPerline) || 1;
        estimatedHeight = wrappedLineCount * charHeight;
      }
      line.changeHeight(estimatedHeight, true);
    }

    viewState._textLayoutStale = false;
  }

  textBounds(morph) {
    let {document: doc, textLayout: tl, lineWrapping, padding} = morph,
        pL = padding.left(),
        pT = padding.top(),
        rowY = pT, rowX = pL,
        {rowCount} = doc,
        textWidth = 0, textHeight = 0,
        lastCharBounds;
    if (rowCount === 0) return new Rectangle(pL, pT, 0, 0)

    if (morph.isClip()) {
      this.estimateLineHeights(morph, false);
      let w = Math.max(morph.viewState.textWidth, morph.width),
          h = doc.height;
      return new Rectangle(pL, pT, w - padding.right(), h);
    }

    // for each line check the char bounds for max width...
    for (let row = 0; row < rowCount; row++) {
      let charBounds = tl.charBoundsOfRow(morph, row);
      if (charBounds.length) lastCharBounds = charBounds;

      // ...if we have lineWrapping enabled we need to check every char
      if (lineWrapping) {
        for (let column = 0; column < charBounds.length; column++) {
          let {x, width} = charBounds[column];
          textWidth = Math.max(textWidth, x + width + rowX);
        }
        let {y, height} = charBounds[charBounds.length-1];
        textHeight = textHeight + y + height;

      // ...otherwise only the last
      } else {
        let {x, width, y, height} = charBounds[charBounds.length-1];
        textWidth = Math.max(textWidth, x + width + rowX);
        textHeight = textHeight + y + height;
      }
    }

    return new Rectangle(pL, pT, textWidth-pL, doc.height);
  }

  isFirstLineVisible(morph) {
    return morph.viewState.firstVisibleRow <= 0;
  }
  isLastLineVisible(morph) {
    return morph.viewState.lastVisibleRow >= morph.lineCount() - 1;
  }

  isLineVisible(morph, row) {
    return row >= morph.viewState.firstVisibleRow && row <= morph.viewState.lastVisibleRow;
  }

  isLineFullyVisible(morph, row) {
    return row >= morph.viewState.firstFullyVisibleRow && row <= morph.viewState.lastFullyVisibleRow;
  }

  whatsVisible(morph) {
    var startRow = morph.viewState.firstVisibleRow,
        endRow = morph.viewState.lastVisibleRow,
        lines = morph.document.lines.slice(startRow, endRow);
    return {lines, startRow, endRow};
  }

  firstFullVisibleLine(morph) { return morph.viewState.firstFullyVisibleRow; }
  lastFullVisibleLine(morph) { return morph.viewState.lastFullyVisibleRow; }

  screenLineRange(morph, textPos, ignoreLeadingWhitespace) {
    // find the range that includes textPos whose start and end chars are in a
    // vertical line. Normally all chars of a line are positioned vertically
    // next to each other, unless the line is wrapped. This is for figuring
    // that out

    let {row, column} = textPos;
    column = Math.max(0, column);
    row = Math.min(morph.lineCount()-1, Math.max(0, row));

    let charBounds = this.charBoundsOfRow(morph, row);
    if (column > charBounds.length-1) column = charBounds.length-1;
    let bounds = charBounds[column];

    let firstIndex = column, lastIndex = column;

    for (let i = column+1; i < charBounds.length; i++) {
      if (charBounds[i].y+charBounds[i].height > bounds.y+bounds.height) break;
      lastIndex = i;
    }
    // For last range we go until end of line
    if (lastIndex === charBounds.length-1) lastIndex++;

    for (let i = column-1; i >= 0; i--) {
      if (charBounds[i].y+charBounds[i].height < bounds.y+bounds.height) break;
      firstIndex = i;
    }

    if (ignoreLeadingWhitespace) {
      let rangeString = morph.getLine(row).slice(firstIndex, lastIndex),
          skip = rangeString.match(/^\s*/)[0].length;
      firstIndex += skip;
    }

    return new Range({start: {row, column: firstIndex}, end: {row, column: lastIndex}});
  }

  rangesOfWrappedLine(morph, row) {
    let lineLength = morph.getLine(row).length,
        column = 0, ranges = [];
    while (true) {
      let range = this.screenLineRange(morph, {row, column}, false);
      ranges.push(range);
      if (range.end.column >= lineLength) break;
      let nextColumn = range.end.column + 1;
      if (nextColumn >= lineLength) break;
      if (nextColumn <= column)
        throw new Error(`should not happen ${range.end.column} vs ${column}`);
      column = nextColumn;
    }
    return ranges;
  }

  chunkAtPos(morph, pos) { todo("chunkAtPos"); }

  charBoundsOfRow(morph, row) {
    let doc = morph.document,
        line = doc.getLine(row);

    let cached = this.lineCharBoundsCache.get(line);
    if (cached) return cached;

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // find char bounds via rendered nodes
    let {fontMetric, viewState: {dom_nodeFirstRow, dom_nodes}, padding} = morph,
        paddingLeft = padding.left(),
        paddingRight = padding.right(),
        paddingTop = padding.top(),
        paddingBottom = padding.bottom(),
        lineNode = dom_nodes[row - dom_nodeFirstRow],
        charBounds;

    if (false && lineNode) { // compute using rendered text node
      let {x: offsetX, y: offsetY} = morph.globalPosition;
      offsetX = offsetX - paddingLeft;
      offsetY = offsetY - paddingTop;
      charBounds = charBoundsOfLine(lineNode, line.text.length, offsetX, offsetY);

    } else {
      let {
        defaultTextStyle,
        extent: {x: width, y: height},
        lineWrapping, clipMode, textAlign
      } = morph;

      charBounds = fontMetric.manuallyComputeCharBoundsOfLine(
        line, 0, 0, {
          defaultTextStyle,
          paddingLeft,paddingRight, paddingTop, paddingBottom,
          width, height, lineWrapping, clipMode, textAlign,
          cssClassName: "newtext-text-layer"
        },
      );
    }

    // if ((!line.height || line.hasEstimatedHeight) && charBounds.length) {
    //   // FIXME: when computing height via charbounds... we need to consider
    //   // line margings/paddings, custom line heights....!
    //   let baseY = charBounds[0].y, lineHeight = 0;
    //   for (let i = 0; i < charBounds.length; i++) {
    //     let {y: charBoundsY, height: charBoundsHeight} = charBounds[i];
    //     lineHeight = Math.max(lineHeight, (charBoundsY - baseY) + charBoundsHeight+1/*????*/);
    //   }
    //   line.changeHeight(lineHeight, false/*not estimated*/)
    // }

    this.lineCharBoundsCache.set(line, charBounds);
    return charBounds;
  }

  boundsFor(morph, docPos) {
    let {row, column} = docPos;
    column = Math.max(0, column);
    row = Math.min(morph.lineCount()-1, Math.max(0, row));

    let charBounds = this.charBoundsOfRow(morph, row),
        bounds;

    if (charBounds.length > 0) {
      if (column >= charBounds.length) {
        let {x, y, width, height} = charBounds[charBounds.length-1];
        bounds = new Rectangle(x+width, y, 0, height);
      } else bounds = Rectangle.fromLiteral(charBounds[column]);
    }

    if (!bounds) {
      // throw new Error(`Cannot compute bounds for line ${row}/${column}`);
      console.warn(`Cannot compute bounds for line ${row}/${column}`);
      bounds = new Rectangle(0, 0, 0, 0);
    }

    let {document: doc, padding} = morph;
    bounds.x = bounds.x + padding.left();
    bounds.y = bounds.y + padding.top() + doc.computeVerticalOffsetOf(row);

    return bounds;
  }

  pixelPositionFor(morph, docPos) {
    var { x, y } = this.boundsFor(morph, docPos);
    return pt(x, y);
  }

  textPositionFromPoint(morph, point) {
    this.estimateLineHeights(morph);

    let {x, y} = point,
        {document: doc, padding} = morph,
        padL = padding.left(),
        padT = padding.top(),
        found = doc.findLineByVerticalOffset(y-padT);

    if (!found) {
      if (y >= doc.height+padT) found = {line: {row: doc.rowCount-1}}
      else if (y <= padL) found = {line: {row: 0}}
      else return {row: 0, column: 0};/*????*/
    }

    let {line: {row}} = found,
        charBounds = this.charBoundsOfRow(morph, row),
        nChars = charBounds.length,
        first = charBounds[0],
        last = charBounds[nChars-1],
        result = {row, column: 0};

    // charBounds are in local line coordinates. Translate points x, y so that it fits
    x = x - padL;
    y = y - (padT + doc.computeVerticalOffsetOf(row));

    // everything to the left of the first char + half its width is col 0
    if (nChars === 0 || (x <= first.x + Math.round(first.width/2)
                      && y >= first.y && y <= first.y + first.height)) return result;

    if (x > last.x + Math.round(last.width/2) && y >= last.y && y <= last.y + last.height) {
      result.column = nChars;
      return result;
    }

    // find col so that x between right side of char[col-1] and left side of char[col]
    // consider wrapped lines, i.e. charBounds.y <= y <= charBounds.y+charBounds.height
    let wrappedCharBoundsWithMatchingVerticalPos = [];
    for (var i = nChars-1; i >= 0; i--) {
      let cb = charBounds[i],
          {x: cbX, width: cbWidth, y: cbY, height: cbHeight} = cb;
      if (y < cbY || y > cbY + cbHeight) continue;
      wrappedCharBoundsWithMatchingVerticalPos.push(cb);
      if (x >= cbX + Math.round(cbWidth/2)) {

        let clickedAtEndOfWrappedLine = wrappedCharBoundsWithMatchingVerticalPos.length === 1;
        result.column = clickedAtEndOfWrappedLine ? i/*pos in front of line break*/ : i+1;
        return result;
      }
    }

    // we counted backwards, so the last bounds in
    // wrappedCharBoundsWithMatchingVerticalPos is actually the first in the
    // wrapped line. Since we are left of its center, we just take it
    if (wrappedCharBoundsWithMatchingVerticalPos.length) {
      result.column = charBounds.indexOf(arr.last(wrappedCharBoundsWithMatchingVerticalPos));
      return result;
    }

    // if still not found we go by proximity...
    let minDist = Infinity, minIndex = -1;
    for (let i = 0; i < charBounds.length; i++) {
      let cb = charBounds[i],
          {x: cbX, width: cbWidth, y: cbY, height: cbHeight} = cb,
          dist = point.distSquared(pt(cbX+cbWidth/2, cbY+cbHeight/2));
      if (dist >= minDist) continue;
      minDist = dist;
      minIndex = i;
    }

    result.column = minIndex;
    return result;
  }
}<|endoftext|>


<commit_before>import { sortByYAndX } from 'utils/variables/collected-variables-utils';
import maxBy from 'lodash.maxby';

import * as ResponseFormatSimple from './response-format-simple';
import * as ResponseFormatSingle from './response-format-single';
import * as CodeList from './codes-list';
import * as Dimension from './dimension';
import * as Responses from './responses';
import { hasChild } from 'utils/codes-lists/codes-lists-utils';

import {
  DIMENSION_TYPE,
  DIMENSION_FORMATS,
  QUESTION_TYPE_ENUM,
  DATATYPE_NAME,
  DEFAULT_CODES_LIST_SELECTOR_PATH,
} from 'constants/pogues-constants';

const { PRIMARY, SECONDARY, MEASURE, LIST_MEASURE } = DIMENSION_TYPE;
const { LIST, CODES_LIST } = DIMENSION_FORMATS;
const { SIMPLE, SINGLE_CHOICE } = QUESTION_TYPE_ENUM;
const { TEXT } = DATATYPE_NAME;

// HELPERS

function getDimensionsByType(type, dimensions) {
  let dimension;

  for (let i = 0; i < dimensions.length; i += 1) {
    if (dimensions[i].dimensionType === type) {
      dimension = dimensions[i];
      break;
    }
  }

  return dimension;
}

function getDimensionsMeasures(dimensions) {
  return dimensions.reduce((acc, d) => {
    if (d.dimensionType === MEASURE) acc.push(d);
    return acc;
  }, []);
}

function getResponsesOffset(primaryState, secondaryState, activeCodeLists) {
  let responseOffset = 1;

  if (primaryState.type === CODES_LIST) {
    const {
      CODES_LIST: {
        [DEFAULT_CODES_LIST_SELECTOR_PATH]: { id: codesListIdPrimary },
      },
    } = primaryState;
    let responseOffsetSecondary = 1;

    if (secondaryState) {
      const {
        [DEFAULT_CODES_LIST_SELECTOR_PATH]: { id: codesListIdSecondary },
      } = secondaryState;
      responseOffsetSecondary = Object.keys(
        activeCodeLists[codesListIdSecondary].codes,
      ).length;
    }
    const listCodes = Object.keys(
      activeCodeLists[codesListIdPrimary].codes,
    ).map(key => activeCodeLists[codesListIdPrimary].codes[key]);
    const codes = listCodes.filter(code => !hasChild(code, listCodes));
    responseOffset = codes.length * responseOffsetSecondary;
  }
  return responseOffset;
}

function getMeasuresModel(responses, dimensions, offset) {
  const responsesModel = [];
  for (let i = 0; i < dimensions.length; i += 1) {
    if (responses[i].Datatype.typeName === DATATYPE_NAME.DATE) {
      responses[i].Datatype.Format ===
        responses[i].Datatype.Format.toLowerCase();
    }

    if (responses[i].Datatype.typeName === DATATYPE_NAME.DURATION) {
      if (responses[i].Datatype.Minimum !== undefined) {
        const strminimum = responses[i].Datatype.Minimum;
        const matches_minimum = strminimum.match(/\d+/g);
        if (responses[i].Datatype.Format === 'PTnHnM') {
          responses[i].Datatype.Mihours =
            matches_minimum[0] == 0 ? '' : matches_minimum[0];
          responses[i].Datatype.Miminutes =
            matches_minimum[1] == 0 ? '' : matches_minimum[0];
        }
        if (responses[i].Datatype.Format === 'PnYnM') {
          responses[i].Datatype.Miyears =
            matches_minimum[0] == 0 ? '' : matches_minimum[0];
          responses[i].Datatype.Mimonths =
            matches_minimum[1] == 0 ? '' : matches_minimum[0];
        }
        if (responses[i].Datatype.Format === 'HH:CH') {
          responses[i].Datatype.Mihundhours =
            matches_minimum[0][0] == 0
              ? matches_minimum[0].slice(1)
              : matches_minimum[0];
          responses[i].Datatype.Mihundredths =
            matches_minimum[1][0] == 0
              ? matches_minimum[1].slice(1)
              : matches_minimum[1];
        }
      }
      if (responses[i].Datatype.Maximum !== undefined) {
        const strmaximum = responses[i].Datatype.Maximum;
        const matches_maximum = strmaximum.match(/\d+/g);
        if (responses[i].Datatype.Format === 'PTnHnM') {
          responses[i].Datatype.Mahours =
            matches_maximum[0] == 0 ? '' : matches_maximum[0];
          responses[i].Datatype.Maminutes =
            matches_maximum[1] == 0 ? '' : matches_maximum[0];
        }
        if (responses[i].Datatype.Format === 'PnYnM') {
          responses[i].Datatype.Mayears =
            matches_maximum[0] == 0 ? '' : matches_maximum[0];
          responses[i].Datatype.Mamonths =
            matches_maximum[1] == 0 ? '' : matches_maximum[0];
        }
        if (responses[i].Datatype.Format === 'HH:CH') {
          responses[i].Datatype.Mahundhours =
            matches_maximum[0][0] == 0
              ? matches_maximum[0].slice(1)
              : matches_maximum[0];
          responses[i].Datatype.Mahundredths =
            matches_maximum[1][0] == 0
              ? matches_maximum[1].slice(1)
              : matches_maximum[1];
        }
      }
    }

    responsesModel.push({
      Label: dimensions[i].Label,
      response: responses[i * offset],
    });
  }

  return responsesModel;
}

function parseDynamic(dynamic) {
  return dynamic.split('-').map(v => {
    return v.length > 0 ? parseInt(v, 10) : 0;
  });
}

// REMOTE TO STATE

function remoteToStatePrimary(remote) {
  const { totalLabel, dynamic, CodeListReference } = remote;
  let state = {};

  if (totalLabel) {
    state.showTotalLabel = '1';
    state.totalLabel = totalLabel;
  }

  if (CodeListReference) {
    state = {
      ...state,
      type: CODES_LIST,
      [CODES_LIST]: {
        [DEFAULT_CODES_LIST_SELECTOR_PATH]: CodeList.remoteToState(
          CodeListReference,
        ),
      },
    };
  } else {
    const [numLinesMin, numLinesMax] = parseDynamic(dynamic);
    state = {
      ...state,
      type: LIST,
      [LIST]: {
        numLinesMin: numLinesMin,
        numLinesMax: numLinesMax,
      },
    };
  }

  return state;
}

function remoteToStateSecondary(remote) {
  const { totalLabel, CodeListReference } = remote;
  const state = {
    showSecondaryAxis: true,
    [DEFAULT_CODES_LIST_SELECTOR_PATH]: CodeList.remoteToState(
      CodeListReference,
    ),
  };

  if (totalLabel) {
    state.showTotalLabel = '1';
    state.totalLabel = totalLabel;
  }

  return state;
}

function remoteToStateMeasure(remote) {
  const {
    Label: label,
    response: { CodeListReference, Datatype },
  } = remote;
  const state = {};

  if (CodeListReference) {
    state.type = SINGLE_CHOICE;
    state[SINGLE_CHOICE] = ResponseFormatSingle.remoteToState({
      responses: [{ Datatype, CodeListReference }],
    });
  } else {
    state.type = SIMPLE;
    state[SIMPLE] = ResponseFormatSimple.remoteToState({
      responses: [{ Datatype }],
    });
  }

  return {
    label,
    ...state,
  };
}

export function remoteToState(remote, codesListsStore) {
  const { dimensions, responses } = remote;
  const state = {};

  // Dimensions
  const dimensionSecondaryModel = getDimensionsByType(SECONDARY, dimensions);
  const dimensionPrimaryModel = getDimensionsByType(PRIMARY, dimensions);
  const dimensionMeasuresModel = getDimensionsMeasures(dimensions);

  // Primary and secondary state
  state[PRIMARY] = remoteToStatePrimary(dimensionPrimaryModel);

  if (dimensionSecondaryModel) {
    state[SECONDARY] = remoteToStateSecondary(dimensionSecondaryModel);
  }

  // Measures
  const responsesOffset = getResponsesOffset(
    state[PRIMARY],
    state[SECONDARY],
    codesListsStore,
  );
  const responsesMeasuresModel = getMeasuresModel(
    responses,
    dimensionMeasuresModel,
    responsesOffset,
  );

  if (dimensionSecondaryModel) {
    state[MEASURE] = remoteToStateMeasure(responsesMeasuresModel[0]);
  } else {
    state[LIST_MEASURE] = responsesMeasuresModel.map(m =>
      remoteToStateMeasure(m),
    );
  }

  return state;
}

// STATE TO REMOTE

function stateToResponseState(state) {
  const { type: measureType, [measureType]: measureTypeState } = state;
  let responseState = {};

  if (measureType === SIMPLE) {
    const {
      mandatory,
      type: typeName,
      [typeName]: simpleState,
    } = measureTypeState;

    let customsimpleState = simpleState;

    if (
      customsimpleState.format !== undefined &&
      typeName === DATATYPE_NAME.DATE
    ) {
      const { format, minimum, maximum, ...durationsimpleState } = simpleState;

      durationsimpleState.format = format.toUpperCase();
      if (customsimpleState.minimum !== '') {
        durationsimpleState.minimum = minimum;
      }
      if (customsimpleState.maximum !== '') {
        durationsimpleState.maximum = maximum;
      }
      customsimpleState = durationsimpleState;
    }

    if (typeName === DATATYPE_NAME.DURATION) {
      const {
        miyears,
        mimonths,
        mayears,
        mamonths,
        mihours,
        miminutes,
        mahours,
        maminutes,
        mihundhours,
        mihundredths,
        mahundhours,
        mahundredths,
      } = customsimpleState;
      const durationDataType = {};
      durationDataType.format = simpleState.format;
      if (simpleState.format === 'PnYnM') {
        if (miyears !== '' || mimonths !== '') {
          durationDataType.minimum = `P${miyears || 0}Y${mimonths || 0}M`;
        }
        if (mayears !== '' || mamonths !== '') {
          durationDataType.maximum = `P${mayears || 0}Y${mamonths || 0}M`;
        }
      }
      if (simpleState.format === 'PTnHnM') {
        if (mihours || miminutes) {
          durationDataType.minimum = `PT${mihours || 0}H${miminutes || 0}M`;
        }
        if (mahours || maminutes) {
          durationDataType.maximum = `PT${mahours || 0}H${maminutes || 0}M`;
        }
      }
      if (simpleState.format === 'HH:CH') {
        if (mihundhours || mihundredths) {
          durationDataType.minimum = `${
            mihundhours ? `0${mihundhours}`.slice(-2) : '00'
          }:${mihundredths ? `0${mihundredths}`.slice(-2) : '00'}`;
        }
        if (mahundhours || mahundredths) {
          durationDataType.maximum = `${
            mahundhours ? `0${mahundhours}`.slice(-2) : '00'
          }:${mahundredths ? `0${mahundredths}`.slice(-2) : '00'}`;
        }
      }
      customsimpleState = durationDataType;
    }

    responseState = { mandatory, typeName, ...customsimpleState };
  } else {
    const {
      mandatory,
      visHint,
      [DEFAULT_CODES_LIST_SELECTOR_PATH]: { id: codesListId },
    } = measureTypeState;
    responseState = {
      mandatory,
      codesListId,
      typeName: TEXT,
      maxLength: 1,
      pattern: '',
      visHint,
    };
  }

  return responseState;
}

export function stateToRemote(
  state,
  collectedVariables,
  collectedVariablesStore,
) {
  const {
    [PRIMARY]: primaryState,
    [SECONDARY]: secondaryState,
    [MEASURE]: measureState,
    [LIST_MEASURE]: listMeasuresState,
  } = state;

  const {
    type,
    [type]: { type: typePrimaryCodesList, ...primaryTypeState },
    ...totalLabelPrimaryState
  } = primaryState;
  const dimensionsModel = [];
  let responsesState = [];

  // Primary and secondary dimension
  dimensionsModel.push(
    Dimension.stateToRemote({
      type: PRIMARY,
      ...primaryTypeState,
      ...totalLabelPrimaryState,
    }),
  );
  if (secondaryState) {
    const {
      type: typeSecondaryCodesList,
      ...secondaryTypeState
    } = secondaryState;
    dimensionsModel.push(
      Dimension.stateToRemote({ type: SECONDARY, ...secondaryTypeState }),
    );
  }
  // Measures dimensions
  if (measureState) {
    dimensionsModel.push(
      Dimension.stateToRemote({ type: MEASURE, label: measureState.label }),
    );
    responsesState = [stateToResponseState(measureState)];
  } else {
    for (let i = 0; i < listMeasuresState.length; i += 1) {
      dimensionsModel.push(
        Dimension.stateToRemote({
          type: MEASURE,
          label: listMeasuresState[i].label,
        }),
      );
      responsesState.push(stateToResponseState(listMeasuresState[i]));
    }
  }

  // Responses

  const numDataTypes = measureState
    ? maxBy(collectedVariables.map(key => collectedVariablesStore[key]), 'y')
        .y || 1
    : listMeasuresState.length;
  let responsesModel = [];
  let mappingsModel = [];
  let attributesModel = [];

  for (let i = 0; i < numDataTypes; i += 1) {
    const collectedVariablesByDatatype = collectedVariables
      .sort(sortByYAndX(collectedVariablesStore))
      .map(key => collectedVariablesStore[key])
      .filter(variable => !variable.y || variable.y === i + 1)
      .map(variable => variable.id);

    const responsesModelByRow = Responses.stateToModel(
      responsesState[measureState ? 0 : i],
      collectedVariablesByDatatype,
      collectedVariablesStore,
      QUESTION_TYPE_ENUM.TABLE,
    );

    responsesModel = [...responsesModel, ...responsesModelByRow.Response];
    mappingsModel = [...mappingsModel, ...responsesModelByRow.Mapping];
    attributesModel = [...attributesModel, ...responsesModelByRow.Attribute];
  }

  mappingsModel = mappingsModel.sort((m1, m2) => {
    const [x1, y1] = m1.MappingTarget.split(' ');
    const [x2, y2] = m2.MappingTarget.split(' ');
    if (!y1) return parseInt(x1, 10) - parseInt(x2, 10);
    return (
      parseInt(y1, 10) * 100 +
      parseInt(x1, 10) -
      (parseInt(y2, 10) * 100 + parseInt(x2, 10))
    );
  });
  return {
    Dimension: dimensionsModel,
    Response: responsesModel,
    Mapping: mappingsModel,
    Attribute: attributesModel,
  };
}
<commit_msg>'getMeasuresModel'<commit_after>import { sortByYAndX } from 'utils/variables/collected-variables-utils';
import maxBy from 'lodash.maxby';

import * as ResponseFormatSimple from './response-format-simple';
import * as ResponseFormatSingle from './response-format-single';
import * as CodeList from './codes-list';
import * as Dimension from './dimension';
import * as Responses from './responses';
import { hasChild } from 'utils/codes-lists/codes-lists-utils';

import {
  DIMENSION_TYPE,
  DIMENSION_FORMATS,
  QUESTION_TYPE_ENUM,
  DATATYPE_NAME,
  DEFAULT_CODES_LIST_SELECTOR_PATH,
} from 'constants/pogues-constants';

const { PRIMARY, SECONDARY, MEASURE, LIST_MEASURE } = DIMENSION_TYPE;
const { LIST, CODES_LIST } = DIMENSION_FORMATS;
const { SIMPLE, SINGLE_CHOICE } = QUESTION_TYPE_ENUM;
const { TEXT } = DATATYPE_NAME;

// HELPERS

function getDimensionsByType(type, dimensions) {
  let dimension;

  for (let i = 0; i < dimensions.length; i += 1) {
    if (dimensions[i].dimensionType === type) {
      dimension = dimensions[i];
      break;
    }
  }

  return dimension;
}

function getDimensionsMeasures(dimensions) {
  return dimensions.reduce((acc, d) => {
    if (d.dimensionType === MEASURE) acc.push(d);
    return acc;
  }, []);
}

function getResponsesOffset(primaryState, secondaryState, activeCodeLists) {
  let responseOffset = 1;

  if (primaryState.type === CODES_LIST) {
    const {
      CODES_LIST: {
        [DEFAULT_CODES_LIST_SELECTOR_PATH]: { id: codesListIdPrimary },
      },
    } = primaryState;
    let responseOffsetSecondary = 1;

    if (secondaryState) {
      const {
        [DEFAULT_CODES_LIST_SELECTOR_PATH]: { id: codesListIdSecondary },
      } = secondaryState;
      responseOffsetSecondary = Object.keys(
        activeCodeLists[codesListIdSecondary].codes,
      ).length;
    }
    const listCodes = Object.keys(
      activeCodeLists[codesListIdPrimary].codes,
    ).map(key => activeCodeLists[codesListIdPrimary].codes[key]);
    const codes = listCodes.filter(code => !hasChild(code, listCodes));
    responseOffset = codes.length * responseOffsetSecondary;
  }
  return responseOffset;
}

function getMeasuresModel(responses, dimensions, offset) {
  const responsesModel = [];
  for (let i = 0; i < dimensions.length; i += 1) {
    if (responses[i].Datatype.typeName === DATATYPE_NAME.DATE) {
      responses[i].Datatype.Format = responses[i].Datatype.Format.toLowerCase();
    }

    if (responses[i].Datatype.typeName === DATATYPE_NAME.DURATION) {
      if (responses[i].Datatype.Minimum !== undefined) {
        const strminimum = responses[i].Datatype.Minimum;
        const matches_minimum = strminimum.match(/\d+/g);
        if (responses[i].Datatype.Format === 'PTnHnM') {
          responses[i].Datatype.Mihours =
            matches_minimum[0] === 0 ? '' : matches_minimum[0];
          responses[i].Datatype.Miminutes =
            matches_minimum[1] === 0 ? '' : matches_minimum[0];
        }
        if (responses[i].Datatype.Format === 'PnYnM') {
          responses[i].Datatype.Miyears =
            matches_minimum[0] === 0 ? '' : matches_minimum[0];
          responses[i].Datatype.Mimonths =
            matches_minimum[1] === 0 ? '' : matches_minimum[0];
        }
        if (responses[i].Datatype.Format === 'HH:CH') {
          responses[i].Datatype.Mihundhours =
            matches_minimum[0][0] === 0
              ? matches_minimum[0].slice(1)
              : matches_minimum[0];
          responses[i].Datatype.Mihundredths =
            matches_minimum[1][0] === 0
              ? matches_minimum[1].slice(1)
              : matches_minimum[1];
        }
      }
      if (responses[i].Datatype.Maximum !== undefined) {
        const strmaximum = responses[i].Datatype.Maximum;
        const matches_maximum = strmaximum.match(/\d+/g);
        if (responses[i].Datatype.Format === 'PTnHnM') {
          responses[i].Datatype.Mahours =
            matches_maximum[0] === 0 ? '' : matches_maximum[0];
          responses[i].Datatype.Maminutes =
            matches_maximum[1] === 0 ? '' : matches_maximum[0];
        }
        if (responses[i].Datatype.Format === 'PnYnM') {
          responses[i].Datatype.Mayears =
            matches_maximum[0] === 0 ? '' : matches_maximum[0];
          responses[i].Datatype.Mamonths =
            matches_maximum[1] === 0 ? '' : matches_maximum[0];
        }
        if (responses[i].Datatype.Format === 'HH:CH') {
          responses[i].Datatype.Mahundhours =
            matches_maximum[0][0] === 0
              ? matches_maximum[0].slice(1)
              : matches_maximum[0];
          responses[i].Datatype.Mahundredths =
            matches_maximum[1][0] === 0
              ? matches_maximum[1].slice(1)
              : matches_maximum[1];
        }
      }
    }

    responsesModel.push({
      Label: dimensions[i].Label,
      response: responses[i * offset],
    });
  }

  return responsesModel;
}

function parseDynamic(dynamic) {
  return dynamic.split('-').map(v => {
    return v.length > 0 ? parseInt(v, 10) : 0;
  });
}

// REMOTE TO STATE

function remoteToStatePrimary(remote) {
  const { totalLabel, dynamic, CodeListReference } = remote;
  let state = {};

  if (totalLabel) {
    state.showTotalLabel = '1';
    state.totalLabel = totalLabel;
  }

  if (CodeListReference) {
    state = {
      ...state,
      type: CODES_LIST,
      [CODES_LIST]: {
        [DEFAULT_CODES_LIST_SELECTOR_PATH]: CodeList.remoteToState(
          CodeListReference,
        ),
      },
    };
  } else {
    const [numLinesMin, numLinesMax] = parseDynamic(dynamic);
    state = {
      ...state,
      type: LIST,
      [LIST]: {
        numLinesMin: numLinesMin,
        numLinesMax: numLinesMax,
      },
    };
  }

  return state;
}

function remoteToStateSecondary(remote) {
  const { totalLabel, CodeListReference } = remote;
  const state = {
    showSecondaryAxis: true,
    [DEFAULT_CODES_LIST_SELECTOR_PATH]: CodeList.remoteToState(
      CodeListReference,
    ),
  };

  if (totalLabel) {
    state.showTotalLabel = '1';
    state.totalLabel = totalLabel;
  }

  return state;
}

function remoteToStateMeasure(remote) {
  const {
    Label: label,
    response: { CodeListReference, Datatype },
  } = remote;
  const state = {};

  if (CodeListReference) {
    state.type = SINGLE_CHOICE;
    state[SINGLE_CHOICE] = ResponseFormatSingle.remoteToState({
      responses: [{ Datatype, CodeListReference }],
    });
  } else {
    state.type = SIMPLE;
    state[SIMPLE] = ResponseFormatSimple.remoteToState({
      responses: [{ Datatype }],
    });
  }

  return {
    label,
    ...state,
  };
}

export function remoteToState(remote, codesListsStore) {
  const { dimensions, responses } = remote;
  const state = {};

  // Dimensions
  const dimensionSecondaryModel = getDimensionsByType(SECONDARY, dimensions);
  const dimensionPrimaryModel = getDimensionsByType(PRIMARY, dimensions);
  const dimensionMeasuresModel = getDimensionsMeasures(dimensions);

  // Primary and secondary state
  state[PRIMARY] = remoteToStatePrimary(dimensionPrimaryModel);

  if (dimensionSecondaryModel) {
    state[SECONDARY] = remoteToStateSecondary(dimensionSecondaryModel);
  }

  // Measures
  const responsesOffset = getResponsesOffset(
    state[PRIMARY],
    state[SECONDARY],
    codesListsStore,
  );
  const responsesMeasuresModel = getMeasuresModel(
    responses,
    dimensionMeasuresModel,
    responsesOffset,
  );

  if (dimensionSecondaryModel) {
    state[MEASURE] = remoteToStateMeasure(responsesMeasuresModel[0]);
  } else {
    state[LIST_MEASURE] = responsesMeasuresModel.map(m =>
      remoteToStateMeasure(m),
    );
  }

  return state;
}

// STATE TO REMOTE

function stateToResponseState(state) {
  const { type: measureType, [measureType]: measureTypeState } = state;
  let responseState = {};

  if (measureType === SIMPLE) {
    const {
      mandatory,
      type: typeName,
      [typeName]: simpleState,
    } = measureTypeState;

    let customsimpleState = simpleState;

    if (
      customsimpleState.format !== undefined &&
      typeName === DATATYPE_NAME.DATE
    ) {
      const { format, minimum, maximum, ...durationsimpleState } = simpleState;

      durationsimpleState.format = format.toUpperCase();
      if (customsimpleState.minimum !== '') {
        durationsimpleState.minimum = minimum;
      }
      if (customsimpleState.maximum !== '') {
        durationsimpleState.maximum = maximum;
      }
      customsimpleState = durationsimpleState;
    }

    if (typeName === DATATYPE_NAME.DURATION) {
      const {
        miyears,
        mimonths,
        mayears,
        mamonths,
        mihours,
        miminutes,
        mahours,
        maminutes,
        mihundhours,
        mihundredths,
        mahundhours,
        mahundredths,
      } = customsimpleState;
      const durationDataType = {};
      durationDataType.format = simpleState.format;
      if (simpleState.format === 'PnYnM') {
        if (miyears !== '' || mimonths !== '') {
          durationDataType.minimum = `P${miyears || 0}Y${mimonths || 0}M`;
        }
        if (mayears !== '' || mamonths !== '') {
          durationDataType.maximum = `P${mayears || 0}Y${mamonths || 0}M`;
        }
      }
      if (simpleState.format === 'PTnHnM') {
        if (mihours || miminutes) {
          durationDataType.minimum = `PT${mihours || 0}H${miminutes || 0}M`;
        }
        if (mahours || maminutes) {
          durationDataType.maximum = `PT${mahours || 0}H${maminutes || 0}M`;
        }
      }
      if (simpleState.format === 'HH:CH') {
        if (mihundhours || mihundredths) {
          durationDataType.minimum = `${
            mihundhours ? `0${mihundhours}`.slice(-2) : '00'
          }:${mihundredths ? `0${mihundredths}`.slice(-2) : '00'}`;
        }
        if (mahundhours || mahundredths) {
          durationDataType.maximum = `${
            mahundhours ? `0${mahundhours}`.slice(-2) : '00'
          }:${mahundredths ? `0${mahundredths}`.slice(-2) : '00'}`;
        }
      }
      customsimpleState = durationDataType;
    }

    responseState = { mandatory, typeName, ...customsimpleState };
  } else {
    const {
      mandatory,
      visHint,
      [DEFAULT_CODES_LIST_SELECTOR_PATH]: { id: codesListId },
    } = measureTypeState;
    responseState = {
      mandatory,
      codesListId,
      typeName: TEXT,
      maxLength: 1,
      pattern: '',
      visHint,
    };
  }

  return responseState;
}

export function stateToRemote(
  state,
  collectedVariables,
  collectedVariablesStore,
) {
  const {
    [PRIMARY]: primaryState,
    [SECONDARY]: secondaryState,
    [MEASURE]: measureState,
    [LIST_MEASURE]: listMeasuresState,
  } = state;

  const {
    type,
    [type]: { type: typePrimaryCodesList, ...primaryTypeState },
    ...totalLabelPrimaryState
  } = primaryState;
  const dimensionsModel = [];
  let responsesState = [];

  // Primary and secondary dimension
  dimensionsModel.push(
    Dimension.stateToRemote({
      type: PRIMARY,
      ...primaryTypeState,
      ...totalLabelPrimaryState,
    }),
  );
  if (secondaryState) {
    const {
      type: typeSecondaryCodesList,
      ...secondaryTypeState
    } = secondaryState;
    dimensionsModel.push(
      Dimension.stateToRemote({ type: SECONDARY, ...secondaryTypeState }),
    );
  }
  // Measures dimensions
  if (measureState) {
    dimensionsModel.push(
      Dimension.stateToRemote({ type: MEASURE, label: measureState.label }),
    );
    responsesState = [stateToResponseState(measureState)];
  } else {
    for (let i = 0; i < listMeasuresState.length; i += 1) {
      dimensionsModel.push(
        Dimension.stateToRemote({
          type: MEASURE,
          label: listMeasuresState[i].label,
        }),
      );
      responsesState.push(stateToResponseState(listMeasuresState[i]));
    }
  }

  // Responses

  const numDataTypes = measureState
    ? maxBy(collectedVariables.map(key => collectedVariablesStore[key]), 'y')
        .y || 1
    : listMeasuresState.length;
  let responsesModel = [];
  let mappingsModel = [];
  let attributesModel = [];

  for (let i = 0; i < numDataTypes; i += 1) {
    const collectedVariablesByDatatype = collectedVariables
      .sort(sortByYAndX(collectedVariablesStore))
      .map(key => collectedVariablesStore[key])
      .filter(variable => !variable.y || variable.y === i + 1)
      .map(variable => variable.id);

    const responsesModelByRow = Responses.stateToModel(
      responsesState[measureState ? 0 : i],
      collectedVariablesByDatatype,
      collectedVariablesStore,
      QUESTION_TYPE_ENUM.TABLE,
    );

    responsesModel = [...responsesModel, ...responsesModelByRow.Response];
    mappingsModel = [...mappingsModel, ...responsesModelByRow.Mapping];
    attributesModel = [...attributesModel, ...responsesModelByRow.Attribute];
  }

  mappingsModel = mappingsModel.sort((m1, m2) => {
    const [x1, y1] = m1.MappingTarget.split(' ');
    const [x2, y2] = m2.MappingTarget.split(' ');
    if (!y1) return parseInt(x1, 10) - parseInt(x2, 10);
    return (
      parseInt(y1, 10) * 100 +
      parseInt(x1, 10) -
      (parseInt(y2, 10) * 100 + parseInt(x2, 10))
    );
  });
  return {
    Dimension: dimensionsModel,
    Response: responsesModel,
    Mapping: mappingsModel,
    Attribute: attributesModel,
  };
}
<|endoftext|>


<commit_before>/*
 * This program is part of the OpenLMIS logistics management information system platform software.
 * Copyright © 2017 VillageReach
 *
 * This program is free software: you can redistribute it and/or modify it under the terms
 * of the GNU Affero General Public License as published by the Free Software Foundation, either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details. You should have received a copy of
 * the GNU Affero General Public License along with this program. If not, see
 * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.
 */

(function() {

    'use strict';

    /**
       * @ngdoc directive
       * @restrict E
       * @name openlmis-form.directive:openlmisDatepicker
       *
       * @description
       * Directive allows to add date picker input.
       *
       * @example
       * To make this directive work only 'value' attribute is required, however there are more attributes to use.
       * In order to make datepicker input use id you can add 'input-id' attribute.
       * The 'change-method' attribute takes function that will be executed after datepicker value change.
       * Datepicker directive also can take max-date and min-date attributes. Their values can be set from other datepickers or manually.
       * ```
       * <openlmis-datepicker
       * 	   value="startDate"
       *     input-id="datepicker-id"
       *     change-method="afterChange()"
       *     min-date="10/05/2016"
       *     max-date="endDate">
       * </openlmis-datepicker>
       *
       * <openlmis-datepicker
       * 	   value="endDate">
       * </openlmis-datepicker>
       * ```
       */
    angular
        .module('openlmis-form')
        .directive('openlmisDatepicker', datepicker);

    datepicker.$inject = ['$filter', 'jQuery', 'messageService', 'DEFAULT_DATE_FORMAT', 'DEFAULT_DATEPICKER_FORMAT'];

    function datepicker($filter, jQuery, messageService, DEFAULT_DATE_FORMAT, DEFAULT_DATEPICKER_FORMAT) {
        return {
            restrict: 'E',
            scope: {
                value: '=',
                inputId: '@?',
                minDate: '=?',
                maxDate: '=?',
                changeMethod: '=?',
                dateFormat: '=?',
                language: '=?',
                required: '=?'
            },
            templateUrl: 'openlmis-form/openlmis-datepicker.html',
            link: link
        };

        function link(scope, element, attrs, modelCtrl) {
            setupFormatting(scope);

            if (scope.value) {
                // Populate initial value, if passed to directive
                scope.dateString = $filter('openlmisDate')(scope.value);
            }

            scope.$watch('dateString', function() {
                var date = element.find('input').datepicker('getUTCDate');
                if (!date && scope.value) {
                    date = scope.value;
                }

                scope.value = date ? date : undefined;

                if (scope.changeMethod && scope.changeMethod instanceof Function) {
                    scope.changeMethod();
                }
            });

            scope.$watch('value', function() {
                var dateString = $filter('openlmisDate')(scope.value);
                if (dateString !== scope.dateString) {
                    scope.dateString = dateString;
                }
            });
        }

        function setupFormatting(scope) {
            scope.language = messageService.getCurrentLocale();
            scope.dateFormat = angular.isDefined(scope.dateFormat) ? scope.dateFormat : DEFAULT_DATEPICKER_FORMAT;

            var datepickerSettings = jQuery.fn.datepicker.dates;
            if (!datepickerSettings[scope.language]) {
                // We explicitly pass titleFormat, because datepicker doesn't apply it automatically
                // for each language, while this property is required.
                var localization = getDatepickerLabels();
                localization.titleFormat = "MM yyyy";

                datepickerSettings[scope.language] = localization;
            }
        }

        function getDatepickerLabels() {
            var labels = {
                months: [],
                monthsShort: [],
                days: [],
                daysShort: [],
                daysMin: [],
                today: messageService.get('openlmisForm.datepicker.today'),
                clear: messageService.get('openlmisForm.datepicker.clear')
            };

            for (var i = 1; i <= 12; i++) {
                var longKey = 'openlmisForm.datepicker.monthNames.long.' + i;
                labels.months.push(messageService.get(longKey));

                var shortKey = 'openlmisForm.datepicker.monthNames.short.' + i;
                labels.monthsShort.push(messageService.get(shortKey));
            }

            for (var i = 1; i <= 7; i++) {
                var longKey = 'openlmisForm.datepicker.dayNames.long.' + i;
                labels.days.push(messageService.get(longKey));

                var shortKey = 'openlmisForm.datepicker.dayNames.short.' + i;
                labels.daysShort.push(messageService.get(shortKey));

                var minKey = 'openlmisForm.datepicker.dayNames.min.' + i;
                labels.daysMin.push(messageService.get(minKey));
            }

            return labels;
        }
    }

})();
<commit_msg>OLMIS-2833: render date only if value exists<commit_after>/*
 * This program is part of the OpenLMIS logistics management information system platform software.
 * Copyright © 2017 VillageReach
 *
 * This program is free software: you can redistribute it and/or modify it under the terms
 * of the GNU Affero General Public License as published by the Free Software Foundation, either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details. You should have received a copy of
 * the GNU Affero General Public License along with this program. If not, see
 * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.
 */

(function() {

    'use strict';

    /**
       * @ngdoc directive
       * @restrict E
       * @name openlmis-form.directive:openlmisDatepicker
       *
       * @description
       * Directive allows to add date picker input.
       *
       * @example
       * To make this directive work only 'value' attribute is required, however there are more attributes to use.
       * In order to make datepicker input use id you can add 'input-id' attribute.
       * The 'change-method' attribute takes function that will be executed after datepicker value change.
       * Datepicker directive also can take max-date and min-date attributes. Their values can be set from other datepickers or manually.
       * ```
       * <openlmis-datepicker
       * 	   value="startDate"
       *     input-id="datepicker-id"
       *     change-method="afterChange()"
       *     min-date="10/05/2016"
       *     max-date="endDate">
       * </openlmis-datepicker>
       *
       * <openlmis-datepicker
       * 	   value="endDate">
       * </openlmis-datepicker>
       * ```
       */
    angular
        .module('openlmis-form')
        .directive('openlmisDatepicker', datepicker);

    datepicker.$inject = ['$filter', 'jQuery', 'messageService', 'DEFAULT_DATE_FORMAT', 'DEFAULT_DATEPICKER_FORMAT'];

    function datepicker($filter, jQuery, messageService, DEFAULT_DATE_FORMAT, DEFAULT_DATEPICKER_FORMAT) {
        return {
            restrict: 'E',
            scope: {
                value: '=',
                inputId: '@?',
                minDate: '=?',
                maxDate: '=?',
                changeMethod: '=?',
                dateFormat: '=?',
                language: '=?',
                required: '=?'
            },
            templateUrl: 'openlmis-form/openlmis-datepicker.html',
            link: link
        };

        function link(scope, element, attrs, modelCtrl) {
            setupFormatting(scope);

            if (scope.value) {
                // Populate initial value, if passed to directive
                scope.dateString = $filter('openlmisDate')(scope.value);
            }

            scope.$watch('dateString', function() {
                var date = element.find('input').datepicker('getUTCDate');
                if (!date && scope.value) {
                    date = scope.value;
                }

                scope.value = date ? date : undefined;

                if (scope.changeMethod && scope.changeMethod instanceof Function) {
                    scope.changeMethod();
                }
            });

            scope.$watch('value', function() {
                if (scope.value) {
                    var dateString = $filter('openlmisDate')(scope.value);
                    if (dateString !== scope.dateString) {
                        scope.dateString = dateString;
                    }
                }
            });
        }

        function setupFormatting(scope) {
            scope.language = messageService.getCurrentLocale();
            scope.dateFormat = angular.isDefined(scope.dateFormat) ? scope.dateFormat : DEFAULT_DATEPICKER_FORMAT;

            var datepickerSettings = jQuery.fn.datepicker.dates;
            if (!datepickerSettings[scope.language]) {
                // We explicitly pass titleFormat, because datepicker doesn't apply it automatically
                // for each language, while this property is required.
                var localization = getDatepickerLabels();
                localization.titleFormat = "MM yyyy";

                datepickerSettings[scope.language] = localization;
            }
        }

        function getDatepickerLabels() {
            var labels = {
                months: [],
                monthsShort: [],
                days: [],
                daysShort: [],
                daysMin: [],
                today: messageService.get('openlmisForm.datepicker.today'),
                clear: messageService.get('openlmisForm.datepicker.clear')
            };

            for (var i = 1; i <= 12; i++) {
                var longKey = 'openlmisForm.datepicker.monthNames.long.' + i;
                labels.months.push(messageService.get(longKey));

                var shortKey = 'openlmisForm.datepicker.monthNames.short.' + i;
                labels.monthsShort.push(messageService.get(shortKey));
            }

            for (var i = 1; i <= 7; i++) {
                var longKey = 'openlmisForm.datepicker.dayNames.long.' + i;
                labels.days.push(messageService.get(longKey));

                var shortKey = 'openlmisForm.datepicker.dayNames.short.' + i;
                labels.daysShort.push(messageService.get(shortKey));

                var minKey = 'openlmisForm.datepicker.dayNames.min.' + i;
                labels.daysMin.push(messageService.get(minKey));
            }

            return labels;
        }
    }

})();
<|endoftext|>


<commit_before>(function (window, $, undefined) {
  'use strict';
  var HeadsUpMenu;
  HeadsUpMenu = function HeadsUpMenu(cockpit) {
    console.log('Loading HeadsUpMenu plugin in the browser.');
    var self = this;

    self.cockpit = cockpit;
    self.items = ko.observableArray();
    self.getTemplateName = function(item) { return "menuRow-" + item.type };

    this.cockpit.on(
      'headsUpMenu.register',
      function (item) {
        item.uniqueId = generateUUID();
        if (item['type'] == undefined) {
          item.type = "button";
        }
        if (item['type'] == 'custom') {
          item.headsUpTemplateId = 'custom-' + item.uniqueId;
        }
        self.items.push(item);
      });

    // Add required UI elements
    $('#video-container').append('

');
    $('#headsup-menu-base').hide();

    var menuItems = [];
    var currentSelected = -1;

    $('#headsup-menu-base').load(
      'plugin/headsup-menu/headsup.html',
      function() {

        ko.applyBindings(self, document.getElementById("headsup-menu-base"));

        $('.menuRow').hover(
          function(){ $(this).find('.btn').addClass('btn-primary') },
          function(){ $(this).find('.btn').removeClass('btn-primary') }
        );
        menuItems = $('#headsup-menu-base').find('.menuRow');
      });

    this.cockpit.emit('inputController.register',
      {
        name: "headsupMenu.show",
        description: "Show the heads up menu.",
        defaults: { keyboard: 'e', gamepad: 'START' },
        down: function() { $('#headsup-menu-base').show(); },
        up: function() {
          var currentId = $(menuItems[currentSelected]).attr('id');
          self.items()
            .forEach(function(item) {
              if (item.uniqueId == currentId) {
                item.callback();
                return;
              }
            });
          $('#headsup-menu-base').hide();
          menuItems.trigger('mouseout');
          currentSelected = -1;
        },
        secondary: [
          {
            name: "headsupMenu.next",
            description: "select the next element of the heads up menu",
            defaults: { keyboard: 'd', gamepad: 'DPAD_DOWN' },
            down: function() {
              menuItems.trigger('mouseout');
              var nextIndex = currentSelected + 1;
              if (nextIndex >= menuItems.length) { nextIndex = -1; }
              var item = menuItems[nextIndex];
              $(item).trigger('mouseover');
              currentSelected = nextIndex;
            }
          }
        ]
      });

    // for plugin management:
    this.name = "headsup-menu" // for the settings
    this.viewName = "Heads up menu"; // for the UI
    this.canBeDisabled = true;
    this.enable = function() { /* to be done */ };
    this.disable = function() { /* to be done */ };
  };

  function generateUUID(){
    var d = Date.now();
    var uuid = '4xxx-yxxx'.replace(/[xy]/g, function(c) {
      var r = (d + Math.random()*16)%16 | 0;
      d = Math.floor(d/16);
      return (c=='x' ? r : (r&0x7|0x8)).toString(16);
    });
    return uuid;
  };

  window.Cockpit.plugins.push(HeadsUpMenu);
}(window, jQuery));<commit_msg>Adding 'move previous' in heads up menu<commit_after>(function (window, $, undefined) {
  'use strict';
  var HeadsUpMenu;
  HeadsUpMenu = function HeadsUpMenu(cockpit) {
    console.log('Loading HeadsUpMenu plugin in the browser.');
    var self = this;

    self.cockpit = cockpit;
    self.items = ko.observableArray();
    self.getTemplateName = function(item) { return "menuRow-" + item.type };

    this.cockpit.on(
      'headsUpMenu.register',
      function (item) {
        item.uniqueId = generateUUID();
        if (item['type'] == undefined) {
          item.type = "button";
        }
        if (item['type'] == 'custom') {
          item.headsUpTemplateId = 'custom-' + item.uniqueId;
        }
        self.items.push(item);
      });

    // Add required UI elements
    $('#video-container').append('

');
    var headsUpMenu = $('#headsup-menu-base');
    headsUpMenu.hide();

    var menuItems = [];
    var currentSelected = -1;

    headsUpMenu.load(
      'plugin/headsup-menu/headsup.html',
      function() {

        ko.applyBindings(self, document.getElementById("headsup-menu-base"));

        $('.menuRow').hover(
          function(){ $(this).find('.btn').addClass('btn-primary') },
          function(){ $(this).find('.btn').removeClass('btn-primary') }
        );
        menuItems = headsUpMenu.find('.menuRow');
      });

    var executeMenuItem = function() {
      var currentId = $(menuItems[currentSelected]).attr('id');
      self.items()
        .forEach(function(item) {
          if (item.uniqueId == currentId) {
            item.callback();
          }
        });
      headsUpMenu.hide();
      menuItems.trigger('mouseout');
      currentSelected = -1;
    };

    var moveSelectionNext = function() {
      menuItems.trigger('mouseout');
      var nextIndex = currentSelected + 1;
      if (nextIndex >= menuItems.length) { nextIndex = -1; }
      var item = menuItems[nextIndex];
      $(item).trigger('mouseover');
      currentSelected = nextIndex;
    };

    var moveSelectionPrev = function() {
      menuItems.trigger('mouseout');
      var nextIndex = currentSelected - 1;
      if (nextIndex === -2 ) { nextIndex = menuItems.length -1; }
      var item = menuItems[nextIndex];
      $(item).trigger('mouseover');
      currentSelected = nextIndex;
    };

    this.cockpit.emit('inputController.register',
      {
        name: "headsupMenu.show",
        description: "Show the heads up menu.",
        defaults: { keyboard: 'e', gamepad: 'START' },
        down: function() { $('#headsup-menu-base').show(); },
        up: executeMenuItem,
        secondary: [
          {
            name: "headsupMenu.next",
            description: "select the next element of the heads up menu",
            defaults: { keyboard: 'c', gamepad: 'DPAD_DOWN' },
            down: moveSelectionNext
          },
          {
            name: "headsupMenu.prev",
            description: "select the previous element of the heads up menu",
            defaults: { keyboard: 'd', gamepad: 'DPAD_UP' },
            down: moveSelectionPrev
          }
        ]
      });

    // for plugin management:
    this.name = "headsup-menu" // for the settings
    this.viewName = "Heads up menu"; // for the UI
    this.canBeDisabled = true;
    this.enable = function() { /* to be done */ };
    this.disable = function() { /* to be done */ };
  };

  function generateUUID(){
    var d = Date.now();
    var uuid = '4xxx-yxxx'.replace(/[xy]/g, function(c) {
      var r = (d + Math.random()*16)%16 | 0;
      d = Math.floor(d/16);
      return (c=='x' ? r : (r&0x7|0x8)).toString(16);
    });
    return uuid;
  };

  window.Cockpit.plugins.push(HeadsUpMenu);
}(window, jQuery));<|endoftext|>


<commit_before>/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactFiberCommitWork
 * @flow
 */

'use strict';

import type { Fiber } from 'ReactFiber';
import type { HostConfig } from 'ReactFiberReconciler';

var ReactTypeOfWork = require('ReactTypeOfWork');
var {
  ClassComponent,
  HostContainer,
  HostComponent,
  HostText,
  Portal,
} = ReactTypeOfWork;
var { callCallbacks } = require('ReactFiberUpdateQueue');

var {
  Placement,
  Update,
  Callback,
} = require('ReactTypeOfSideEffect');

module.exports = function<T, P, I, TI, C>(
  config : HostConfig<T, P, I, TI, C>,
  trapError : (failedFiber : Fiber, error: Error, isUnmounting : boolean) => void
) {

  const commitUpdate = config.commitUpdate;
  const commitTextUpdate = config.commitTextUpdate;

  const appendChild = config.appendChild;
  const insertBefore = config.insertBefore;
  const removeChild = config.removeChild;

  function detachRef(current : Fiber) {
    const ref = current.ref;
    if (ref) {
      ref(null);
    }
  }

  function detachRefIfNeeded(current : ?Fiber, finishedWork : Fiber) {
    if (current) {
      const currentRef = current.ref;
      if (currentRef && currentRef !== finishedWork.ref) {
        currentRef(null);
      }
    }
  }

  function attachRef(current : ?Fiber, finishedWork : Fiber, instance : any) {
    const ref = finishedWork.ref;
    if (ref && (!current || current.ref !== ref)) {
      ref(instance);
    }
  }

  function getHostParent(fiber : Fiber) : null | I | C {
    let parent = fiber.return;
    while (parent) {
      switch (parent.tag) {
        case HostComponent:
          return parent.stateNode;
        case HostContainer:
          return parent.stateNode.containerInfo;
        case Portal:
          return parent.stateNode.containerInfo;
      }
      parent = parent.return;
    }
    return null;
  }

  function isHostParent(fiber : Fiber) : boolean {
    return (
      fiber.tag === HostComponent ||
      fiber.tag === HostContainer ||
      fiber.tag === Portal
    );
  }

  function getHostSibling(fiber : Fiber) : ?I {
    // We're going to search forward into the tree until we find a sibling host
    // node. Unfortunately, if multiple insertions are done in a row we have to
    // search past them. This leads to exponential search for the next sibling.
    // TODO: Find a more efficient way to do this.
    let node : Fiber = fiber;
    siblings: while (true) {
      // If we didn't find anything, let's try the next sibling.
      while (!node.sibling) {
        if (!node.return || isHostParent(node.return)) {
          // If we pop out of the root or hit the parent the fiber we are the
          // last sibling.
          return null;
        }
        node = node.return;
      }
      node.sibling.return = node.return;
      node = node.sibling;
      while (node.tag !== HostComponent && node.tag !== HostText) {
        // If it is not host node and, we might have a host node inside it.
        // Try to search down until we find one.
        // TODO: For coroutines, this will have to search the stateNode.
        if (node.effectTag & Placement) {
          // If we don't have a child, try the siblings instead.
          continue siblings;
        }
        if (!node.child) {
          continue siblings;
        } else {
          node.child.return = node;
          node = node.child;
        }
      }
      // Check if this host node is stable or about to be placed.
      if (!(node.effectTag & Placement)) {
        // Found it!
        return node.stateNode;
      }
    }
  }

  function commitInsertion(finishedWork : Fiber) : void {
    // Recursively insert all host nodes into the parent.
    const parent = getHostParent(finishedWork);
    if (!parent) {
      return;
    }
    const before = getHostSibling(finishedWork);
    // We only have the top Fiber that was inserted but we need recurse down its
    // children to find all the terminal nodes.
    let node : Fiber = finishedWork;
    while (true) {
      if (node.tag === HostComponent || node.tag === HostText) {
        if (before) {
          insertBefore(parent, node.stateNode, before);
        } else {
          appendChild(parent, node.stateNode);
        }
      } else if (node.tag === Portal) {
        // If the insertion itself is a portal, then we don't want to traverse
        // down its children. Instead, we'll get insertions from each child in
        // the portal directly.
      } else if (node.child) {
        // TODO: Coroutines need to visit the stateNode.
        node = node.child;
        continue;
      }
      if (node === finishedWork) {
        return;
      }
      while (!node.sibling) {
        if (!node.return || node.return === finishedWork) {
          return;
        }
        node = node.return;
      }
      node = node.sibling;
    }
  }

  function commitNestedUnmounts(root : Fiber): void {
    // While we're inside a removed host node we don't want to call
    // removeChild on the inner nodes because they're removed by the top
    // call anyway. We also want to call componentWillUnmount on all
    // composites before this host node is removed from the tree. Therefore
    // we do an inner loop while we're still inside the host node.
    let node : Fiber = root;
    while (true) {
      commitUnmount(node);
      if (node.child) {
        // TODO: Coroutines need to visit the stateNode.
        node.child.return = node;
        node = node.child;
        continue;
      }
      if (node === root) {
        return;
      }
      while (!node.sibling) {
        if (!node.return || node.return === root) {
          return;
        }
        node = node.return;
      }
      node.sibling.return = node.return;
      node = node.sibling;
    }
  }

  function unmountHostComponents(parent, current): void {
    // We only have the top Fiber that was inserted but we need recurse down its
    // children to find all the terminal nodes.
    let node : Fiber = current;
    while (true) {
      if (node.tag === HostComponent || node.tag === HostText) {
        commitNestedUnmounts(node);
        // After all the children have unmounted, it is now safe to remove the
        // node from the tree.
        if (parent) {
          removeChild(parent, node.stateNode);
        }
      } else if (node.tag === Portal) {
        // If this is a portal, then the parent is actually the portal itself.
        // We need to keep track of which parent we're removing from.
        // TODO: This uses a recursive call. We can get rid of that by mutating
        // the parent binding and restoring it by searching for the host parent
        // again when we pop past a portal.
        const portalParent = node.stateNode.containerInfo;
        let child = node.child;
        while (child) {
          unmountHostComponents(portalParent, child);
          child = child.sibling;
        }
      } else {
        commitUnmount(node);
        if (node.child) {
          // TODO: Coroutines need to visit the stateNode.
          node.child.return = node;
          node = node.child;
          continue;
        }
      }
      if (node === current) {
        return;
      }
      while (!node.sibling) {
        if (!node.return || node.return === current) {
          return;
        }
        node = node.return;
      }
      node.sibling.return = node.return;
      node = node.sibling;
    }
  }

  function commitDeletion(current : Fiber) : void {
    // Recursively delete all host nodes from the parent.
    const parent = getHostParent(current);
    // Detach refs and call componentWillUnmount() on the whole subtree.
    unmountHostComponents(parent, current);

    // Cut off the return pointers to disconnect it from the tree. Ideally, we
    // should clear the child pointer of the parent alternate to let this
    // get GC:ed but we don't know which for sure which parent is the current
    // one so we'll settle for GC:ing the subtree of this child. This child
    // itself will be GC:ed when the parent updates the next time.
    current.return = null;
    current.child = null;
    if (current.alternate) {
      current.alternate.child = null;
      current.alternate.return = null;
    }
  }

  function commitUnmount(current : Fiber) : void {
    switch (current.tag) {
      case ClassComponent: {
        detachRef(current);
        const instance = current.stateNode;
        if (typeof instance.componentWillUnmount === 'function') {
          const error = tryCallComponentWillUnmount(instance);
          if (error) {
            trapError(current, error, true);
          }
        }
        return;
      }
      case HostComponent: {
        detachRef(current);
        return;
      }
    }
  }

  function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
    switch (finishedWork.tag) {
      case ClassComponent: {
        detachRefIfNeeded(current, finishedWork);
        return;
      }
      case HostComponent: {
        const instance : I = finishedWork.stateNode;
        if (instance != null && current) {
          // Commit the work prepared earlier.
          const newProps = finishedWork.memoizedProps;
          const oldProps = current.memoizedProps;
          commitUpdate(instance, oldProps, newProps, finishedWork);
        }
        detachRefIfNeeded(current, finishedWork);
        return;
      }
      case HostText: {
        if (finishedWork.stateNode == null || !current) {
          throw new Error('This should only be done during updates.');
        }
        const textInstance : TI = finishedWork.stateNode;
        const newText : string = finishedWork.memoizedProps;
        const oldText : string = current.memoizedProps;
        commitTextUpdate(textInstance, oldText, newText);
        return;
      }
      case HostContainer: {
        return;
      }
      case Portal: {
        return;
      }
      default:
        throw new Error('This unit of work tag should not have side-effects.');
    }
  }

  function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : void {
    switch (finishedWork.tag) {
      case ClassComponent: {
        const instance = finishedWork.stateNode;
        let firstError = null;
        if (finishedWork.effectTag & Update) {
          if (!current) {
            if (typeof instance.componentDidMount === 'function') {
              firstError = tryCallComponentDidMount(instance);
            }
          } else {
            if (typeof instance.componentDidUpdate === 'function') {
              const prevProps = current.memoizedProps;
              const prevState = current.memoizedState;
              firstError = tryCallComponentDidUpdate(instance, prevProps, prevState);
            }
          }
          attachRef(current, finishedWork, instance);
        }
        // Clear updates from current fiber.
        if (finishedWork.alternate) {
          finishedWork.alternate.updateQueue = null;
        }
        if (finishedWork.effectTag & Callback) {
          if (finishedWork.callbackList) {
            const callbackError = callCallbacks(finishedWork.callbackList, instance);
            firstError = firstError || callbackError;
            finishedWork.callbackList = null;
          }
        }
        if (firstError) {
          trapError(finishedWork, firstError, false);
        }
        return;
      }
      case HostContainer: {
        const rootFiber = finishedWork.stateNode;
        let firstError = null;
        if (rootFiber.callbackList) {
          const { callbackList } = rootFiber;
          rootFiber.callbackList = null;
          firstError = callCallbacks(callbackList, rootFiber.current.child.stateNode);
        }
        if (firstError) {
          trapError(rootFiber, firstError, false);
        }
        return;
      }
      case HostComponent: {
        const instance : I = finishedWork.stateNode;
        attachRef(current, finishedWork, instance);
        return;
      }
      case HostText: {
        // We have no life-cycles associated with text.
        return;
      }
      case Portal: {
        // We have no life-cycles associated with portals.
        return;
      }
      default:
        throw new Error('This unit of work tag should not have side-effects.');
    }
  }

  function tryCallComponentDidMount(instance) {
    try {
      instance.componentDidMount();
      return null;
    } catch (error) {
      return error;
    }
  }

  function tryCallComponentDidUpdate(instance, prevProps, prevState) {
    try {
      instance.componentDidUpdate(prevProps, prevState);
      return null;
    } catch (error) {
      return error;
    }
  }

  function tryCallComponentWillUnmount(instance) {
    try {
      instance.componentWillUnmount();
      return null;
    } catch (error) {
      return error;
    }
  }

  return {
    commitInsertion,
    commitDeletion,
    commitWork,
    commitLifeCycles,
  };

};
<commit_msg>Assert that we always find a parent for DOM mutations<commit_after>/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactFiberCommitWork
 * @flow
 */

'use strict';

import type { Fiber } from 'ReactFiber';
import type { HostConfig } from 'ReactFiberReconciler';

var ReactTypeOfWork = require('ReactTypeOfWork');
var {
  ClassComponent,
  HostContainer,
  HostComponent,
  HostText,
  Portal,
} = ReactTypeOfWork;
var { callCallbacks } = require('ReactFiberUpdateQueue');

var {
  Placement,
  Update,
  Callback,
} = require('ReactTypeOfSideEffect');

module.exports = function<T, P, I, TI, C>(
  config : HostConfig<T, P, I, TI, C>,
  trapError : (failedFiber : Fiber, error: Error, isUnmounting : boolean) => void
) {

  const commitUpdate = config.commitUpdate;
  const commitTextUpdate = config.commitTextUpdate;

  const appendChild = config.appendChild;
  const insertBefore = config.insertBefore;
  const removeChild = config.removeChild;

  function detachRef(current : Fiber) {
    const ref = current.ref;
    if (ref) {
      ref(null);
    }
  }

  function detachRefIfNeeded(current : ?Fiber, finishedWork : Fiber) {
    if (current) {
      const currentRef = current.ref;
      if (currentRef && currentRef !== finishedWork.ref) {
        currentRef(null);
      }
    }
  }

  function attachRef(current : ?Fiber, finishedWork : Fiber, instance : any) {
    const ref = finishedWork.ref;
    if (ref && (!current || current.ref !== ref)) {
      ref(instance);
    }
  }

  function getHostParent(fiber : Fiber) : I | C {
    let parent = fiber.return;
    while (parent) {
      switch (parent.tag) {
        case HostComponent:
          return parent.stateNode;
        case HostContainer:
          return parent.stateNode.containerInfo;
        case Portal:
          return parent.stateNode.containerInfo;
      }
      parent = parent.return;
    }
    throw new Error('Expected to find a host parent.');
  }

  function isHostParent(fiber : Fiber) : boolean {
    return (
      fiber.tag === HostComponent ||
      fiber.tag === HostContainer ||
      fiber.tag === Portal
    );
  }

  function getHostSibling(fiber : Fiber) : ?I {
    // We're going to search forward into the tree until we find a sibling host
    // node. Unfortunately, if multiple insertions are done in a row we have to
    // search past them. This leads to exponential search for the next sibling.
    // TODO: Find a more efficient way to do this.
    let node : Fiber = fiber;
    siblings: while (true) {
      // If we didn't find anything, let's try the next sibling.
      while (!node.sibling) {
        if (!node.return || isHostParent(node.return)) {
          // If we pop out of the root or hit the parent the fiber we are the
          // last sibling.
          return null;
        }
        node = node.return;
      }
      node.sibling.return = node.return;
      node = node.sibling;
      while (node.tag !== HostComponent && node.tag !== HostText) {
        // If it is not host node and, we might have a host node inside it.
        // Try to search down until we find one.
        // TODO: For coroutines, this will have to search the stateNode.
        if (node.effectTag & Placement) {
          // If we don't have a child, try the siblings instead.
          continue siblings;
        }
        if (!node.child) {
          continue siblings;
        } else {
          node.child.return = node;
          node = node.child;
        }
      }
      // Check if this host node is stable or about to be placed.
      if (!(node.effectTag & Placement)) {
        // Found it!
        return node.stateNode;
      }
    }
  }

  function commitInsertion(finishedWork : Fiber) : void {
    // Recursively insert all host nodes into the parent.
    const parent = getHostParent(finishedWork);
    const before = getHostSibling(finishedWork);
    // We only have the top Fiber that was inserted but we need recurse down its
    // children to find all the terminal nodes.
    let node : Fiber = finishedWork;
    while (true) {
      if (node.tag === HostComponent || node.tag === HostText) {
        if (before) {
          insertBefore(parent, node.stateNode, before);
        } else {
          appendChild(parent, node.stateNode);
        }
      } else if (node.tag === Portal) {
        // If the insertion itself is a portal, then we don't want to traverse
        // down its children. Instead, we'll get insertions from each child in
        // the portal directly.
      } else if (node.child) {
        // TODO: Coroutines need to visit the stateNode.
        node = node.child;
        continue;
      }
      if (node === finishedWork) {
        return;
      }
      while (!node.sibling) {
        if (!node.return || node.return === finishedWork) {
          return;
        }
        node = node.return;
      }
      node = node.sibling;
    }
  }

  function commitNestedUnmounts(root : Fiber): void {
    // While we're inside a removed host node we don't want to call
    // removeChild on the inner nodes because they're removed by the top
    // call anyway. We also want to call componentWillUnmount on all
    // composites before this host node is removed from the tree. Therefore
    // we do an inner loop while we're still inside the host node.
    let node : Fiber = root;
    while (true) {
      commitUnmount(node);
      if (node.child) {
        // TODO: Coroutines need to visit the stateNode.
        node.child.return = node;
        node = node.child;
        continue;
      }
      if (node === root) {
        return;
      }
      while (!node.sibling) {
        if (!node.return || node.return === root) {
          return;
        }
        node = node.return;
      }
      node.sibling.return = node.return;
      node = node.sibling;
    }
  }

  function unmountHostComponents(parent, current): void {
    // We only have the top Fiber that was inserted but we need recurse down its
    // children to find all the terminal nodes.
    let node : Fiber = current;
    while (true) {
      if (node.tag === HostComponent || node.tag === HostText) {
        commitNestedUnmounts(node);
        // After all the children have unmounted, it is now safe to remove the
        // node from the tree.
        removeChild(parent, node.stateNode);
      } else if (node.tag === Portal) {
        // If this is a portal, then the parent is actually the portal itself.
        // We need to keep track of which parent we're removing from.
        // TODO: This uses a recursive call. We can get rid of that by mutating
        // the parent binding and restoring it by searching for the host parent
        // again when we pop past a portal.
        const portalParent = node.stateNode.containerInfo;
        let child = node.child;
        while (child) {
          unmountHostComponents(portalParent, child);
          child = child.sibling;
        }
      } else {
        commitUnmount(node);
        if (node.child) {
          // TODO: Coroutines need to visit the stateNode.
          node.child.return = node;
          node = node.child;
          continue;
        }
      }
      if (node === current) {
        return;
      }
      while (!node.sibling) {
        if (!node.return || node.return === current) {
          return;
        }
        node = node.return;
      }
      node.sibling.return = node.return;
      node = node.sibling;
    }
  }

  function commitDeletion(current : Fiber) : void {
    // Recursively delete all host nodes from the parent.
    const parent = getHostParent(current);
    // Detach refs and call componentWillUnmount() on the whole subtree.
    unmountHostComponents(parent, current);

    // Cut off the return pointers to disconnect it from the tree. Ideally, we
    // should clear the child pointer of the parent alternate to let this
    // get GC:ed but we don't know which for sure which parent is the current
    // one so we'll settle for GC:ing the subtree of this child. This child
    // itself will be GC:ed when the parent updates the next time.
    current.return = null;
    current.child = null;
    if (current.alternate) {
      current.alternate.child = null;
      current.alternate.return = null;
    }
  }

  function commitUnmount(current : Fiber) : void {
    switch (current.tag) {
      case ClassComponent: {
        detachRef(current);
        const instance = current.stateNode;
        if (typeof instance.componentWillUnmount === 'function') {
          const error = tryCallComponentWillUnmount(instance);
          if (error) {
            trapError(current, error, true);
          }
        }
        return;
      }
      case HostComponent: {
        detachRef(current);
        return;
      }
    }
  }

  function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
    switch (finishedWork.tag) {
      case ClassComponent: {
        detachRefIfNeeded(current, finishedWork);
        return;
      }
      case HostComponent: {
        const instance : I = finishedWork.stateNode;
        if (instance != null && current) {
          // Commit the work prepared earlier.
          const newProps = finishedWork.memoizedProps;
          const oldProps = current.memoizedProps;
          commitUpdate(instance, oldProps, newProps, finishedWork);
        }
        detachRefIfNeeded(current, finishedWork);
        return;
      }
      case HostText: {
        if (finishedWork.stateNode == null || !current) {
          throw new Error('This should only be done during updates.');
        }
        const textInstance : TI = finishedWork.stateNode;
        const newText : string = finishedWork.memoizedProps;
        const oldText : string = current.memoizedProps;
        commitTextUpdate(textInstance, oldText, newText);
        return;
      }
      case HostContainer: {
        return;
      }
      case Portal: {
        return;
      }
      default:
        throw new Error('This unit of work tag should not have side-effects.');
    }
  }

  function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : void {
    switch (finishedWork.tag) {
      case ClassComponent: {
        const instance = finishedWork.stateNode;
        let firstError = null;
        if (finishedWork.effectTag & Update) {
          if (!current) {
            if (typeof instance.componentDidMount === 'function') {
              firstError = tryCallComponentDidMount(instance);
            }
          } else {
            if (typeof instance.componentDidUpdate === 'function') {
              const prevProps = current.memoizedProps;
              const prevState = current.memoizedState;
              firstError = tryCallComponentDidUpdate(instance, prevProps, prevState);
            }
          }
          attachRef(current, finishedWork, instance);
        }
        // Clear updates from current fiber.
        if (finishedWork.alternate) {
          finishedWork.alternate.updateQueue = null;
        }
        if (finishedWork.effectTag & Callback) {
          if (finishedWork.callbackList) {
            const callbackError = callCallbacks(finishedWork.callbackList, instance);
            firstError = firstError || callbackError;
            finishedWork.callbackList = null;
          }
        }
        if (firstError) {
          trapError(finishedWork, firstError, false);
        }
        return;
      }
      case HostContainer: {
        const rootFiber = finishedWork.stateNode;
        let firstError = null;
        if (rootFiber.callbackList) {
          const { callbackList } = rootFiber;
          rootFiber.callbackList = null;
          firstError = callCallbacks(callbackList, rootFiber.current.child.stateNode);
        }
        if (firstError) {
          trapError(rootFiber, firstError, false);
        }
        return;
      }
      case HostComponent: {
        const instance : I = finishedWork.stateNode;
        attachRef(current, finishedWork, instance);
        return;
      }
      case HostText: {
        // We have no life-cycles associated with text.
        return;
      }
      case Portal: {
        // We have no life-cycles associated with portals.
        return;
      }
      default:
        throw new Error('This unit of work tag should not have side-effects.');
    }
  }

  function tryCallComponentDidMount(instance) {
    try {
      instance.componentDidMount();
      return null;
    } catch (error) {
      return error;
    }
  }

  function tryCallComponentDidUpdate(instance, prevProps, prevState) {
    try {
      instance.componentDidUpdate(prevProps, prevState);
      return null;
    } catch (error) {
      return error;
    }
  }

  function tryCallComponentWillUnmount(instance) {
    try {
      instance.componentWillUnmount();
      return null;
    } catch (error) {
      return error;
    }
  }

  return {
    commitInsertion,
    commitDeletion,
    commitWork,
    commitLifeCycles,
  };

};
<|endoftext|>


<commit_before>import { stringify } from 'qs'
import { duration } from 'moment'
import { caching } from 'cache-manager'
import isNil from 'lodash.isnil'
import fetch from 'isomorphic-fetch'

import { parseCardNumber, parseSkipass } from '../../parsers/bukovel-ticket'

const memoryCache = caching({
  store: 'memory',
  max: 100,
  // time to live
  ttl: duration(1, 'h').asSeconds()
})

const BUKOVEL_TICKETS_URL = 'http://tickets.bukovel.com/'
const getResponseText = response => {
  if (!response.ok) {
    return Promise.reject(`got not OK (${response.status}) response from ${response.url}`)
  }

  return response.text()
}

const getSkipassCardNumber = id => {
  if (isNil(id)) {
    return Promise.reject('id is required')
  }

  const params = {
    NumTicket: id
  }
  const url = `${BUKOVEL_TICKETS_URL}?${stringify(params)}`

  return fetch(url).then(getResponseText).then(parseCardNumber)
}

const getSkipassCardNumberCached = id => {
  return memoryCache.wrap(id, () => getSkipassCardNumber(id))
}

const getSkipass = async id => {
  const cardNumber = await getSkipassCardNumberCached(id)
  const params = {
    NumTicket: id,
    Card: cardNumber
  }
  const url = `${BUKOVEL_TICKETS_URL}?${stringify(params)}`

  return fetch(url).then(getResponseText).then(parseSkipass)
}

export {
  getSkipassCardNumberCached as getSkipassCardNumber,
  getSkipass
}
<commit_msg>increase caching time for cardNumber<commit_after>import { stringify } from 'qs'
import { caching } from 'cache-manager'
import isNil from 'lodash.isnil'
import fetch from 'isomorphic-fetch'

import { parseCardNumber, parseSkipass } from '../../parsers/bukovel-ticket'

const cardNumberMemoryCache = caching({
  store: 'memory',
  max: 100
})

const BUKOVEL_TICKETS_URL = 'http://tickets.bukovel.com/'
const getResponseText = response => {
  if (!response.ok) {
    return Promise.reject(`got not OK (${response.status}) response from ${response.url}`)
  }

  return response.text()
}

const getSkipassCardNumber = id => {
  if (isNil(id)) {
    return Promise.reject('id is required')
  }

  const params = {
    NumTicket: id
  }
  const url = `${BUKOVEL_TICKETS_URL}?${stringify(params)}`

  return fetch(url).then(getResponseText).then(parseCardNumber)
}

const getSkipassCardNumberCached = id => {
  return cardNumberMemoryCache.wrap(id, () => getSkipassCardNumber(id))
}

const getSkipass = async id => {
  const cardNumber = await getSkipassCardNumberCached(id)
  const params = {
    NumTicket: id,
    Card: cardNumber
  }
  const url = `${BUKOVEL_TICKETS_URL}?${stringify(params)}`

  return fetch(url).then(getResponseText).then(parseSkipass)
}

export {
  getSkipassCardNumberCached as getSkipassCardNumber,
  getSkipass
}
<|endoftext|>