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}} -