all files / ui/ IsolatedNodeComponent.js

62.75% Statements 64/102
61.11% Branches 33/54
35.29% Functions 6/17
63.64% Lines 63/99
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                                                  60×       122× 122× 122×     122× 122×       122×     122× 44×   122× 122×     122×     122×             122×         122×     122×   122× 122× 122×         122× 19× 19×     122×   122×       122×                                                                                                         152× 152× 152×   100× 92× 92× 92× 35× 19× 19× 16×     92× 13×               100×                       29× 29× 29×                                                           122×                                                        
import { Coordinate } from '../model'
import Component from './Component'
import AbstractIsolatedNodeComponent from './AbstractIsolatedNodeComponent'
 
const BRACKET = 'X'
 
/*
  Isolation Strategies:
    - default: IsolatedNode renders a blocker the content gets enabled by a double-click.
    - open: No blocker. Content is enabled when parent surface is.
 
    > Notes:
 
      The blocker is used to shield the inner UI and not to interfer with general editing gestures.
      In some cases however, e.g. a figure (with image and caption), it feels better if the content directly accessible.
      In this case, the content component must provide a means to drag the node, e.g. set `<img draggable=true>`.
      This works only in browsers that are able to deal with 'contenteditable' isles,
      i.e. a structure where the isolated node is contenteditable=false, and inner elements have contenteditable=true
      Does not work in Edge. Works in Chrome, Safari
 
      The the default unblocking gesture requires the content to implement a grabFocus() method, which should set the selection
      into one of the surfaces, or set a CustomSelection.
*/
class IsolatedNodeComponent extends AbstractIsolatedNodeComponent {
 
  constructor(...args) {
    super(...args)
  }
 
  render($$) {
    let node = this.props.node
    let ContentClass = this.ContentClass
    let disabled = this.props.disabled
 
    // console.log('##### IsolatedNodeComponent.render()', $$.capturing);
    let el = $$('div')
    el.addClass(this.getClassNames())
      .addClass('sc-isolated-node')
      .addClass('sm-'+this.props.node.type)
      .attr("data-id", node.id)
    Iif (disabled) {
      el.addClass('sm-disabled')
    }
    if (this.state.mode) {
      el.addClass('sm-'+this.state.mode)
    }
    Eif (!ContentClass.noStyle) {
      el.addClass('sm-default-style')
    }
    // always handle ESCAPE
    el.on('keydown', this.onKeydown)
 
    // console.log('##### rendering IsolatedNode', this.id)
    let shouldRenderBlocker = (
      this.blockingMode === 'closed' &&
      !this.state.unblocked
    )
 
    // HACK: we need something 'editable' where we can put DOM selection into,
    // otherwise native cursor navigation gets broken
    el.append(
      $$('div').addClass('se-bracket sm-left').ref('left')
        .append(BRACKET)
    )
 
    let content = this.renderContent($$, node, {
      disabled: this.props.disabled || shouldRenderBlocker
    }).ref('content')
    content.attr('contenteditable', false)
 
    el.append(content)
    el.append($$(Blocker).ref('blocker'))
    el.append(
      $$('div').addClass('se-bracket sm-right').ref('right')
        .append(BRACKET)
    )
 
    if (!shouldRenderBlocker) {
      el.addClass('sm-no-blocker')
      el.on('click', this.onClick)
        .on('dblclick', this.onDblClick)
    }
    el.on('mousedown', this._reserveMousedown, this)
 
    return el
  }
 
  getClassNames() {
    return ''
  }
 
  getContent() {
    return this.refs.content
  }
 
  selectNode() {
    // console.log('IsolatedNodeComponent: selecting node.');
    let editorSession = this.context.editorSession
    let surface = this.context.surface
    let nodeId = this.props.node.id
    editorSession.setSelection({
      type: 'node',
      nodeId: nodeId,
      containerId: surface.getContainerId(),
      surfaceId: surface.id
    })
  }
 
  // EXPERIMENTAL: trying to catch clicks not handler by the
  // content when this is unblocked
  onClick(event) {
    // console.log('### Clicked on IsolatedNode', this.id, event.target)
    event.stopPropagation()
  }
 
  onDblClick(event) {
    // console.log('### DblClicked on IsolatedNode', this.id, event.target)
    event.stopPropagation()
  }
 
