diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-gallery.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-gallery.js
index 47c743fdf5..316b1a5c42 100644
--- a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-gallery.js
+++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-gallery.js
@@ -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');
}
diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-card.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-card.js
index 810e966485..bb5528837c 100644
--- a/ghost/admin/lib/koenig-editor/addon/components/koenig-card.js
+++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-card.js
@@ -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();
diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js
index 9fb5df4d50..a77bb2f2ac 100644
--- a/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js
+++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js
@@ -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() {
diff --git a/ghost/admin/lib/koenig-editor/addon/lib/dnd/container.js b/ghost/admin/lib/koenig-editor/addon/lib/dnd/container.js
index cde76995ac..48f4e702b4 100644
--- a/ghost/admin/lib/koenig-editor/addon/lib/dnd/container.js
+++ b/ghost/admin/lib/koenig-editor/addon/lib/dnd/container.js
@@ -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 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
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
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);
+ });
+ }
}
}
diff --git a/ghost/admin/lib/koenig-editor/addon/lib/dnd/utils.js b/ghost/admin/lib/koenig-editor/addon/lib/dnd/utils.js
index b9d9597aa2..3ce22253c3 100644
--- a/ghost/admin/lib/koenig-editor/addon/lib/dnd/utils.js
+++ b/ghost/admin/lib/koenig-editor/addon/lib/dnd/utils.js
@@ -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';
diff --git a/ghost/admin/lib/koenig-editor/addon/options/cards.js b/ghost/admin/lib/koenig-editor/addon/options/cards.js
index 49d33ba030..4d9b84ad77 100644
--- a/ghost/admin/lib/koenig-editor/addon/options/cards.js
+++ b/ghost/admin/lib/koenig-editor/addon/options/cards.js
@@ -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 [
diff --git a/ghost/admin/lib/koenig-editor/addon/services/koenig-drag-drop-handler.js b/ghost/admin/lib/koenig-editor/addon/services/koenig-drag-drop-handler.js
index 79765a150b..1ba1be1109 100644
--- a/ghost/admin/lib/koenig-editor/addon/services/koenig-drag-drop-handler.js
+++ b/ghost/admin/lib/koenig-editor/addon/services/koenig-drag-drop-handler.js
@@ -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';
diff --git a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card.hbs b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card.hbs
index 1a13c6a01f..079eae4e91 100644
--- a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card.hbs
+++ b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card.hbs
@@ -12,7 +12,7 @@
))}}
{{#if toolbar}}
-