All files / src/components DropTarget.jsx

15.96% Statements 15/94
0% Branches 0/61
0% Functions 0/20
16.85% Lines 15/89

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 2571x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x     1x         1x                                                                                                                                                       1x       1x                                                                                                                                                                                                                                                                                                                            
import React, {Component, createContext} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types/prop-types';
import NodeEditable from './NodeEditable';
import SHARED from '../shared';
import {DropNodeTarget} from '../dnd';
import classNames from 'classnames';
import {isErrorFree} from '../store';
import BlockComponent from './BlockComponent';
import {gensym} from '../utils';
import {drop, InsertTarget} from '../actions';
 
// Provided by `Node`
export const NodeContext = createContext({
  node: null
});
 
// Provided by `DropTargetContainer`
export const DropTargetContext = createContext({
  node: null,
  field: null,
});
 
// Find the id of the drop target (if any) on the given side of `child` node.
export function findAdjacentDropTargetId(child, onLeft) {
  let prevDropTargetId = null;
  let targetId = `block-node-${child.id}`;
 
  function findDT(elem) {
    if (!elem.children) {
      return null;
    }
    // Convert array-like object into an Array.
    let children = [...elem.children];
    // If we want the drop-target to the right, iterate in reverse
    if (!onLeft) { children.reverse(); }
 
    for (let sibling of children) {
      if (sibling.id?.startsWith("block-drop-target-")) {
        // We've hit a drop-target. Remember its id, in case it's adjacent to the node.
        prevDropTargetId = sibling.id.substring(18); // skip "block-drop-target-"
      } else if (sibling.id == targetId) {
        // We've found this node! Return the id of the adjacent drop target.
        return prevDropTargetId;
      } else if (sibling.id?.startsWith("block-node-")) {
        // It's a different node. Skip it.
      } else if (sibling.children) {
        // We're... somewhere else. If it has children, traverse them to look for the node.
        let result = findDT(sibling);
        if (result !== null) {
          return result; // node found.
        }
      }
    }
    return null;
  }
  if (!child.parent) return null;
  return findDT(child.parent.element);
}
 
// NOTE(Justin) It sure would be nice to generate the id inside of DropTarget.
// But AFAIK that's not feasible, because the `id` needs to be accessible
// inside `mapStateToProps`, and it's only accessible if it's a `prop`.
// Hence this extraneous class.
export class DropTarget extends Component {
  static contextType = NodeContext;
 
  static propTypes = {
    field: PropTypes.string.isRequired,
  }
 
  constructor(props) {
    super(props);
    this.isDropTarget = true;
    this.id = gensym(); // generate a unique ID
  }
 
  render() {
    const value = {
      field: this.props.field,
      node: this.context.node,
    };
    return (
      <DropTargetContext.Provider value={value}>
        <ActualDropTarget id={this.id} />
      </DropTargetContext.Provider>
    );
  }
}
 
// These `isEditable` and `setEditable` methods allow DropTargetSiblings to
// check to see whether an adjacent DropTarget is being edited, or, for when the
// insert-left or insert-right shortcut is pressed, _set_ an adjacent DropTarget
// as editable.
const mapStateToProps2 = ({ast, editable}, {id}) => ({
  ast,
  isEditable: editable[id] || false,
});
const mapDispatchToProps2 = (dispatch, {id}) => ({
  dispatch,
  setEditable: (bool) => dispatch({type: 'SET_EDITABLE', id, bool}),
});
 
@connect(mapStateToProps2, mapDispatchToProps2)
@DropNodeTarget(function(monitor) {
  const target = new InsertTarget(this.context.node, this.context.field, this.getLocation());
  return drop(monitor.getItem(), target);
})
class ActualDropTarget extends BlockComponent {
 
  static contextType = DropTargetContext;
 
  static propTypes = {
    // fulfilled by @connect
    isEditable: PropTypes.bool.isRequired,
    setEditable: PropTypes.func.isRequired,
    // Every DropTarget has a globally unique `id` which can be used to look up
    // its corresponding DOM element.
    id: PropTypes.string.isRequired,
    // fulfilled by DropNodeTarget
    connectDropTarget: PropTypes.func.isRequired,
    isOver: PropTypes.bool.isRequired,
  }
 
  constructor(props) {
    super(props);
    this.isDropTarget = true;
 
    this.state = {
      value: "",
      mouseOver: false,
    };
  }
 
