Added ability to drag images in and out of galleries

no issue

- adjust drag handlers in the editor and gallery card to handle drag/drop of image cards as well as straight images
- adjust drag handlers in the gallery card to handle image inserts as well as re-orders
- add `onDragEnd` event/action to the Koenig drag-n-drop handler so that containers can perform cleanup if one of their draggables was successfully dropped into a different container
- change ghost element when dragging an image card to be an image rather than a card icon
  - allow `createGhostElement` function passed in when registering a drag-n-drop container to fall back to the default behaviour by returning a falsy value
This commit is contained in:
Kevin Ansfield 2019-06-27 16:34:23 +01:00
parent 6140b3ebf0
commit aa8c0200e3
4 changed files with 168 additions and 48 deletions

View File

@ -360,17 +360,12 @@ export default Component.extend({
draggableSelector: '[data-image]',
droppableSelector: '[data-image]',
isDragEnabled: !isEmpty(this.images),
onDragStart: run.bind(this, function () {
this.element.querySelector('figure').classList.remove('kg-card-selected');
}),
onDragEnd: run.bind(this, function () {
if (this.isSelected) {
this.element.querySelector('figure').classList.add('kg-card-selected');
}
}),
onDragStart: run.bind(this, this._dragStart),
onDragEnd: run.bind(this, this._dragEnd),
getDraggableInfo: run.bind(this, this._getDraggableInfo),
getIndicatorPosition: run.bind(this, this._getDropIndicatorPosition),
onDrop: run.bind(this, this._onDrop)
onDrop: run.bind(this, this._onDrop),
onDropEnd: run.bind(this, this._onDropEnd)
}
);
}
@ -378,10 +373,28 @@ export default Component.extend({
}
},
_dragStart(draggableInfo) {
this.element.querySelector('figure').classList.remove('kg-card-selected');
// enable dropping when an image is dragged in from outside of this card
let isImageDrag = draggableInfo.type === 'image' || draggableInfo.cardName === 'image';
if (isImageDrag && draggableInfo.payload.src && this.images.length !== MAX_IMAGES) {
this._dragDropContainer.enableDrag();
}
},
_dragEnd() {
if (this.isSelected) {
this.element.querySelector('figure').classList.add('kg-card-selected');
} else {
this._dragDropContainer.disableDrag();
}
},
_getDraggableInfo(draggableElement) {
let src = draggableElement.querySelector('img').getAttribute('src');
let image = this.images.findBy('src', src) || this.images.findBy('previewSrc', src);
let payload = image && image.getProperties('fileName', 'src', 'row', 'width', 'height');
let payload = image && image.getProperties('fileName', 'src', 'row', 'width', 'height', 'caption');
if (image) {
return {
@ -393,9 +406,9 @@ export default Component.extend({
return {};
},
_onDrop(draggableInfo/*, droppableElem, position*/) {
// do not allow dragging between galleries for now
if (!this.element.contains(draggableInfo.element)) {
_onDrop(draggableInfo, droppableElem, position) {
// do not allow dropping of non-images
if (draggableInfo.type !== 'image' && draggableInfo.cardName !== 'image') {
return false;
}
@ -403,12 +416,56 @@ export default Component.extend({
let draggableIndex = droppables.indexOf(draggableInfo.element);
if (this._isDropAllowed(draggableIndex, draggableInfo.insertIndex)) {
let draggedImage = this.images.findBy('src', draggableInfo.payload.src);
if (draggableIndex === -1) {
// external image being added
let {payload} = draggableInfo;
let img = draggableInfo.element.querySelector(`img[src="${payload.src}"]`);
let insertIndex = draggableInfo.insertIndex;
// insert index needs adjusting because we're not shuffling
if (position && position.match(/right/)) {
insertIndex += 1;
}
// image card payloads may not have all of the details we need but we can fill them in
payload.width = payload.width || img.naturalWidth;
payload.height = payload.height || img.naturalHeight;
if (!payload.fileName) {
let url = new URL(img.src);
let fileName = url.pathname.match(/\/([^/]*)$/)[1];
payload.fileName = fileName;
}
this.images.insertAt(insertIndex, EmberObject.create(payload));
} else {
// internal image being re-ordered
let draggedImage = this.images.findBy('src', draggableInfo.payload.src);
this.images.removeObject(draggedImage);
this.images.insertAt(draggableInfo.insertIndex, draggedImage);
}
this.images.removeObject(draggedImage);
this.images.insertAt(draggableInfo.insertIndex, draggedImage);
this._recalculateImageRows();
this._buildAndSaveImagesPayload();
this._dragDropContainer.refresh();
this._skipOnDragEnd = true;
return true;
}
return false;
},
// if an image is dragged out of a gallery we need to remove it
_onDropEnd(draggableInfo, success) {
if (this._skipOnDragEnd || !success) {
this._skipOnDragEnd = false;
return;
}
let image = this.images.findBy('src', draggableInfo.payload.src);
if (image) {
this.images.removeObject(image);
this._recalculateImageRows();
this._buildAndSaveImagesPayload();
this._dragDropContainer.refresh();
}
@ -422,8 +479,8 @@ export default Component.extend({
// droppableIndex:
// }
_getDropIndicatorPosition(draggableInfo, droppableElem, position) {
// do not allow dragging between galleries for now
if (!this.element.contains(draggableInfo.element)) {
// do not allow dropping of non-images
if (draggableInfo.type !== 'image' && draggableInfo.cardName !== 'image') {
return false;
}
@ -480,6 +537,11 @@ export default Component.extend({
// we don't allow an image to be dropped where it would end up in the
// same position within the gallery
_isDropAllowed(draggableIndex, droppableIndex, position = '') {
// external images can always be dropped
if (draggableIndex === -1) {
return true;
}
// can't drop on itself or when droppableIndex doesn't exist
if (draggableIndex === droppableIndex || typeof droppableIndex === 'undefined') {
return false;

View File

@ -1286,18 +1286,21 @@ export default Component.extend({
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();
}),
onDragStart: run.bind(this, this._onDragStart),
getDraggableInfo: run.bind(this, this._getDraggableInfo),
createGhostElement: run.bind(this, this._createCardDragElement),
getIndicatorPosition: run.bind(this, this._getDropIndicatorPosition),
onDrop: run.bind(this, this._onCardDrop)
onDrop: run.bind(this, this._onCardDrop),
onDropEnd: run.bind(this, this._onDropEnd)
});
this._cardDragDropContainer = cardDragDropContainer;
},
_onDragStart() {
this._cardDragDropContainer.refresh();
},
_getDraggableInfo(draggableElement) {
let card = this.getCardFromElement(draggableElement);
@ -1318,7 +1321,7 @@ export default Component.extend({
_createCardDragElement(draggableInfo) {
let {cardName} = draggableInfo;
if (!cardName) {
if (!cardName || cardName === 'image') {
return;
}
@ -1345,9 +1348,8 @@ export default Component.extend({
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') {
// allow card and image drops (images can be dragged out of a gallery)
if (draggableInfo.type !== 'card' && draggableInfo.type !== 'image') {
return false;
}
@ -1379,7 +1381,7 @@ export default Component.extend({
},
_onCardDrop(draggableInfo) {
if (draggableInfo.type !== 'card') {
if (draggableInfo.type !== 'card' && draggableInfo.type !== 'image') {
return false;
}
@ -1387,26 +1389,46 @@ export default Component.extend({
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 (draggableInfo.type === 'card') {
let card = this.getCardFromElement(draggableInfo.element);
let cardSection = this.getSectionFromCard(card);
let difference = draggableIndex - draggableInfo.insertIndex;
if (draggableIndex < draggableInfo.insertIndex) {
difference += 1;
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);
});
}
// make sure we don't remove the dropped card in the card->card drop handler
this._skipOnDropEnd = true;
return true;
}
if (difference !== 0) {
if (draggableInfo.type === 'image') {
// we need to create an image card from a raw image payload
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);
let imageCard = postEditor.builder.createCardSection('image', draggableInfo.payload);
let sections = this.editor.post.sections;
let droppableSection = sections.objectAt(draggableInfo.insertIndex);
postEditor.insertSectionBefore(sections, imageCard, droppableSection);
postEditor.setRange(imageCard.tailPosition());
});
return true;
}
}
},
@ -1414,6 +1436,11 @@ export default Component.extend({
// TODO: more or less duplicated in koenig-card-gallery other than direction
// - move to DnD container?
_isCardDropAllowed(draggableIndex, droppableIndex, position = '') {
// images can be dragged out of a gallery to any position
if (draggableIndex === -1) {
return true;
}
// can't drop on itself or when droppableIndex doesn't exist
if (draggableIndex === droppableIndex || typeof droppableIndex === 'undefined') {
return false;
@ -1431,6 +1458,17 @@ export default Component.extend({
return droppableIndex !== draggableIndex;
},
// a card can be dropped into another card which means we need to remove the original
_onDropEnd(draggableInfo, success) {
if (this._skipOnDropEnd || !success || draggableInfo.type !== 'card') {
this._skipOnDropEnd = false;
return;
}
let card = this.getCardFromElement(draggableInfo.element);
this.deleteCard(card, NO_CURSOR_MOVEMENT);
},
// 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

@ -13,6 +13,11 @@ import {A} from '@ember/array';
class Container {
constructor(element, options) {
if (options.createGhostElement) {
this._createGhostElement = options.createGhostElement;
delete options.createGhostElement;
}
Object.assign(this, {
element,
draggables: A([]),
@ -48,8 +53,9 @@ class Container {
onDragOverDroppable() { }
onDragLeaveDroppable() { }
onDragLeaveContainer() { }
onDrop() { }
onDragEnd() { }
onDrop() { }
onDropEnd() { }
// TODO: allow configuration for ghost element creation
// builds an element that is attached to the mouse pointer when dragging.
@ -57,7 +63,13 @@ class Container {
// - a selector for which element in the draggable to copy
// - a function to hand off element creation to the consumer
createGhostElement(draggableInfo) {
if (draggableInfo.type === 'image') {
let ghostElement;
if (typeof this._createGhostElement === 'function') {
ghostElement = this._createGhostElement(draggableInfo);
}
if (!ghostElement && (draggableInfo.type === 'image' || draggableInfo.cardName === 'image')) {
let image = draggableInfo.element.querySelector('img');
if (image) {
let aspectRatio = image.width / image.height;
@ -72,7 +84,7 @@ class Container {
height = 200;
}
let ghostElement = document.createElement('img');
ghostElement = document.createElement('img');
ghostElement.width = width;
ghostElement.height = height;
ghostElement.id = 'koenig-drag-drop-ghost';
@ -82,8 +94,6 @@ class Container {
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');
@ -91,6 +101,10 @@ class Container {
}
}
if (ghostElement) {
return ghostElement;
}
// eslint-disable-next-line
console.warn(`No default createGhostElement handler for type "${draggableInfo.type}"`);
}

View File

@ -149,15 +149,21 @@ export default Service.extend({
_onMouseUp(/*event*/) {
if (this.draggableInfo) {
let success = false;
// TODO: accept object rather than positioned args? OR, should the
// droppable data be stored on draggableInfo?
if (this._currentOverContainer) {
this._currentOverContainer.onDrop(
success = this._currentOverContainer.onDrop(
this.draggableInfo,
this._currentOverDroppableElem,
this._currentOverDroppablePosition
);
}
this.containers.forEach((container) => {
container.onDropEnd(this.draggableInfo, success);
});
}
// remove drag info and any ghost element