text: // This file defines a ProseMirror selection subclass that models
  // table cell selections. The table plugin needs to be active to wire
  // in the user interaction part of table selections (so that you
  // actually get such selections when you select across cells).
  
  import {Selection, TextSelection, NodeSelection, SelectionRange} from "prosemirror-state"
  import {Decoration, DecorationSet} from "prosemirror-view"
  import {Fragment, Slice} from "prosemirror-model"
  
  
  import {cellAround, inSameTable, isPosInTable, pointsAtCell, setAttr, rmColSpan} from "./util"
  import {TableMap} from "./tablemap"
  
  // ::- A [`Selection`](http://prosemirror.net/docs/ref/#state.Selection)
  // subclass that represents a cell selection spanning part of a table.
  // With the plugin enabled, these will be created when the user
  // selects across cells, and will be drawn by giving selected cells a
  // `selectedCell` CSS class.
  export class CellSelection extends Selection {
    // :: (ResolvedPos, ?ResolvedPos)
    // A table selection is identified by its anchor and head cells. The
    // positions given to this constructor should point _before_ two
    // cells in the same table. They may be the same, to select a single
    // cell.
    constructor($anchorCell, $headCell = $anchorCell) {
      let table = $anchorCell.node(-1), map = TableMap.get(table), start = $anchorCell.start(-1)
      let rect = map.rectBetween($anchorCell.pos - start, $headCell.pos - start)
      let doc = $anchorCell.node(0)
      let cells = map.cellsInRect(rect).filter(p => p != $headCell.pos - start)
      // Make the head cell the first range, so that it counts as the
      // primary part of the selection
      cells.unshift($headCell.pos - start)
      let ranges = cells.map(pos => {
        let cell = table.nodeAt(pos), from = pos + start + 1
        return new SelectionRange(doc.resolve(from), doc.resolve(from + cell.content.size))
      })
      super(ranges[0].$from, ranges[0].$to, ranges)
      // :: ResolvedPos
      // A resolved position pointing _in front of_ the anchor cell (the one
      // that doesn't move when extending the selection).
      this.$anchorCell = $anchorCell
      // :: ResolvedPos
      // A resolved position pointing in front of the head cell (the one
      // moves when extending the selection).
      this.$headCell = $headCell
    }
  
    map(doc, mapping) {
      let $anchorCell = doc.resolve(mapping.map(this.$anchorCell.pos))
      let $headCell = doc.resolve(mapping.map(this.$headCell.pos))
      if (pointsAtCell($anchorCell) && pointsAtCell($headCell) && inSameTable($anchorCell, $headCell)) {
        let tableChanged = this.$anchorCell.node(-1) != $anchorCell.node(-1)
        if (tableChanged && this.isRowSelection())
          return CellSelection.rowSelection($anchorCell, $headCell)
        else if (tableChanged && this.isColSelection())
          return CellSelection.colSelection($anchorCell, $headCell)
        else
          return new CellSelection($anchorCell, $headCell)
      }
      return TextSelection.between($anchorCell, $headCell)
    }
  
    // :: () → Slice
    // Returns a rectangular slice of table rows containing the selected
    // cells.
    content() {
      let table = this.$anchorCell.node(-1), map = TableMap.get(table), start = this.$anchorCell.start(-1)
      let rect = map.rectBetween(this.$anchorCell.pos - start, this.$headCell.pos - start)
      let seen = {}, rows = []
      for (let row = rect.top; row < rect.bottom; row++) {
        let rowContent = []
        for (let index = row * map.width + rect.left, col = rect.left; col < rect.right; col++, index++) {
          let pos = map.map[index]
          if (!seen[pos]) {
            seen[pos] = true
            let cellRect = map.findCell(pos), cell = table.nodeAt(pos)
            let extraLeft = rect.left - cellRect.left, extraRight = cellRect.right - rect.right
            if (extraLeft > 0 || extraRight > 0) {
              let attrs = cell.attrs
              if (extraLeft > 0) attrs = rmColSpan(attrs, 0, extraLeft)
              if (extraRight > 0) attrs = rmColSpan(attrs, attrs.colspan - extraRight, extraRight)
              if (cellRect.left < rect.left) cell = cell.type.createAndFill(attrs)
              else cell = cell.type.create(attrs, cell.content)
            }
            if (cellRect.top < rect.top || cellRect.bottom > rect.bottom) {
              let attrs = setAttr(cell.attrs, "rowspan", Math.min(cellRect.bottom, rect.bottom) - Math.max(cellRect.top, rect.top))
              if (cellRect.top < rect.top) cell = cell.type.createAndFill(attrs)
              else cell = cell.type.create(attrs, cell.content)
            }
            rowContent.push(cell)
          }
        }
        rows.push(table.child(row).copy(Fragment.from(rowContent)))
      }
  
      const fragment = this.isColSelection() && this.isRowSelection() ? table : rows;
      return new Slice(Fragment.from(fragment), 1, 1)
    }
  
    replace(tr, content = Slice.empty) {
      let mapFrom = tr.steps.length, ranges = this.ranges
      for (let i = 0; i < ranges.length; i++) {
        let {$from, $to} = ranges[i], mapping = tr.mapping.slice(mapFrom)
        tr.replace(mapping.map($from.pos), mapping.map($to.pos), i ? Slice.empty : content)
      }
      let sel = Selection.findFrom(tr.doc.resolve(tr.mapping.slice(mapFrom).map(this.to)), -1)
      if (sel) tr.setSelection(sel)
    }
  
    replaceWith(tr, node) {
      this.replace(tr, new Slice(Fragment.from(node), 0, 0))
    }
  
    forEachCell(f) {
      let table = this.$anchorCell.node(-1), map = TableMap.get(table), start = this.$anchorCell.start(-1)
      let cells = map.cellsInRect(map.rectBetween(this.$anchorCell.pos - start, this.$headCell.pos - start))
      for (let i = 0; i < cells.length; i++)
        f(table.nodeAt(cells[i]), start + cells[i])
    }
  
    // :: () → bool
    // True if this selection goes all the way from the top to the
    // bottom of the table.
    isColSelection() {
      let anchorTop = this.$anchorCell.index(-1), headTop = this.$headCell.index(-1)
      if (Math.min(anchorTop, headTop) > 0) return false
      let anchorBot = anchorTop + this.$anchorCell.nodeAfter.attrs.rowspan,
          headBot = headTop + this.$headCell.nodeAfter.attrs.rowspan
      return Math.max(anchorBot, headBot) == this.$headCell.node(-1).childCount
    }
  
    // :: (ResolvedPos, ?ResolvedPos) → CellSelection
    // Returns the smallest column selection that covers the given anchor
    // and head cell.
    static colSelection($anchorCell, $headCell = $anchorCell) {
      let map = TableMap.get($anchorCell.node(-1)), start = $anchorCell.start(-1)
      let anchorRect = map.findCell($anchorCell.pos - start), headRect = map.findCell($headCell.pos - start)
      let doc = $anchorCell.node(0)
      if (anchorRect.top <= headRect.top) {
        if (anchorRect.top > 0)
          $anchorCell = doc.resolve(start + map.map[anchorRect.left])
        if (headRect.bottom < map.height)
          $headCell = doc.resolve(start + map.map[map.width * (map.height - 1) + headRect.right - 1])
      } else {
        if (headRect.top > 0)
          $headCell = doc.resolve(start + map.map[headRect.left])
        if (anchorRect.bottom < map.height)
          $anchorCell = doc.resolve(start + map.map[map.width * (map.height - 1) + anchorRect.right - 1])
      }
      return new CellSelection($anchorCell, $headCell)
    }
  
    // :: () → bool
    // True if this selection goes all the way from the left to the
    // right of the table.
    isRowSelection() {
      let map = TableMap.get(this.$anchorCell.node(-1)), start = this.$anchorCell.start(-1)
      let anchorLeft = map.colCount(this.$anchorCell.pos - start),
          headLeft = map.colCount(this.$headCell.pos - start)
      if (Math.min(anchorLeft, headLeft) > 0) return false
      let anchorRight = anchorLeft + this.$anchorCell.nodeAfter.attrs.colspan,
          headRight = headLeft + this.$headCell.nodeAfter.attrs.colspan
      return Math.max(anchorRight, headRight) == map.width
    }
  
    eq(other) {
      return other instanceof CellSelection && other.$anchorCell.pos == this.$anchorCell.pos &&
        other.$headCell.pos == this.$headCell.pos
    }
  
    // :: (ResolvedPos, ?ResolvedPos) → CellSelection
    // Returns the smallest row selection that covers the given anchor
    // and head cell.
    static rowSelection($anchorCell, $headCell = $anchorCell) {
      let map = TableMap.get($anchorCell.node(-1)), start = $anchorCell.start(-1)
      let anchorRect = map.findCell($anchorCell.pos - start), headRect = map.findCell($headCell.pos - start)
      let doc = $anchorCell.node(0)
      if (anchorRect.left <= headRect.left) {
        if (anchorRect.left > 0)
          $anchorCell = doc.resolve(start + map.map[anchorRect.top * map.width])
        if (headRect.right < map.width)
          $headCell = doc.resolve(start + map.map[map.width * (headRect.top + 1) - 1])
      } else {
        if (headRect.left > 0)
          $headCell = doc.resolve(start + map.map[headRect.top * map.width])
        if (anchorRect.right < map.width)
          $anchorCell = doc.resolve(start + map.map[map.width * (anchorRect.top + 1) - 1])
      }
      return new CellSelection($anchorCell, $headCell)
    }
  
    toJSON() {
      return {type: "cell", anchor: this.$anchorCell.pos, head: this.$headCell.pos}
    }
  
    static fromJSON(doc, json) {
      return new CellSelection(doc.resolve(json.anchor), doc.resolve(json.head))
    }
  
    // :: (Node, number, ?number) → CellSelection
    static create(doc, anchorCell, headCell = anchorCell) {
      return new CellSelection(doc.resolve(anchorCell), doc.resolve(headCell))
    }
  
    getBookmark() { return new CellBookmark(this.$anchorCell.pos, this.$headCell.pos) }
  }
  
  CellSelection.prototype.visible = false
  
  Selection.jsonID("cell", CellSelection)
  
  class CellBookmark {
    constructor(anchor, head) {
      this.anchor = anchor
      this.head = head
    }
    map(mapping) {
      return new CellBookmark(mapping.map(this.anchor), mapping.map(this.head))
    }
    resolve(doc) {
      let $anchorCell = doc.resolve(this.anchor), $headCell = doc.resolve(this.head)
      if ($anchorCell.parent.type.spec.tableRole == "row" &&
          $headCell.parent.type.spec.tableRole == "row" &&
          $anchorCell.index() < $anchorCell.parent.childCount &&
          $headCell.index() < $headCell.parent.childCount &&
          inSameTable($anchorCell, $headCell))
        return new CellSelection($anchorCell, $headCell)
      else
        return Selection.near($headCell, 1)
    }
  }
  
  export function drawCellSelection(state) {
    if (!(state.selection instanceof CellSelection)) return null
    let cells = []
    state.selection.forEachCell((node, pos) => {
      cells.push(Decoration.node(pos, pos + node.nodeSize, {class: "selectedCell"}))
    })
    return DecorationSet.create(state.doc, cells)
  }
  
  function isCellBoundarySelection({$from, $to}) {
    if ($from.pos == $to.pos || $from.pos < $from.pos - 6) return false // Cheap elimination
    let afterFrom = $from.pos, beforeTo = $to.pos, depth = $from.depth
    for (; depth >= 0; depth--, afterFrom++)
      if ($from.after(depth + 1) < $from.end(depth)) break
    for (let d = $to.depth; d >= 0; d--, beforeTo--)
      if ($to.before(d + 1) > $to.start(d)) break
    return afterFrom == beforeTo && /row|table/.test($from.node(depth).type.spec.tableRole)
  }
  
  function isTextSelectionAcrossCells({$from, $to}) {
    let fromCellBoundaryNode;
    let toCellBoundaryNode;
  
    for (let i = $from.depth; i > 0; i--) {
      let node = $from.node(i);
      if (node.type.spec.tableRole === 'cell' || node.type.spec.tableRole === 'header_cell') {
        fromCellBoundaryNode = node;
        break;
      }
    }
  
    for (let i = $to.depth; i > 0; i--) {
      let node = $to.node(i);
      if (node.type.spec.tableRole === 'cell' || node.type.spec.tableRole === 'header_cell') {
        toCellBoundaryNode = node;
        break;
      }
    }
  
    return fromCellBoundaryNode !== toCellBoundaryNode && $to.parentOffset === 0
  }
  
  export function normalizeSelection(state, tr, allowTableNodeSelection) {
    let sel = (tr || state).selection, doc = (tr || state).doc, normalize, role
    if (sel instanceof NodeSelection && (role = sel.node.type.spec.tableRole)) {
      if (role == "cell" || role == "header_cell") {
        normalize = CellSelection.create(doc, sel.from)
      } else if (role == "row") {
        let $cell = doc.resolve(sel.from + 1)
        normalize = CellSelection.rowSelection($cell, $cell)
      } else if (!allowTableNodeSelection) {
        let map = TableMap.get(sel.node), start = sel.from + 1
        let lastCell = start + map.map[map.width * map.height - 1]
        normalize = CellSelection.create(doc, start + 1, lastCell)
      }
    } else if (sel instanceof TextSelection && isCellBoundarySelection(sel)) {
      normalize = TextSelection.create(doc, sel.from)
    } else if (sel instanceof TextSelection && !sel.empty) {
      // Fix up any selections that are trying to select only part of the table.
      // Partial selections lead to partial deletions which leads to problems.
      // Instead we extend the selection to cover the entire table.
      let tableAtStart = isPosInTable(sel.$from)
      let tableAtEnd = isPosInTable(sel.$to)
      if (tableAtStart != tableAtEnd) {
        if (tableAtEnd) {
          let $toCell = cellAround(sel.$to)
          if ($toCell) normalize = TextSelection.create(doc, sel.from, $toCell.end(-1))
        }
        if (tableAtStart) {
          let $fromCell = cellAround(sel.$from)
          if ($fromCell) normalize = TextSelection.create(doc, sel.to, $fromCell.start(-1))
        }
      }
    } else if (sel instanceof TextSelection && isTextSelectionAcrossCells(sel)) {
      normalize = TextSelection.create(doc, sel.$from.start(), sel.$from.end());
    }
    if (normalize)
      (tr || (tr = state.tr)).setSelection(normalize)
    return tr
  }
  