mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
✨ 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:
parent
7b3f5deb11
commit
526c94d954
4 changed files with 168 additions and 48 deletions
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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}"`);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue