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.
243 lines (222 loc) • 7.74 kB
JavaScript
import { forEach, getRelativeBoundingRect } from '../../util'
import { Component } from '../../ui'
export default class Dropzones extends Component {
didMount() {
this.context.dragManager.on('drag:started', this.onDragStarted, this)
this.context.dragManager.on('drag:finished', this.onDragFinished, this)
}
render($$) {
let el = $$('div').addClass('sc-dropzones')
if (this.state.dropzones) {
el.on('dragenter', this.onDrag)
.on('dragover', this.onDrag)
// Dropzones are scoped by surfaceId
forEach(this.state.dropzones, (dropzones, surfaceId) => {
dropzones.forEach((dropzone, index) => {
let dropType = dropzone.type
let dropzoneEl
if (dropType === 'place') {
dropzoneEl = $$('div').addClass('se-dropzone')
.attr({
'data-dropzone-index': index,
'data-dropzone-surface': surfaceId
}).append(
$$('div').addClass('se-drop-teaser').css({
top: dropzone.teaserPos
})
)
} else if (dropType === 'custom') {
dropzoneEl = $$('div').addClass('se-custom-dropzone').attr({
'data-dropzone-index': index,
'data-dropzone-surface': surfaceId
}).append(
// TODO: also provide se-custom-drop-teaser when custom
// dropzone is provided
$$('div').addClass('se-message').append(dropzone.message)
)
}
if (dropzoneEl) {
let shield = $$('div').addClass('se-drop-shield')
.on('dragenter', this.onDragEnter)
.on('dragleave', this.onDragLeave)
.on('drop', this.onDrop)
.on('mouseenter', this.onDragEnter)
.on('mouseleave', this.onDragLeave)
.on('mouseup', this.onDrop)
dropzoneEl.append(shield)
dropzoneEl.css({
position: 'absolute',
top: dropzone.top,
left: dropzone.left,
width: dropzone.width,
height: dropzone.height
})
el.append(dropzoneEl)
}
})
})
} else {
el.addClass('sm-hidden')
}
return el
}
// triggered by DragManager
onDragStarted(dragState) {
let dropzones = this._computeDropzones(dragState)
setTimeout(() => {
this.setState({
dropzones: dropzones
})
}, 250)
}
// triggered by DragManager
onDragFinished() {
this.setState({})
}
onDragEnter(e) {
// console.log('onDragEnter', e.target)
e.target.parentNode.classList.add('sm-over')
}
onDragLeave(e) {
// console.log('onDragLeave', e.target)
e.target.parentNode.classList.remove('sm-over')
}
// just so that the teaser does not prevent dropping
onDrag(e) { // eslint-disable-line
// console.log('onDrag', e.target)
e.preventDefault()
}
onDrop(e) {
// console.log('Dropzones.onDrop()', e.target)
// HACK: try if this is really necessary
e.__reserved__ = true
e.preventDefault()
e.stopPropagation()
let dropzoneIndex = e.target.parentNode.dataset.dropzoneIndex
let dropzoneSurface = e.target.parentNode.dataset.dropzoneSurface
let dropzone = this.state.dropzones[dropzoneSurface][dropzoneIndex]
let dropParams = dropzone.dropParams
let dropType = dropzone.type
// Determine target surface
let targetSurface = this.context.surfaceManager.getSurface(dropzoneSurface)
// Original component (e.g. img element)
let component = dropzone.component
let dropzoneComponent = dropzone.dropzoneComponent
// HACK: extending the dragState here
let dragManager = this.context.dragManager
dragManager.extendDragState({
targetSurface,
dropType,
dropParams,
component,
dropzoneComponent
})
dragManager._onDragEnd(e)
}
/*
Get bounding rect for a component (relative to scrollPane content element)
*/
_getBoundingRect(comp) {
let scrollPane = comp.context.scrollPane
let contentElement = scrollPane.getContentElement().getNativeElement()
let rect = getRelativeBoundingRect(comp.getNativeElement(), contentElement)
return rect
}
_computeDropzones(dragState) {
let scrollPaneName = this.context.scrollPane.getName()
let surfaces = dragState.scrollPanes[scrollPaneName].surfaces
let scopedDropzones = {}
forEach(surfaces, (surface) => {
let components = surface.childNodes
// e.g. 3 components = 4 drop zones (1 before, 1 after, 2 in-between)
let numDropzones = components.length + 1
let dropzones = []
for (let i = 0; i < numDropzones; i++) {
if (i === 0) {
// First dropzone
let firstComp = this._getBoundingRect(components[0])
dropzones.push({
type: 'place',
left: firstComp.left,
top: firstComp.top,
width: firstComp.width,
height: firstComp.height / 2,
teaserPos: 0,
dropParams: {
insertPos: i
}
})
} else if (i === numDropzones - 1) {
// Last dropzone
let lastComp = this._getBoundingRect(components[i - 1])
dropzones.push({
type: 'place',
left: lastComp.left,
top: lastComp.top + lastComp.height / 2,
width: lastComp.width,
height: lastComp.height / 2,
teaserPos: lastComp.height / 2,
dropParams: {
insertPos: i
}
})
} else {
// Drop zone in between two components
let upperComp = this._getBoundingRect(components[i-1])
let lowerComp = this._getBoundingRect(components[i])
let topBound = upperComp.top + upperComp.height / 2
let bottomBound = lowerComp.top + lowerComp.height / 2
dropzones.push({
type: 'place',
left: upperComp.left,
top: topBound,
width: upperComp.width,
height: bottomBound - topBound,
teaserPos: (upperComp.top + upperComp.height + lowerComp.top) / 2 - topBound,
dropParams: {
insertPos: i
}
})
}
if (i < numDropzones - 1) {
let comp = components[i]
// We get the isolated node wrapper and want to use the content element
if (comp._isIsolatedNodeComponent) {
comp = comp.getContent()
}
// If component has dropzones declared
if (comp.getDropzoneSpecs) {
let dropzoneSpecs = comp.getDropzoneSpecs()
dropzoneSpecs.forEach((dropzoneSpec) => {
let dropzoneComp = dropzoneSpec.component
let rect = this._getBoundingRect(dropzoneComp)
dropzones.push({
type: 'custom',
component: comp,
dropzoneComponent: dropzoneComp,
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
message: dropzoneSpec.message,
dropParams: dropzoneSpec.dropParams
})
})
}
}
}
scopedDropzones[surface.getName()] = dropzones
})
return scopedDropzones
}
_renderDropTeaser(hints) {
if (hints.visible) {
this.el.removeClass('sm-hidden')
this.el.css('top', hints.rect.top)
this.el.css('left', hints.rect.left)
this.el.css('right', hints.rect.right)
} else {
this.el.addClass('sm-hidden')
}
}
}