lib/network/modules/components/shared/LabelAccumulator.js

Summary

Maintainability
B
5 hrs
Test Coverage
/**
 * Callback to determine text dimensions, using the parent label settings.
 * @callback MeasureText
 * @param {text} text
 * @param {text} mod
 * @return {Object} { width, values} width in pixels and font attributes
 */


/**
 * Helper class for Label which collects results of splitting labels into lines and blocks.
 *
 * @private
 */
class LabelAccumulator {

  /**
   * @param {MeasureText} measureText
   */
  constructor(measureText) {
    this.measureText = measureText;
    this.current = 0;
    this.width   = 0;
    this.height  = 0;
    this.lines   = [];
  }


  /**
   * Append given text to the given line.
   *
   * @param {number}  l    index of line to add to
   * @param {string}  text string to append to line
   * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
   * @private
   */
  _add(l, text, mod = 'normal') { 

    if (this.lines[l] === undefined) {
      this.lines[l] = {
        width : 0,
        height: 0,
        blocks: []
      };
    }

    // We still need to set a block for undefined and empty texts, hence return at this point
    // This is necessary because we don't know at this point if we're at the
    // start of an empty line or not.
    // To compensate, empty blocks are removed in `finalize()`.
    //
    // Empty strings should still have a height
    let tmpText = text;
    if (text === undefined || text === "") tmpText = " ";

    // Determine width and get the font properties
    let result = this.measureText(tmpText, mod);
    let block = Object.assign({}, result.values);
    block.text  = text;
    block.width = result.width;
    block.mod   = mod;

    if (text === undefined || text === "") {
      block.width = 0;
    }

    this.lines[l].blocks.push(block);

    // Update the line width. We need this for determining if a string goes over max width
    this.lines[l].width += block.width;
  }


  /**
   * Returns the width in pixels of the current line.
   *
   * @returns {number}
   */
  curWidth() {
    let line = this.lines[this.current];
    if (line === undefined) return 0;

    return line.width;
  }


   /**
    * Add text in block to current line
    *
    * @param {string} text
    * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
    */
   append(text, mod = 'normal') { 
     this._add(this.current, text, mod);
   }


  /**
   * Add text in block to current line and start a new line
   *
   * @param {string} text
   * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
   */
  newLine(text, mod = 'normal') {
    this._add(this.current, text, mod);
    this.current++;
  }


  /**
   * Determine and set the heights of all the lines currently contained in this instance
   *
   * Note that width has already been set.
   * 
   * @private
   */
  determineLineHeights() {
    for (let k = 0; k < this.lines.length; k++) {
      let line   = this.lines[k];

      // Looking for max height of blocks in line
      let height = 0;

      if (line.blocks !== undefined) {  // Can happen if text contains e.g. '\n '
        for (let l = 0; l < line.blocks.length; l++) {
          let block =  line.blocks[l];

          if (height < block.height) {
            height = block.height;
          }
        }
      }
  
      line.height = height;
    }
  }


  /**
   * Determine the full size of the label text, as determined by current lines and blocks
   * 
   * @private
   */
  determineLabelSize() {
    let width  = 0;
    let height = 0;
    for (let k = 0; k < this.lines.length; k++) {
      let line   = this.lines[k];

      if (line.width > width) {
        width = line.width;
      }
      height += line.height;
    }

    this.width  = width;
    this.height = height;
  }


  /**
   * Remove all empty blocks and empty lines we don't need
   * 
   * This must be done after the width/height determination,
   * so that these are set properly for processing here.
   *
   * @returns {Array<Line>} Lines with empty blocks (and some empty lines) removed
   * @private
   */
  removeEmptyBlocks() {
    let tmpLines = [];
    for (let k = 0; k < this.lines.length; k++) {
      let line   = this.lines[k];

      // Note: an empty line in between text has width zero but is still relevant to layout.
      // So we can't use width for testing empty line here
      if (line.blocks.length === 0) continue;

      // Discard final empty line always
      if(k === this.lines.length - 1) {
        if (line.width === 0) continue;
      }

      let tmpLine = {};
      Object.assign(tmpLine, line);
      tmpLine.blocks = [];

      let firstEmptyBlock;
      let tmpBlocks = []
      for (let l = 0; l < line.blocks.length; l++) {
        let block = line.blocks[l];
        if (block.width !== 0) {
          tmpBlocks.push(block);
        } else {
          if (firstEmptyBlock === undefined) {
            firstEmptyBlock = block;
          }
        }
      }

      // Ensure that there is *some* text present
      if (tmpBlocks.length === 0 && firstEmptyBlock !== undefined) {
        tmpBlocks.push(firstEmptyBlock);
      }

      tmpLine.blocks = tmpBlocks;

      tmpLines.push(tmpLine);
    }

    return tmpLines;
  }


  /**
   * Set the sizes for all lines and the whole thing.
   *
   * @returns {{width: (number|*), height: (number|*), lines: Array}}
   */
  finalize() {
    //console.log(JSON.stringify(this.lines, null, 2));

    this.determineLineHeights();
    this.determineLabelSize();
    let tmpLines = this.removeEmptyBlocks();


    // Return a simple hash object for further processing.
    return {
      width : this.width,
      height: this.height,
      lines : tmpLines
    }
  }
} 


export default LabelAccumulator;