  getLocation() {
    let prevNodeId = null;
    let targetId = `block-drop-target-${this.props.id}`;
    let ast = this.props.ast;
    let dropTargetWasFirst = false;
 
    function findLoc(elem) {
      if (elem == null || elem.children == null) { // if it's a new element (insertion)
        return null;
      }
      // We've hit an ASTNode. Remember its id, in case it's the node just before the drop target.
      if (elem.id?.startsWith("block-node-")) {
        prevNodeId = elem.id.substring(11); // skip "block-node-"
      }
      for (let sibling of elem.children) {
        if (sibling.id?.startsWith("block-node-")) {
          // We've hit an ASTNode. Remember its id, in case it's the node just before the drop target.
          prevNodeId = sibling.id.substring(11); // skip "block-node-"
          if (dropTargetWasFirst) {
            // Edge case: the drop target was literally the first thing, so we
            // need to return the `from` of its _next_ sibling. That's this one.
            return ast.getNodeById(prevNodeId).from;
          }
        } else if (sibling.id == targetId) {
          // We've found this drop target! Return the `to` location of the previous ASTNode.
          if (prevNodeId) {
            return ast.getNodeById(prevNodeId).to;
          } else {
            // Edge case: nothing is before the drop target.
            dropTargetWasFirst = true;
          }
        } else if (sibling.id?.startsWith("block-drop-target")) {
          // It's a different drop target. Skip it.
        } else if (sibling.children) {
          // We're... somewhere else. If it has children, traverse them to look for the drop target.
          let result = findLoc(sibling);
          if (result !== null) {
            return result; // drop target found.
          }
        }
      }
      return null;
    }
    return findLoc(this.context.node.element) || this.context.pos;
  }
 
  handleClick = e => {
    e.stopPropagation();
    if (!isErrorFree()) return; // TODO(Oak): is this the best way to handle this?
    this.props.setEditable(true);
  }
 
  handleMouseEnterRelated = e => {
    e.preventDefault();
    e.stopPropagation();
    this.setState({mouseOver: true});
  }
 
  handleMouseLeaveRelated = e => {
    e.preventDefault();
    e.stopPropagation();
    this.setState({mouseOver: false});
  }
 
  handleMouseDragRelated = _ => {
    //NOTE(ds26gte): dummy handler
  }
 
  handleChange = (value) => {
    this.setState({value});
  }
 
  render() {
    const props = {
      tabIndex          : "-1",
      role              : 'textbox',
      'aria-setsize'    : '1',
      'aria-posinset'   : '1',
      'aria-level'      : '1',
      id                : `block-drop-target-${this.props.id}`,
    };
    if (this.props.isEditable) {
      const target = new InsertTarget(this.context.node, this.context.field, this.getLocation());
      return (
        <NodeEditable target={target}
                      value={this.state.value}
                      onChange={this.handleChange}
                      onMouseEnter={this.handleMouseEnterRelated}
                      onDragEnter={this.handleMouseEnterRelated}
                      onMouseLeave={this.handleMouseLeaveRelated}
                      onDragLeave={this.handleMouseLeaveRelated}
                      onMouseOver={this.handleMouseDragRelated}
                      onDragOver={this.handleMouseDragRelated}
                      onDrop={this.handleMouseDragRelated}
                      isInsertion={true}
                      contentEditableProps={props}
                      extraClasses={['blocks-node', 'blocks-white-space']}
                      onDisableEditable={() => this.props.setEditable(false)} />
      );
    }
    const classes = [
      'blocks-drop-target',
      'blocks-white-space',
      {'blocks-over-target' : this.props.isOver || this.state.mouseOver}
    ];
    return this.props.connectDropTarget(
      <span
        id={`block-drop-target-${this.props.id}`}
        className={classNames(classes)}
        onMouseEnter={this.handleMouseEnterRelated}
        onDragEnter={this.handleMouseEnterRelated}
        onMouseLeave={this.handleMouseLeaveRelated}
        onDragLeave={this.handleMouseLeaveRelated}
        onMouseOver={this.handleMouseDragRelated}
        onDragOver={this.handleMouseDragRelated}
        onDrop={this.handleMouseDragRelated}
        onClick = {this.handleClick} 
        data-field = {this.context.field}
       />
    );
  }
}