mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
46d5b779ae
no issue - the position that images were inserted did not always match the position indicator because of errors in the insert index calculations
715 lines
27 KiB
JavaScript
715 lines
27 KiB
JavaScript
import * as constants from '../lib/dnd/constants';
|
|
import * as utils from '../lib/dnd/utils';
|
|
import Container from '../lib/dnd/container';
|
|
import ScrollHandler from '../lib/dnd/scroll-handler';
|
|
import Service from '@ember/service';
|
|
import {A} from '@ember/array';
|
|
import {alias} from '@ember/object/computed';
|
|
import {didCancel, task, waitForProperty} from 'ember-concurrency';
|
|
import {run} from '@ember/runloop';
|
|
import {inject as service} from '@ember/service';
|
|
|
|
// this service allows registration of "containers"
|
|
// containers can have both draggables and droppables
|
|
// - draggables are elements that can be dragged
|
|
// - droppables are elements that should respond if dragged over
|
|
// containers will handle the drag start/drag over/drop events triggered by this service
|
|
// this service keeps track of all containers and has centralized event handling for mouse events
|
|
|
|
export default Service.extend({
|
|
koenigUi: service(),
|
|
|
|
containers: null,
|
|
ghostInfo: null,
|
|
grabbedElement: null, // TODO: standardise on draggableInfo.element
|
|
sourceContainer: null,
|
|
|
|
isDragging: alias('koenigUi.isDragging'),
|
|
|
|
_eventHandlers: null,
|
|
|
|
// lifecycle ---------------------------------------------------------------
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
|
|
this.containers = A([]);
|
|
this.scrollHandler = new ScrollHandler();
|
|
this._eventHandlers = {};
|
|
this._transformedDroppables = A([]);
|
|
|
|
// bind any raf handler functions
|
|
this._rafUpdateGhostElementPosition = run.bind(this, this._updateGhostElementPosition);
|
|
|
|
// set up document event listeners
|
|
this._addGrabListeners();
|
|
|
|
// append body elements
|
|
this._appendGhostContainerElement();
|
|
},
|
|
|
|
willDestroy() {
|
|
this._super(...arguments);
|
|
|
|
// reset any on-going drag and remove any temporary listeners
|
|
this.cleanup();
|
|
|
|
// clean up document event listeners
|
|
this._removeGrabListeners();
|
|
|
|
// remove body elements
|
|
this._removeDropIndicator();
|
|
this._removeGhostContainerElement();
|
|
},
|
|
|
|
// interface ---------------------------------------------------------------
|
|
|
|
registerContainer(element, options) {
|
|
let container = new Container(element, options);
|
|
this.containers.pushObject(container);
|
|
|
|
// return a minimal interface to the container because this service
|
|
// should be used for management rather than the container class instance
|
|
return {
|
|
enableDrag: () => {
|
|
container.enableDrag();
|
|
},
|
|
|
|
disableDrag: () => {
|
|
container.disableDrag();
|
|
},
|
|
|
|
refresh: () => {
|
|
// re-calculate draggables/droppables
|
|
container.refresh();
|
|
},
|
|
|
|
destroy: () => {
|
|
// unregister container
|
|
this.containers.removeObject(container);
|
|
}
|
|
};
|
|
},
|
|
|
|
// remove all containers and event handlers, useful when leaving an editor route
|
|
cleanup() {
|
|
this.containers = A([]);
|
|
// cancel any tasks and remove intermittent event handlers
|
|
this._resetDrag();
|
|
},
|
|
|
|
// event handlers ----------------------------------------------------------
|
|
|
|
// we use a custom "drag" detection rather than native drag events because it
|
|
// allows better tracking across multiple containers and gives more flexibilty
|
|
// for handling touch events later if required
|
|
_onMouseDown(event) {
|
|
if (!this.isDragging && (event.button === undefined || event.button === 0)) {
|
|
this.grabbedElement = utils.getParent(event.target, constants.DRAGGABLE_SELECTOR);
|
|
|
|
if (this.grabbedElement) {
|
|
// some elements may have explicitly disabled dragging such as
|
|
// captions where we want to allow text selection instead
|
|
let dragDisabledElement = utils.getParent(event.target, constants.DRAG_DISABLED_SELECTOR);
|
|
if (dragDisabledElement && this.grabbedElement.contains(dragDisabledElement)) {
|
|
return;
|
|
}
|
|
|
|
let containerElement = utils.getParent(this.grabbedElement, constants.CONTAINER_SELECTOR);
|
|
let container = this.containers.findBy('element', containerElement);
|
|
this.sourceContainer = container;
|
|
|
|
if (container.isDragEnabled) {
|
|
this._waitForDragStart.perform(event).then(() => {
|
|
// stop the drag creating a selection
|
|
window.getSelection().removeAllRanges();
|
|
// set up the drag details
|
|
this._initiateDrag(event);
|
|
}).catch((error) => {
|
|
// ignore cancelled tasks and throw unrecognized errors
|
|
if (!didCancel(error)) {
|
|
throw error;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
_onMouseMove(event) {
|
|
event.preventDefault();
|
|
|
|
if (this.draggableInfo) {
|
|
this.draggableInfo.mousePosition.x = event.clientX;
|
|
this.draggableInfo.mousePosition.y = event.clientY;
|
|
|
|
this._handleDrag(event);
|
|
}
|
|
},
|
|
|
|
_onMouseUp(/*event*/) {
|
|
if (this.draggableInfo) {
|
|
let success = false;
|
|
|
|
// TODO: accept object rather than positioned args? OR, should the
|
|
// droppable data be stored on draggableInfo?
|
|
if (this._currentOverContainer) {
|
|
success = this._currentOverContainer.onDrop(
|
|
this.draggableInfo,
|
|
this._currentOverDroppableElem,
|
|
this._currentOverDroppablePosition
|
|
);
|
|
}
|
|
|
|
this.containers.forEach((container) => {
|
|
container.onDropEnd(this.draggableInfo, success);
|
|
});
|
|
}
|
|
|
|
// remove drag info and any ghost element
|
|
this._resetDrag();
|
|
},
|
|
|
|
_onKeyDown(event) {
|
|
// cancel drag on escape
|
|
if (this.isDragging && event.key === 'Escape') {
|
|
this._resetDrag();
|
|
}
|
|
},
|
|
|
|
// private -----------------------------------------------------------------
|
|
|
|
// called when we detect a mousedown event on a draggable element. Sets
|
|
// up temporary event handlers for mousemove, mouseup, and drag. If
|
|
// sufficient movement is detected before the mouse is released and we don't
|
|
// detect a native drag event then the task will resolve. Mouseup or drag
|
|
// events will cancel the task which will result in a rejected promise if
|
|
// the task has been cast to a promise
|
|
_waitForDragStart: task(function* (startEvent) {
|
|
let moveThreshold = 1;
|
|
|
|
this.set('_dragStartConditionsMet', false);
|
|
|
|
let onMove = (event) => {
|
|
let {clientX: currentX, clientY: currentY} = event;
|
|
|
|
if (
|
|
Math.abs(startEvent.clientX - currentX) > moveThreshold ||
|
|
Math.abs(startEvent.clientY - currentY) > moveThreshold
|
|
) {
|
|
this.set('_dragStartConditionsMet', true);
|
|
}
|
|
};
|
|
|
|
let onUp = () => {
|
|
this._waitForDragStart.cancelAll();
|
|
};
|
|
|
|
// give preference to native drag/drop handlers
|
|
let onHtmlDrag = () => {
|
|
this._waitForDragStart.cancelAll();
|
|
};
|
|
|
|
// register local events
|
|
document.addEventListener('mousemove', onMove, {passive: false});
|
|
document.addEventListener('mouseup', onUp, {passive: false});
|
|
document.addEventListener('drag', onHtmlDrag, {passive: false});
|
|
|
|
try {
|
|
yield waitForProperty(this, '_dragStartConditionsMet');
|
|
} finally {
|
|
// finally is always called on task cancellation
|
|
this.set('_dragStartConditionsMet', false);
|
|
document.removeEventListener('mousemove', onMove, {passive: false});
|
|
document.removeEventListener('mouseup', onUp, {passive: false});
|
|
document.removeEventListener('drag', onHtmlDrag, {passive: false});
|
|
}
|
|
}).keepLatest(),
|
|
|
|
_initiateDrag(startEvent) {
|
|
this.set('isDragging', true);
|
|
utils.applyUserSelect(document.body, 'none');
|
|
|
|
let container = this.sourceContainer;
|
|
let draggableInfo = container.getDraggableInfo(this.grabbedElement);
|
|
|
|
if (!draggableInfo) {
|
|
this._resetDrag();
|
|
return;
|
|
}
|
|
|
|
// append the drop indicator if it doesn't already exist - we append to
|
|
// the editor's element rather than body so it needs to be re-appended
|
|
// each time a drag is initiated in a new editor instance
|
|
this._appendDropIndicator();
|
|
|
|
draggableInfo = Object.assign({}, draggableInfo, {
|
|
element: this.grabbedElement,
|
|
mousePosition: {
|
|
x: startEvent.clientX,
|
|
y: startEvent.clientY
|
|
}
|
|
});
|
|
this.set('draggableInfo', draggableInfo);
|
|
|
|
this.containers.forEach((container) => {
|
|
container.onDragStart(draggableInfo);
|
|
});
|
|
|
|
// style the dragged element
|
|
this.draggableInfo.element.style.opacity = 0.5;
|
|
|
|
// create the ghost element and cache it's position so avoid costly
|
|
// getBoundingClientRect calls in the mousemove handler
|
|
let ghostElement = container.createGhostElement(this.draggableInfo);
|
|
if (ghostElement && ghostElement instanceof HTMLElement) {
|
|
this._ghostContainerElement.appendChild(ghostElement);
|
|
let ghostElementRect = ghostElement.getBoundingClientRect();
|
|
let ghostInfo = {
|
|
element: ghostElement,
|
|
positionX: ghostElementRect.x,
|
|
positionY: ghostElementRect.y
|
|
};
|
|
this.set('ghostInfo', ghostInfo);
|
|
} else {
|
|
// eslint-disable-next-line
|
|
console.warn('container.createGhostElement did not return an element', this.draggableInfo, {ghostElement});
|
|
this._resetDrag();
|
|
return;
|
|
}
|
|
|
|
// add watches to follow the drag/drop
|
|
this._addMoveListeners();
|
|
this._addReleaseListeners();
|
|
this._addKeyDownListeners();
|
|
|
|
// start ghost element following the mouse
|
|
requestAnimationFrame(this._rafUpdateGhostElementPosition);
|
|
|
|
// let the scroll handler select the scrollable element
|
|
this.scrollHandler.dragStart(this.draggableInfo);
|
|
|
|
// prevent the pointer showing the text caret over text content whilst dragging
|
|
document.querySelectorAll('[data-kg="editor"]').forEach((el) => {
|
|
el.style.setProperty('cursor', 'default', 'important');
|
|
});
|
|
|
|
// prevent card hover showing whilst dragging
|
|
this._elementsWithHoverRemoved = document.querySelectorAll('.kg-card-hover');
|
|
this._elementsWithHoverRemoved.forEach((el) => {
|
|
el.classList.remove('kg-card-hover');
|
|
});
|
|
|
|
this._handleDrag();
|
|
},
|
|
|
|
_handleDrag() {
|
|
// hide the ghost element so that it's not picked up by elementFromPoint
|
|
// when determining the target element under the mouse
|
|
this._ghostContainerElement.hidden = true;
|
|
let target = document.elementFromPoint(
|
|
this.draggableInfo.mousePosition.x,
|
|
this.draggableInfo.mousePosition.y
|
|
);
|
|
this.draggableInfo.target = target;
|
|
this._ghostContainerElement.hidden = false;
|
|
|
|
this.scrollHandler.dragMove(this.draggableInfo);
|
|
|
|
let overContainerElem = utils.getParent(target, constants.CONTAINER_SELECTOR);
|
|
let overDroppableElem = utils.getParent(target, constants.DROPPABLE_SELECTOR);
|
|
|
|
// it's possible for the mouse to be over a "dead" area when dragging over
|
|
// the position indicator, in this case we want to prevent a parent
|
|
// container's droppable from being picked up
|
|
if (!overContainerElem || !overContainerElem.contains(overDroppableElem)) {
|
|
overDroppableElem = null;
|
|
}
|
|
|
|
let isLeavingContainer = this._currentOverContainerElem && overContainerElem !== this._currentOverContainerElem;
|
|
let isLeavingDroppable = this._currentOverDroppableElem && overDroppableElem !== this._currentOverDroppableElem;
|
|
let isOverContainer = overContainerElem && overContainerElem !== this._currentOverContainer;
|
|
let isOverDroppable = overDroppableElem;
|
|
|
|
if (isLeavingContainer) {
|
|
this._currentOverContainer.onDragLeaveContainer();
|
|
this._currentOverContainer = null;
|
|
this._currentOverContainerElem = null;
|
|
this._hideDropIndicator();
|
|
}
|
|
|
|
if (isOverContainer) {
|
|
let container = this.containers.findBy('element', overContainerElem);
|
|
if (!this._currentOverContainer) {
|
|
container.onDragEnterContainer();
|
|
}
|
|
|
|
this._currentOverContainer = container;
|
|
this._currentOverContainerElem = overContainerElem;
|
|
}
|
|
|
|
if (isLeavingDroppable) {
|
|
if (this._currentOverContainer) {
|
|
this._currentOverContainer.onDragLeaveDroppable(overDroppableElem);
|
|
}
|
|
this._currentOverDroppableElem = null;
|
|
}
|
|
|
|
if (isOverDroppable) {
|
|
// get position within the droppable
|
|
// TODO: cache droppable rects to avoid costly queries whilst dragging
|
|
let rect = overDroppableElem.getBoundingClientRect();
|
|
let inTop = this.draggableInfo.mousePosition.y < (rect.y + rect.height / 2);
|
|
let inLeft = this.draggableInfo.mousePosition.x < (rect.x + rect.width / 2);
|
|
let position = `${inTop ? 'top' : 'bottom'}-${inLeft ? 'left' : 'right'}`;
|
|
|
|
if (!this._currentOverDroppableElem) {
|
|
this._currentOverContainer.onDragEnterDroppable(overDroppableElem, position);
|
|
}
|
|
|
|
if (overDroppableElem !== this._currentOverDroppableElem || position !== this._currentOverDroppablePosition) {
|
|
this._currentOverDroppableElem = overDroppableElem;
|
|
this._currentOverDroppablePosition = position;
|
|
this._currentOverContainer.onDragOverDroppable(overDroppableElem, position);
|
|
|
|
// container.getIndicatorPosition returns false if the drop is not allowed
|
|
let indicatorPosition = this._currentOverContainer.getIndicatorPosition(this.draggableInfo, overDroppableElem, position);
|
|
if (indicatorPosition) {
|
|
this.draggableInfo.insertIndex = indicatorPosition.insertIndex;
|
|
this._showDropIndicator(indicatorPosition);
|
|
} else {
|
|
this._hideDropIndicator();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
_updateGhostElementPosition() {
|
|
if (this.isDragging) {
|
|
requestAnimationFrame(this._rafUpdateGhostElementPosition);
|
|
}
|
|
|
|
let {ghostInfo, draggableInfo} = this;
|
|
if (draggableInfo && ghostInfo) {
|
|
let left = (ghostInfo.positionX * -1) + draggableInfo.mousePosition.x;
|
|
let top = (ghostInfo.positionY * -1) + draggableInfo.mousePosition.y;
|
|
ghostInfo.element.style.transform = `translate3d(${left}px, ${top}px, 0)`;
|
|
}
|
|
},
|
|
|
|
// direction = horizontal/vertical
|
|
// horizontal = beforeElems shift left, afterElems shift right
|
|
// vertical = afterElems shift down
|
|
// position = above/below/left/right, used to place the indicator
|
|
_showDropIndicator({direction, position, beforeElems, afterElems}) {
|
|
let dropIndicator = this._dropIndicator;
|
|
|
|
// reset everything except insertIndex before re-displaying indicator
|
|
this._hideDropIndicator({clearInsertIndex: false});
|
|
|
|
if (direction === 'horizontal') {
|
|
beforeElems.forEach((elem) => {
|
|
elem.style.transform = 'translate3d(-30px, 0, 0)';
|
|
elem.style.transitionDuration = '250ms';
|
|
this._transformedDroppables.push(elem);
|
|
});
|
|
|
|
afterElems.forEach((elem) => {
|
|
elem.style.transform = 'translate3d(30px, 0, 0)';
|
|
elem.style.transitionDuration = '250ms';
|
|
this._transformedDroppables.push(elem);
|
|
});
|
|
|
|
let leftAdjustment = 0;
|
|
let droppable = this._currentOverDroppableElem;
|
|
let droppableStyles = getComputedStyle(droppable);
|
|
// calculate position based on offset parent to avoid the transform
|
|
// being accounted for
|
|
let parentRect = droppable.offsetParent.getBoundingClientRect();
|
|
let offsetLeft = parentRect.left + droppable.offsetLeft;
|
|
let offsetTop = parentRect.top + droppable.offsetTop;
|
|
|
|
if (position === 'left') {
|
|
leftAdjustment -= parseInt(droppableStyles.marginLeft);
|
|
} else {
|
|
leftAdjustment += parseInt(droppable.offsetWidth) + parseInt(droppableStyles.marginRight);
|
|
}
|
|
|
|
// account for indicator width
|
|
leftAdjustment -= 2;
|
|
|
|
let dropIndicatorParentRect = dropIndicator.parentNode.getBoundingClientRect();
|
|
let lastLeft = parseInt(dropIndicator.style.left);
|
|
let lastTop = parseInt(dropIndicator.style.top);
|
|
let newLeft = offsetLeft + leftAdjustment - dropIndicatorParentRect.left;
|
|
let newTop = offsetTop - dropIndicatorParentRect.top;
|
|
let newHeight = droppable.offsetHeight;
|
|
|
|
// if indicator hasn't moved, keep it showing, otherwise wait for
|
|
// the transform transitions to almost finish before re-positioning
|
|
// and showing
|
|
// NOTE: +- 1px is due to sub-pixel positioning of droppables
|
|
if (
|
|
newTop >= lastTop - 1 && newTop <= lastTop + 1 &&
|
|
newLeft >= lastLeft - 1 && newLeft <= lastLeft + 1
|
|
) {
|
|
dropIndicator.style.opacity = 1;
|
|
} else {
|
|
dropIndicator.style.opacity = 0;
|
|
|
|
this._dropIndicatorTimeout = run.later(this, function () {
|
|
dropIndicator.style.width = '4px';
|
|
dropIndicator.style.height = `${newHeight}px`;
|
|
dropIndicator.style.left = `${newLeft}px`;
|
|
dropIndicator.style.top = `${newTop}px`;
|
|
dropIndicator.style.opacity = 1;
|
|
}, 150);
|
|
}
|
|
}
|
|
|
|
if (direction === 'vertical') {
|
|
let transformSize = 60;
|
|
let droppable = this._currentOverDroppableElem;
|
|
let topElement, bottomElement;
|
|
|
|
if (position === 'top') {
|
|
topElement = utils.getPreviousSibling(droppable, constants.DROPPABLE_SELECTOR);
|
|
bottomElement = droppable;
|
|
} else if (position === 'bottom') {
|
|
topElement = droppable;
|
|
bottomElement = utils.getNextSibling(droppable, constants.DROPPABLE_SELECTOR);
|
|
}
|
|
|
|
// marginTop of the first element affects the offset of the
|
|
// children so it needs to be taken into account
|
|
let firstElement = (topElement || bottomElement).parentElement.children[0];
|
|
let firstElementStyles = getComputedStyle(firstElement);
|
|
let firstTopMargin = parseInt(firstElementStyles.marginTop);
|
|
|
|
let newWidth = droppable.offsetWidth;
|
|
let newLeft = droppable.offsetLeft;
|
|
let newTop;
|
|
|
|
if (topElement && bottomElement) {
|
|
let topElementStyles = getComputedStyle(topElement);
|
|
let bottomElementStyles = getComputedStyle(bottomElement);
|
|
|
|
let offsetTop = bottomElement.offsetTop;
|
|
|
|
let topMargin = parseInt(topElementStyles.marginBottom);
|
|
let bottomMargin = parseInt(bottomElementStyles.marginTop);
|
|
let marginHeight = topMargin + bottomMargin;
|
|
|
|
newTop = offsetTop - (marginHeight / 2) + firstTopMargin;
|
|
} else if (topElement) {
|
|
// at the bottom of the container
|
|
newTop = topElement.offsetTop + topElement.offsetHeight + firstTopMargin;
|
|
} else if (bottomElement) {
|
|
// at the top of the container, place the indicator 0px from the top
|
|
newTop = -26; // account for later adjustments and indicator height
|
|
transformSize = 30; // halve normal adjustment because there's no gap needed between top element
|
|
}
|
|
|
|
// account for indicator height
|
|
newTop -= 2;
|
|
|
|
// vertical always pushes elements down
|
|
newTop += 30;
|
|
|
|
// if indicator hasn't moved, keep it showing, otherwise wait for
|
|
// the transform transitions to almost finish before re-positioning
|
|
// and showing
|
|
// NOTE: +- 1px is due to sub-pixel positioning of droppables
|
|
let lastLeft = parseInt(dropIndicator.style.left);
|
|
let lastTop = parseInt(dropIndicator.style.top);
|
|
|
|
if (
|
|
newTop >= lastTop - 1 && newTop <= lastTop + 1 &&
|
|
newLeft >= lastLeft - 1 && newLeft <= lastLeft + 1
|
|
) {
|
|
dropIndicator.style.opacity = 1;
|
|
} else {
|
|
dropIndicator.style.opacity = 0;
|
|
|
|
this._dropIndicatorTimeout = run.later(this, function () {
|
|
dropIndicator.style.height = '4px';
|
|
dropIndicator.style.width = `${newWidth}px`;
|
|
dropIndicator.style.left = `${newLeft}px`;
|
|
dropIndicator.style.top = `${newTop}px`;
|
|
dropIndicator.style.opacity = 1;
|
|
}, 150);
|
|
}
|
|
|
|
// always update the droppable transforms so that re-positining in
|
|
// the same place still moves the elements. Effectively a no-op if
|
|
// the styles already exist
|
|
beforeElems.forEach((elem) => {
|
|
elem.style.transform = 'translate3d(0, 0, 0)';
|
|
elem.style.transitionDuration = '250ms';
|
|
this._transformedDroppables.push(elem);
|
|
});
|
|
|
|
afterElems.forEach((elem) => {
|
|
elem.style.transform = `translate3d(0, ${transformSize}px, 0)`;
|
|
elem.style.transitionDuration = '250ms';
|
|
this._transformedDroppables.push(elem);
|
|
});
|
|
}
|
|
},
|
|
|
|
_hideDropIndicator({clearInsertIndex = true} = {}) {
|
|
// make sure the indicator isn't shown due to a running timeout
|
|
run.cancel(this._dropIndicatorTimeout);
|
|
|
|
// clear droppable insert index unless instructed not to (eg, when
|
|
// resetting the display before re-positioning the indicator)
|
|
if (clearInsertIndex && this.draggableInfo) {
|
|
delete this.draggableInfo.insertIndex;
|
|
}
|
|
|
|
// reset all transforms
|
|
this._transformedDroppables.forEach((elem) => {
|
|
elem.style.transform = '';
|
|
});
|
|
this.transformedDroppables = A([]);
|
|
|
|
// hide drop indicator
|
|
if (this._dropIndicator) {
|
|
this._dropIndicator.style.opacity = 0;
|
|
}
|
|
},
|
|
|
|
_resetDrag() {
|
|
this._waitForDragStart.cancelAll();
|
|
this._hideDropIndicator();
|
|
this._removeMoveListeners();
|
|
this._removeReleaseListeners();
|
|
|
|
this.scrollHandler.dragStop();
|
|
|
|
if (this.grabbedElement) {
|
|
this.grabbedElement.style.opacity = '';
|
|
}
|
|
|
|
this.set('isDragging', false);
|
|
this.set('grabbedElement', null);
|
|
this.set('sourceContainer', null);
|
|
|
|
if (this.ghostInfo) {
|
|
this.ghostInfo.element.remove();
|
|
this.set('ghostInfo', null);
|
|
}
|
|
|
|
this.containers.forEach((container) => {
|
|
container.onDragEnd();
|
|
});
|
|
|
|
if (this._elementsWithHoverRemoved) {
|
|
this._elementsWithHoverRemoved.forEach((el) => {
|
|
el.classList.add('kg-card-hover');
|
|
});
|
|
}
|
|
delete this._elementsWithHoverRemoved;
|
|
|
|
utils.applyUserSelect(document.body, '');
|
|
document.querySelectorAll('[data-kg="editor"]').forEach((el) => {
|
|
el.style.cursor = '';
|
|
});
|
|
},
|
|
|
|
_appendDropIndicator() {
|
|
let dropIndicator = document.querySelector(`#${constants.DROP_INDICATOR_ID}`);
|
|
if (!dropIndicator) {
|
|
dropIndicator = document.createElement('div');
|
|
dropIndicator.id = constants.DROP_INDICATOR_ID;
|
|
dropIndicator.classList.add('bg-blue', 'br-pill');
|
|
dropIndicator.style.position = 'absolute';
|
|
dropIndicator.style.opacity = 0;
|
|
dropIndicator.style.width = '4px';
|
|
dropIndicator.style.height = 0;
|
|
dropIndicator.style.zIndex = constants.DROP_INDICATOR_ZINDEX;
|
|
dropIndicator.style.pointerEvents = 'none';
|
|
|
|
// TODO: the scrollableElement should probably be configurable, it
|
|
// may need to be set on a per-container basis in case there are
|
|
// scrollable containers within a card
|
|
let scrollableElement = document.querySelector('.koenig-editor')
|
|
|| utils.getDocumentScrollingElement();
|
|
scrollableElement.appendChild(dropIndicator);
|
|
}
|
|
this._dropIndicator = dropIndicator;
|
|
},
|
|
|
|
_appendGhostContainerElement() {
|
|
if (!this._ghostContainerElement) {
|
|
let ghostContainerElement = document.createElement('div');
|
|
ghostContainerElement.id = constants.GHOST_CONTAINER_ID;
|
|
ghostContainerElement.style.position = 'fixed';
|
|
ghostContainerElement.style.width = '100%';
|
|
document.body.appendChild(ghostContainerElement);
|
|
this._ghostContainerElement = ghostContainerElement;
|
|
}
|
|
},
|
|
|
|
_removeDropIndicator() {
|
|
if (this._dropIndicator) {
|
|
this._dropIndicator.remove();
|
|
}
|
|
},
|
|
|
|
_removeGhostContainerElement() {
|
|
if (this.ghostContainerElement) {
|
|
this.ghostContainerElement.remove();
|
|
}
|
|
},
|
|
|
|
_addGrabListeners() {
|
|
this._addEventListener('mousedown', this._onMouseDown, {passive: false});
|
|
},
|
|
|
|
_removeGrabListeners() {
|
|
this._removeEventListener('mousedown');
|
|
},
|
|
|
|
_addMoveListeners() {
|
|
this._addEventListener('mousemove', this._onMouseMove, {passive: false});
|
|
},
|
|
|
|
_removeMoveListeners() {
|
|
this._removeEventListener('mousemove');
|
|
},
|
|
|
|
_addReleaseListeners() {
|
|
this._addEventListener('mouseup', this._onMouseUp, {passive: false});
|
|
},
|
|
|
|
_removeReleaseListeners() {
|
|
this._removeEventListener('mouseup');
|
|
},
|
|
|
|
_addKeyDownListeners() {
|
|
this._addEventListener('keydown', this._onKeyDown);
|
|
},
|
|
|
|
_removeKeyDownListeners() {
|
|
this._removeEventListener('keydown');
|
|
},
|
|
|
|
_addEventListener(e, method, options) {
|
|
if (!this._eventHandlers[e]) {
|
|
let handler = run.bind(this, method);
|
|
this._eventHandlers[e] = {handler, options};
|
|
document.addEventListener(e, handler, options);
|
|
}
|
|
},
|
|
|
|
_removeEventListener(e) {
|
|
let event = this._eventHandlers[e];
|
|
if (event) {
|
|
document.removeEventListener(e, event.handler, event.options);
|
|
delete this._eventHandlers[e];
|
|
}
|
|
}
|
|
|
|
});
|