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:
Kevin Ansfield 2018-12-17 14:02:40 +00:00 committed by GitHub
parent d27c067bd1
commit bc788994f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 463 additions and 108 deletions

View File

@ -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');
}

View File

@ -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();

View File

@ -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() {

View File

@ -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);
});
}
}
}

View File

@ -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';

View File

@ -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 [

View File

@ -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';

View File

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

View File

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