mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
✨ Added drag-and-drop card re-ordering in the editor (#1085)
no issue - add vertical drop position indicator handling to `koenig-drag-drop-handler` service - fixed issues with nested drag-and-drop containers - register card drag/drop handler in `koenig-editor` - add drag icon creation
This commit is contained in:
parent
c2d9ea8e1c
commit
661b35ba51
9 changed files with 463 additions and 108 deletions
|
@ -341,7 +341,6 @@ export default Component.extend({
|
|||
.click();
|
||||
},
|
||||
|
||||
// TODO: revisit when the container is created and when drag is enabled/disabled
|
||||
// - rename container so that it's more explicit when we have an initial file
|
||||
// drop container vs a drag reorder+file drop container?
|
||||
_registerOrRefreshDragDropHandler() {
|
||||
|
@ -363,14 +362,9 @@ export default Component.extend({
|
|||
droppableSelector: '[data-image]',
|
||||
isDragEnabled: !isEmpty(this.images),
|
||||
onDragStart: run.bind(this, function () {
|
||||
// TODO: can this be handled in koenig-card?
|
||||
// not currently done so because kg-card-hover is added as a base class
|
||||
// by (kg-style "media-card")
|
||||
this.element.querySelector('figure').classList.remove('kg-card-hover');
|
||||
this.element.querySelector('figure').classList.remove('kg-card-selected');
|
||||
}),
|
||||
onDragEnd: run.bind(this, function () {
|
||||
this.element.querySelector('figure').classList.add('kg-card-hover');
|
||||
if (this.isSelected) {
|
||||
this.element.querySelector('figure').classList.add('kg-card-selected');
|
||||
}
|
||||
|
|
|
@ -4,10 +4,13 @@ import layout from '../templates/components/koenig-card';
|
|||
import {computed} from '@ember/object';
|
||||
import {htmlSafe} from '@ember/string';
|
||||
import {run} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const TICK_HEIGHT = 8;
|
||||
|
||||
export default Component.extend({
|
||||
koenigDragDropHandler: service(),
|
||||
|
||||
layout,
|
||||
attributeBindings: ['style'],
|
||||
classNameBindings: ['selectedClass'],
|
||||
|
@ -48,8 +51,12 @@ export default Component.extend({
|
|||
return htmlSafe(baseStyles);
|
||||
}),
|
||||
|
||||
toolbarStyle: computed('showToolbar', 'toolbarWidth', 'toolbarHeight', function () {
|
||||
let showToolbar = this.showToolbar;
|
||||
shouldShowToolbar: computed('showToolbar', 'koenigDragDropHandler.isDragging', function () {
|
||||
return this.showToolbar && !this.koenigDragDropHandler.isDragging;
|
||||
}),
|
||||
|
||||
toolbarStyle: computed('shouldShowToolbar', 'toolbarWidth', 'toolbarHeight', function () {
|
||||
let showToolbar = this.shouldShowToolbar;
|
||||
let width = this.toolbarWidth;
|
||||
let height = this.toolbarHeight;
|
||||
let styles = [];
|
||||
|
@ -167,7 +174,7 @@ export default Component.extend({
|
|||
mouseUp(event) {
|
||||
let {isSelected, isEditing, hasEditMode, _skipMouseUp} = this;
|
||||
|
||||
if (!_skipMouseUp && hasEditMode && isSelected && !isEditing) {
|
||||
if (!_skipMouseUp && hasEditMode && isSelected && !isEditing && !this.koenigDragDropHandler.isDragging) {
|
||||
this.editCard();
|
||||
this.set('showToolbar', true);
|
||||
event.preventDefault();
|
||||
|
|
|
@ -11,7 +11,7 @@ import MobiledocRange from 'mobiledoc-kit/utils/cursor/range';
|
|||
import calculateReadingTime from '../utils/reading-time';
|
||||
import countWords from '../utils/count-words';
|
||||
import defaultAtoms from '../options/atoms';
|
||||
import defaultCards, {CARD_COMPONENT_MAP} from '../options/cards';
|
||||
import defaultCards, {CARD_COMPONENT_MAP, CARD_ICON_MAP} from '../options/cards';
|
||||
import formatMarkdown from 'ghost-admin/utils/format-markdown';
|
||||
import layout from '../templates/components/koenig-editor';
|
||||
import parserPlugins from '../options/parser-plugins';
|
||||
|
@ -26,9 +26,12 @@ import {copy} from '@ember/object/internals';
|
|||
import {getContentFromPasteEvent} from 'mobiledoc-kit/utils/parse-utils';
|
||||
import {getLinkMarkupFromRange} from '../utils/markup-utils';
|
||||
import {getOwner} from '@ember/application';
|
||||
import {getParent} from '../lib/dnd/utils';
|
||||
import {guidFor} from '@ember/object/internals';
|
||||
import {isBlank} from '@ember/utils';
|
||||
import {run} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {svgJar} from 'ghost-admin/helpers/svg-jar';
|
||||
|
||||
const UNDO_DEPTH = 100;
|
||||
|
||||
|
@ -164,6 +167,8 @@ function insertImageCards(files, postEditor) {
|
|||
}
|
||||
|
||||
export default Component.extend({
|
||||
koenigDragDropHandler: service(),
|
||||
|
||||
layout,
|
||||
tagName: 'article',
|
||||
classNames: ['koenig-editor', 'w-100', 'flex-grow', 'relative', 'center', 'mb0', 'mt0'],
|
||||
|
@ -417,6 +422,7 @@ export default Component.extend({
|
|||
this.set('editor', editor);
|
||||
this.didCreateEditor(this);
|
||||
|
||||
run.schedule('afterRender', this, this._registerCardReorderDragDropHandler);
|
||||
run.schedule('afterRender', this, this._calculateWordCount);
|
||||
},
|
||||
|
||||
|
@ -459,7 +465,7 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
willDestroyElement() {
|
||||
let {editor, _dropTarget} = this;
|
||||
let {editor, _dropTarget, _cardDragDropContainer} = this;
|
||||
|
||||
_dropTarget.removeEventListener('dragover', this._dragOverHandler);
|
||||
_dropTarget.removeEventListener('dragleave', this._dragLeaveHandler);
|
||||
|
@ -471,7 +477,10 @@ export default Component.extend({
|
|||
let editorElement = this.element.querySelector('[data-kg="editor"]');
|
||||
editorElement.removeEventListener('paste', this._pasteHandler);
|
||||
|
||||
_cardDragDropContainer.destroy();
|
||||
|
||||
editor.destroy();
|
||||
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
|
@ -671,6 +680,11 @@ export default Component.extend({
|
|||
|
||||
// re-calculate word count
|
||||
this._calculateWordCount();
|
||||
|
||||
// refresh drag/drop
|
||||
// TODO: can be made more performant by only refreshing when droppable
|
||||
// order changes or when sections are added/removed
|
||||
this._cardDragDropContainer.refresh();
|
||||
},
|
||||
|
||||
cursorDidChange(editor) {
|
||||
|
@ -1099,6 +1113,22 @@ export default Component.extend({
|
|||
return this.componentCards.findBy('destinationElementId', cardId);
|
||||
},
|
||||
|
||||
getCardFromElement(element) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cardElement = element.querySelector('.__mobiledoc-card') || getParent(element, '.__mobiledoc-card');
|
||||
|
||||
if (!cardElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cardId = cardElement.firstChild.id;
|
||||
|
||||
return this.componentCards.findBy('destinationElementId', cardId);
|
||||
},
|
||||
|
||||
getSectionFromCard(card) {
|
||||
return card.env.postModel;
|
||||
},
|
||||
|
@ -1232,6 +1262,155 @@ export default Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
_registerCardReorderDragDropHandler() {
|
||||
let cardDragDropContainer = this.koenigDragDropHandler.registerContainer(this.editor.element, {
|
||||
draggableSelector: ':scope > div', // cards
|
||||
droppableSelector: ':scope > *', // all block elements
|
||||
onDragStart: run.bind(this, function () {
|
||||
this._cardDragDropContainer.refresh();
|
||||
}),
|
||||
getDraggableInfo: run.bind(this, this._getDraggableInfo),
|
||||
createGhostElement: run.bind(this, this._createCardDragElement),
|
||||
getIndicatorPosition: run.bind(this, this._getDropIndicatorPosition),
|
||||
onDrop: run.bind(this, this._onCardDrop)
|
||||
});
|
||||
|
||||
this._cardDragDropContainer = cardDragDropContainer;
|
||||
},
|
||||
|
||||
_getDraggableInfo(draggableElement) {
|
||||
let card = this.getCardFromElement(draggableElement);
|
||||
|
||||
if (!card) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: payload should probably contain everything here as well as the
|
||||
// card payload so that draggableInfo has a consistent shape
|
||||
return {
|
||||
type: 'card',
|
||||
cardName: card.cardName,
|
||||
payload: card.payload,
|
||||
destinationElementId: card.destinationElementId
|
||||
};
|
||||
},
|
||||
|
||||
_createCardDragElement(draggableInfo) {
|
||||
let {cardName} = draggableInfo;
|
||||
|
||||
if (!cardName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ghostElement = document.createElement('div');
|
||||
ghostElement.classList.add('absolute', 'flex', 'flex-column', 'justify-center',
|
||||
'items-center', 'w15', 'h15', 'br3', 'bg-white', 'shadow-2');
|
||||
ghostElement.style.top = '0';
|
||||
ghostElement.style.left = '-100%';
|
||||
ghostElement.style.zIndex = 10001;
|
||||
ghostElement.style.willChange = 'transform';
|
||||
|
||||
let iconElement = document.createElement('div');
|
||||
iconElement.classList.add('flex', 'items-center');
|
||||
|
||||
let svgIconHtml = svgJar(CARD_ICON_MAP[cardName], {class: 'w8 h8'});
|
||||
iconElement.insertAdjacentHTML('beforeend', svgIconHtml.string);
|
||||
|
||||
ghostElement.appendChild(iconElement);
|
||||
return ghostElement;
|
||||
},
|
||||
|
||||
_getDropIndicatorPosition(draggableInfo, droppableElem, position) {
|
||||
let droppables = Array.from(this.editor.element.querySelectorAll(':scope > *'));
|
||||
let droppableIndex = droppables.indexOf(droppableElem);
|
||||
let draggableIndex = droppables.indexOf(draggableInfo.element);
|
||||
|
||||
// only allow card drag/drop for now so it's not possible to drag
|
||||
// images out of a gallery and see drop indicators in the post content
|
||||
if (draggableInfo.type !== 'card') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._isCardDropAllowed(draggableIndex, droppableIndex, position)) {
|
||||
let insertIndex = droppableIndex;
|
||||
if (position.match(/bottom/)) {
|
||||
insertIndex += 1;
|
||||
}
|
||||
|
||||
let beforeElems, afterElems;
|
||||
if (position.match(/bottom/)) {
|
||||
beforeElems = droppables.slice(0, droppableIndex + 1);
|
||||
afterElems = droppables.slice(droppableIndex + 1);
|
||||
} else {
|
||||
beforeElems = droppables.slice(0, droppableIndex);
|
||||
afterElems = droppables.slice(droppableIndex);
|
||||
}
|
||||
|
||||
return {
|
||||
direction: 'vertical',
|
||||
position: position.match(/top/) ? 'top' : 'bottom',
|
||||
beforeElems,
|
||||
afterElems,
|
||||
insertIndex: insertIndex
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_onCardDrop(draggableInfo) {
|
||||
if (draggableInfo.type !== 'card') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let droppables = Array.from(this.editor.element.querySelectorAll(':scope > *'));
|
||||
let draggableIndex = droppables.indexOf(draggableInfo.element);
|
||||
|
||||
if (this._isCardDropAllowed(draggableIndex, draggableInfo.insertIndex)) {
|
||||
let card = this.getCardFromElement(draggableInfo.element);
|
||||
let cardSection = this.getSectionFromCard(card);
|
||||
let difference = draggableIndex - draggableInfo.insertIndex;
|
||||
|
||||
if (draggableIndex < draggableInfo.insertIndex) {
|
||||
difference += 1;
|
||||
}
|
||||
|
||||
if (difference !== 0) {
|
||||
this.editor.run((postEditor) => {
|
||||
do {
|
||||
if (difference > 0) {
|
||||
cardSection = postEditor.moveSectionUp(cardSection);
|
||||
difference -= 1;
|
||||
} else if (difference < 0) {
|
||||
cardSection = postEditor.moveSectionDown(cardSection);
|
||||
difference += 1;
|
||||
}
|
||||
} while (difference !== 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: more or less duplicated in koenig-card-gallery other than direction
|
||||
// - move to DnD container?
|
||||
_isCardDropAllowed(draggableIndex, droppableIndex, position = '') {
|
||||
// can't drop on itself or when droppableIndex doesn't exist
|
||||
if (draggableIndex === droppableIndex || typeof droppableIndex === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// account for dropping at beginning or end of a row
|
||||
if (position.match(/top/)) {
|
||||
droppableIndex -= 1;
|
||||
}
|
||||
|
||||
if (position.match(/bottom/)) {
|
||||
droppableIndex += 1;
|
||||
}
|
||||
|
||||
return droppableIndex !== draggableIndex;
|
||||
},
|
||||
|
||||
// calculate the number of words in rich-text sections and query cards for
|
||||
// their own word and image counts. Image counts are used for reading-time
|
||||
_calculateWordCount() {
|
||||
|
|
|
@ -17,17 +17,7 @@ class Container {
|
|||
element,
|
||||
draggables: A([]),
|
||||
droppables: A([]),
|
||||
isDragEnabled: true,
|
||||
|
||||
// TODO: move these to class-level functions?
|
||||
onDragStart() { },
|
||||
onDragEnterContainer() { },
|
||||
onDragEnterDroppable() { },
|
||||
onDragOverDroppable() { },
|
||||
onDragLeaveDroppable() { },
|
||||
onDragLeaveContainer() { },
|
||||
onDrop() { },
|
||||
onDragEnd() { }
|
||||
isDragEnabled: true
|
||||
}, options);
|
||||
|
||||
element.dataset[constants.CONTAINER_DATA_ATTR] = 'true';
|
||||
|
@ -51,50 +41,70 @@ class Container {
|
|||
return false;
|
||||
}
|
||||
|
||||
// override these via constructor options
|
||||
onDragStart() { }
|
||||
onDragEnterContainer() { }
|
||||
onDragEnterDroppable() { }
|
||||
onDragOverDroppable() { }
|
||||
onDragLeaveDroppable() { }
|
||||
onDragLeaveContainer() { }
|
||||
onDrop() { }
|
||||
onDragEnd() { }
|
||||
|
||||
// TODO: allow configuration for ghost element creation
|
||||
// builds an element that is attached to the mouse pointer when dragging.
|
||||
// currently grabs the first <img> and uses that but should be configurable:
|
||||
// - a selector for which element in the draggable to copy
|
||||
// - a function to hand off element creation to the consumer
|
||||
createGhostElement(draggable) {
|
||||
let image = draggable.querySelector('img');
|
||||
if (image) {
|
||||
let aspectRatio = image.width / image.height;
|
||||
let width, height;
|
||||
createGhostElement(draggableInfo) {
|
||||
if (draggableInfo.type === 'image') {
|
||||
let image = draggableInfo.element.querySelector('img');
|
||||
if (image) {
|
||||
let aspectRatio = image.width / image.height;
|
||||
let width, height;
|
||||
|
||||
// max ghost image size is 200px in either dimension
|
||||
if (image.width > image.height) {
|
||||
width = 200;
|
||||
height = 200 / aspectRatio;
|
||||
// max ghost image size is 200px in either dimension
|
||||
if (image.width > image.height) {
|
||||
width = 200;
|
||||
height = 200 / aspectRatio;
|
||||
} else {
|
||||
width = 200 * aspectRatio;
|
||||
height = 200;
|
||||
}
|
||||
|
||||
let ghostElement = document.createElement('img');
|
||||
ghostElement.width = width;
|
||||
ghostElement.height = height;
|
||||
ghostElement.id = 'koenig-drag-drop-ghost';
|
||||
ghostElement.src = image.src;
|
||||
ghostElement.style.position = 'absolute';
|
||||
ghostElement.style.top = '0';
|
||||
ghostElement.style.left = `-${width}px`;
|
||||
ghostElement.style.zIndex = constants.GHOST_ZINDEX;
|
||||
ghostElement.style.willChange = 'transform';
|
||||
|
||||
return ghostElement;
|
||||
} else {
|
||||
width = 200 * aspectRatio;
|
||||
height = 200;
|
||||
// eslint-disable-next-line
|
||||
console.warn('No <img> element found in draggable');
|
||||
return;
|
||||
}
|
||||
|
||||
let ghostElement = document.createElement('img');
|
||||
ghostElement.width = width;
|
||||
ghostElement.height = height;
|
||||
ghostElement.id = 'koenig-drag-drop-ghost';
|
||||
ghostElement.src = image.src;
|
||||
ghostElement.style.position = 'absolute';
|
||||
ghostElement.style.top = '0';
|
||||
ghostElement.style.left = `-${width}px`;
|
||||
ghostElement.style.zIndex = constants.GHOST_ZINDEX;
|
||||
ghostElement.style.willChange = 'transform';
|
||||
|
||||
return ghostElement;
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
console.warn('No <img> element found in draggable');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.warn(`No default createGhostElement handler for type "${draggableInfo.type}"`);
|
||||
}
|
||||
|
||||
enableDrag() {
|
||||
this.isDragEnabled = true;
|
||||
this.element.dataset[constants.CONTAINER_DATA_ATTR] = 'true';
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
disableDrag() {
|
||||
this.isDragEnabled = false;
|
||||
delete this.element.dataset[constants.CONTAINER_DATA_ATTR];
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
// used to add data attributes to any draggable/droppable elements. This is
|
||||
|
@ -110,16 +120,17 @@ class Container {
|
|||
|
||||
// re-populate draggable/droppable arrays
|
||||
this.draggables = A([]);
|
||||
this.element.querySelectorAll(this.draggableSelector).forEach((draggable) => {
|
||||
draggable.dataset[constants.DRAGGABLE_DATA_ATTR] = 'true';
|
||||
this.draggables.push(draggable);
|
||||
});
|
||||
|
||||
this.droppables = A([]);
|
||||
this.element.querySelectorAll(this.droppableSelector).forEach((droppable) => {
|
||||
droppable.dataset[constants.DROPPABLE_DATA_ATTR] = 'true';
|
||||
this.droppables.push(droppable);
|
||||
});
|
||||
if (this.isDragEnabled) {
|
||||
this.element.querySelectorAll(this.draggableSelector).forEach((draggable) => {
|
||||
draggable.dataset[constants.DRAGGABLE_DATA_ATTR] = 'true';
|
||||
this.draggables.push(draggable);
|
||||
});
|
||||
this.element.querySelectorAll(this.droppableSelector).forEach((droppable) => {
|
||||
droppable.dataset[constants.DROPPABLE_DATA_ATTR] = 'true';
|
||||
this.droppables.push(droppable);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,33 +1,18 @@
|
|||
// TODO: rename to closest? getParent can actually match passed in element
|
||||
export function getParent(element, value) {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
return getWithMatch(element, value, current => current.parentNode);
|
||||
}
|
||||
|
||||
let selector = value;
|
||||
let callback = value;
|
||||
export function getNextSibling(element, value) {
|
||||
// don't match the passed in element
|
||||
element = element.nextElementSibling;
|
||||
return getWithMatch(element, value, current => current.nextElementSibling);
|
||||
}
|
||||
|
||||
let isSelector = typeof value === 'string';
|
||||
let isFunction = typeof value === 'function';
|
||||
|
||||
function matches(currentElement) {
|
||||
if (!currentElement) {
|
||||
return currentElement;
|
||||
} else if (isSelector) {
|
||||
return currentElement.matches(selector);
|
||||
} else if (isFunction) {
|
||||
return callback(currentElement);
|
||||
}
|
||||
}
|
||||
|
||||
let current = element;
|
||||
|
||||
do {
|
||||
if (matches(current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
current = current.parentNode;
|
||||
} while (current && current !== document.body && current !== document);
|
||||
export function getPreviousSibling(element, value) {
|
||||
// don't match the passed in element
|
||||
element = element.previousElementSibling;
|
||||
return getWithMatch(element, value, current => current.previousElementSibling);
|
||||
}
|
||||
|
||||
export function getParentScrollableElement(element) {
|
||||
|
@ -66,6 +51,38 @@ export function applyUserSelect(element, value) {
|
|||
|
||||
/* Not exported --------------------------------------------------------------*/
|
||||
|
||||
function getWithMatch(element, value, next) {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let selector = value;
|
||||
let callback = value;
|
||||
|
||||
let isSelector = typeof value === 'string';
|
||||
let isFunction = typeof value === 'function';
|
||||
|
||||
function matches(currentElement) {
|
||||
if (!currentElement) {
|
||||
return currentElement;
|
||||
} else if (isSelector) {
|
||||
return currentElement.matches(selector);
|
||||
} else if (isFunction) {
|
||||
return callback(currentElement);
|
||||
}
|
||||
}
|
||||
|
||||
let current = element;
|
||||
|
||||
do {
|
||||
if (matches(current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
current = next(current);
|
||||
} while (current && current !== document.body && current !== document);
|
||||
}
|
||||
|
||||
function isStaticallyPositioned(element) {
|
||||
let position = getComputedStyle(element).getPropertyValue('position');
|
||||
return position === 'static';
|
||||
|
|
|
@ -12,6 +12,18 @@ export const CARD_COMPONENT_MAP = {
|
|||
gallery: 'koenig-card-gallery'
|
||||
};
|
||||
|
||||
// map card names to generic icons (used for ghost elements when dragging)
|
||||
export const CARD_ICON_MAP = {
|
||||
hr: 'koenig/kg-card-type-divider',
|
||||
image: 'koenig/kg-card-type-image',
|
||||
markdown: 'koenig/kg-card-type-markdown',
|
||||
'card-markdown': 'koenig/kg-card-type-markdown',
|
||||
html: 'koenig/kg-card-type-html',
|
||||
code: 'koenig/kg-card-type-gen-embed',
|
||||
embed: 'koenig/kg-card-type-gen-embed',
|
||||
gallery: 'koenig/kg-card-type-gallery'
|
||||
};
|
||||
|
||||
// TODO: move koenigOptions directly into cards now that card components register
|
||||
// themselves so that they are available on card.component
|
||||
export default [
|
||||
|
|
|
@ -41,7 +41,6 @@ export default Service.extend({
|
|||
this._addGrabListeners();
|
||||
|
||||
// append body elements
|
||||
this._appendDropIndicator();
|
||||
this._appendGhostContainerElement();
|
||||
},
|
||||
|
||||
|
@ -115,11 +114,6 @@ export default Service.extend({
|
|||
window.getSelection().removeAllRanges();
|
||||
// set up the drag details
|
||||
this._initiateDrag(event);
|
||||
// add watches to follow the drag/drop
|
||||
// TODO: move to _initiateDrag
|
||||
this._addMoveListeners();
|
||||
this._addReleaseListeners();
|
||||
this._addKeyDownListeners();
|
||||
}).catch((error) => {
|
||||
// ignore cancelled tasks and throw unrecognized errors
|
||||
if (!didCancel(error)) {
|
||||
|
@ -220,7 +214,19 @@ export default Service.extend({
|
|||
utils.applyUserSelect(document.body, 'none');
|
||||
|
||||
let container = this.sourceContainer;
|
||||
let draggableInfo = Object.assign({}, container.getDraggableInfo(this.grabbedElement), {
|
||||
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,
|
||||
|
@ -238,15 +244,27 @@ export default Service.extend({
|
|||
|
||||
// create the ghost element and cache it's position so avoid costly
|
||||
// getBoundingClientRect calls in the mousemove handler
|
||||
let ghostElement = container.createGhostElement(this.grabbedElement);
|
||||
this._ghostContainerElement.appendChild(ghostElement);
|
||||
let ghostElementRect = ghostElement.getBoundingClientRect();
|
||||
let ghostInfo = {
|
||||
element: ghostElement,
|
||||
positionX: ghostElementRect.x,
|
||||
positionY: ghostElementRect.y
|
||||
};
|
||||
this.set('ghostInfo', ghostInfo);
|
||||
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);
|
||||
|
@ -254,25 +272,43 @@ export default Service.extend({
|
|||
// 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.ghostInfo.element.hidden = true;
|
||||
this._ghostContainerElement.hidden = true;
|
||||
let target = document.elementFromPoint(
|
||||
this.draggableInfo.mousePosition.x,
|
||||
this.draggableInfo.mousePosition.y
|
||||
);
|
||||
this.draggableInfo.target = target;
|
||||
this.ghostInfo.element.hidden = false;
|
||||
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;
|
||||
|
@ -406,6 +442,7 @@ export default Service.extend({
|
|||
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`;
|
||||
|
@ -414,7 +451,94 @@ export default Service.extend({
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: handle vertical drag/drop
|
||||
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} = {}) {
|
||||
|
@ -423,7 +547,7 @@ export default Service.extend({
|
|||
|
||||
// clear droppable insert index unless instructed not to (eg, when
|
||||
// resetting the display before re-positioning the indicator)
|
||||
if (clearInsertIndex) {
|
||||
if (clearInsertIndex && this.draggableInfo) {
|
||||
delete this.draggableInfo.insertIndex;
|
||||
}
|
||||
|
||||
|
@ -447,7 +571,9 @@ export default Service.extend({
|
|||
|
||||
this.scrollHandler.dragStop();
|
||||
|
||||
this.grabbedElement.style.opacity = '';
|
||||
if (this.grabbedElement) {
|
||||
this.grabbedElement.style.opacity = '';
|
||||
}
|
||||
|
||||
this.set('isDragging', false);
|
||||
this.set('grabbedElement', null);
|
||||
|
@ -462,7 +588,15 @@ export default Service.extend({
|
|||
container.onDragEnd();
|
||||
});
|
||||
|
||||
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() {
|
||||
|
@ -470,7 +604,7 @@ export default Service.extend({
|
|||
if (!dropIndicator) {
|
||||
dropIndicator = document.createElement('div');
|
||||
dropIndicator.id = constants.DROP_INDICATOR_ID;
|
||||
dropIndicator.classList.add('bg-blue');
|
||||
dropIndicator.classList.add('bg-blue', 'br-pill');
|
||||
dropIndicator.style.position = 'absolute';
|
||||
dropIndicator.style.opacity = 0;
|
||||
dropIndicator.style.width = '4px';
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
))}}
|
||||
|
||||
{{#if toolbar}}
|
||||
<ul data-toolbar="true" class="kg-action-bar bg-darkgrey-d1 inline-flex pa0 ma0 pl1 pr1 nl1 list br3 shadow-2 items-center absolute white sans-serif f8 fw6 tracked-2 anim-fast-bezier z-999 {{if showToolbar "" "o-0 pop-down"}}" style={{toolbarStyle}}>
|
||||
<ul data-toolbar="true" class="kg-action-bar bg-darkgrey-d1 inline-flex pa0 ma0 pl1 pr1 nl1 list br3 shadow-2 items-center absolute white sans-serif f8 fw6 tracked-2 anim-fast-bezier z-999 {{if shouldShowToolbar "" "o-0 pop-down"}}" style={{toolbarStyle}}>
|
||||
{{#each toolbar.items as |item|}}
|
||||
{{#if item.divider}}
|
||||
<li class="ma0 kg-action-bar-divider bg-darkgrey-d2 h5"></li>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>kg-card-type-gen-embed</title><g fill="none" fill-rule="evenodd"><path d="M32 2.667C32 .889 31.111 0 29.333 0H2.667C1.93 0 1.302.26.78.781.261 1.301 0 1.931 0 2.667v26.666C0 31.111.889 32 2.667 32h26.666C31.111 32 32 31.111 32 29.333V2.667z" fill="#465961" fill-rule="nonzero"/><path stroke="#FFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M10.5 12l-4 4.333 4 3.667M21.5 12l4 4.333-4 3.667M18 11l-4 10"/></g></svg>
|
After Width: | Height: | Size: 529 B |
Loading…
Add table
Reference in a new issue