  grabFocus(event) {
    let content = this.refs.content
    if (content.grabFocus) {
      content.grabFocus(event)
      return true
    }
  }
 
  // EXPERIMENTAL: Surface and IsolatedNodeComponent communicate via flag on the mousedown event
  // and only reacting on click or mouseup when the mousedown has been reserved
  _reserveMousedown(event) {
    if (event.__reserved__) {
      // console.log('%s: mousedown already reserved by %s', this.id, event.__reserved__.id)
      return
    } else {
      // console.log('%s: taking mousedown ', this.id)
      event.__reserved__ = this
    }
  }
 
  _deriveStateFromSelectionState(selState) {
    let surface = this._getSurface(selState)
    let newState = { mode: null, unblocked: null}
    if (!surface) return newState
    // detect cases where this node is selected or co-selected by inspecting the selection
    if (surface === this.context.surface) {
      let sel = selState.getSelection()
      let nodeId = this.props.node.id
      if (sel.isNodeSelection() && sel.getNodeId() === nodeId) {
        if (sel.isFull()) {
          newState.mode = 'selected'
          newState.unblocked = true
        } else if (sel.isBefore()) {
          newState.mode = 'cursor'
          newState.position = 'before'
        } else Eif (sel.isAfter()) {
          newState.mode = 'cursor'
          newState.position = 'after'
        }
      }
      if (sel.isContainerSelection() && sel.containsNode(nodeId)) {
        newState.mode = 'co-selected'
      }
    } else {
      let isolatedNodeComponent = surface.context.isolatedNodeComponent
      if (isolatedNodeComponent) {
        if (isolatedNodeComponent === this) {
          newState.mode = 'focused'
          newState.unblocked = true
        } else {
          let isolatedNodes = this._getIsolatedNodes(selState)
          if (isolatedNodes.indexOf(this) > -1) {
            newState.mode = 'co-focused'
            newState.unblocked = true
          }
        }
      }
    }
    return newState
  }
 
}
 
IsolatedNodeComponent.prototype._isIsolatedNodeComponent = true
 
IsolatedNodeComponent.prototype._isDisabled = IsolatedNodeComponent.prototype.isDisabled
 
IsolatedNodeComponent.getDOMCoordinate = function(comp, coor) {
  let { start, end } = IsolatedNodeComponent.getDOMCoordinates(comp)
  if (coor.offset === 0) return start
  else return end
}
 
IsolatedNodeComponent.getDOMCoordinates = function(comp) {
  const left = comp.refs.left
  const right = comp.refs.right
  return {
    start: {
      container: left.getNativeElement(),
      offset: 0
    },
    end: {
      container: right.getNativeElement(),
      offset: right.getChildCount()
    }
  }
}
 
IsolatedNodeComponent.getCoordinate = function(nodeEl, options) {
  let comp = Component.unwrap(nodeEl, 'strict').context.isolatedNodeComponent
  let offset = null
  if (options.direction === 'left' || nodeEl === comp.refs.left.el) {
    offset = 0
  } else if (options.direction === 'right' || nodeEl === comp.refs.right.el) {
    offset = 1
  }
  let coor
  if (offset !== null) {
    coor = new Coordinate([comp.props.node.id], offset)
    coor._comp = comp
  }
  return coor
}
 
class Blocker extends Component {
 
  render($$) {
    return $$('div').addClass('sc-isolated-node-blocker')
      .attr('draggable', true)
      .attr('contenteditable', false)
      .on('click', this.onClick)
      .on('dblclick', this.onDblClick)
  }
 
  onClick(event) {
    if (event.target !== this.getNativeElement()) return
    // console.log('Clicked on Blocker of %s', this._getIsolatedNodeComponent().id, event)
    event.stopPropagation()
    const comp = this._getIsolatedNodeComponent()
    comp.extendState({ mode: 'selected', unblocked: true })
    comp.selectNode()
  }
 
  onDblClick(event) {
    // console.log('DblClicked on Blocker of %s', this.getParent().id, event)
    event.stopPropagation()
  }
 
  _getIsolatedNodeComponent() {
    return this.context.isolatedNodeComponent
  }
 
}
 
export default IsolatedNodeComponent