UNPKG

substance

Version:

Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing systems.

256 lines (220 loc) 7.7 kB
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) if (disabled) { el.addClass('sm-disabled') } if (this.state.mode) { el.addClass('sm-'+this.state.mode) } if (!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 if (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