Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/src/.vuepress/components/events.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default {
"drag-enter": true,
"drag-leave": true,
"drop-not-allowed": true,
"should-refresh": false,
drop: true,
},
logPayload: true,
Expand Down Expand Up @@ -103,13 +104,29 @@ export default {
addColumn() {
this.groups.push(generate(this.groups.length + 1));
this.flags.push({ drop: true, animate: true });
this.log("should-refresh", "New column added - auto-refresh triggered");
},

removeColumn() {
this.groups.pop();
this.flags.pop();
},

addItem(groupIndex) {
const newItem = {
id: id(),
data: `New Item ${this.groups[groupIndex].length + 1}`,
};
this.groups[groupIndex].push(newItem);
this.log("should-refresh", "New item added - auto-refresh triggered");
},

removeItem(groupIndex) {
if (this.groups[groupIndex].length > 1) {
this.groups[groupIndex].pop();
}
},

log(name, ...args) {
if (this.logs[name]) {
this.logPayload ? console.log(name, ...args) : console.log(name);
Expand All @@ -130,10 +147,19 @@ export default {
<input type="checkbox" v-model="flags[index].animate" /> Animate
drop
</label>
<div style="display: flex; gap: 5px; margin-top: 8px;">
<button @click="addItem(index)" style="font-size: 0.8em; padding: 4px 8px; background: #28a745; color: white; border: none; border-radius: 3px;">
+ Item
</button>
<button @click="removeItem(index)" :disabled="groups[index].length <= 1" style="font-size: 0.8em; padding: 4px 8px; background: #dc3545; color: white; border: none; border-radius: 3px;">
- Item
</button>
</div>
</div>
<Container
:data-index="index"
group-name="column"
:should-refresh="true"
:get-child-payload="(itemIndex) => getChildPayload(index, itemIndex)"
:should-accept-drop="
(src, payload) => getShouldAcceptDrop(index, src, payload)
Expand Down
117 changes: 117 additions & 0 deletions docs/src/.vuepress/components/pagination.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<script>
import { Container, Draggable } from 'vue-dndrop'

export default {
name: 'Pagination',
components: {
Container,
Draggable
},
data() {
return {
cards: [
{ id: 1, title: 'Card 1' },
{ id: 2, title: 'Card 2' }
],

cards2: [
{ id: 201, title: 'Card 1' },
{ id: 202, title: 'Card 2' }
]
}
},
methods: {
getChildPayload(index) {
return this.cards[index]
},

onDrop(dropResult) {
const { removedIndex, addedIndex } = dropResult

if (removedIndex !== null && addedIndex !== null) {
const itemToAdd = this.cards[removedIndex]
const newCards = [...this.cards]
newCards.splice(removedIndex, 1)
newCards.splice(addedIndex, 0, itemToAdd)
this.cards = newCards
}
},

getChildPayload2(index) {
return this.cards2[index]
},

onDrop2(dropResult) {
const { removedIndex, addedIndex } = dropResult

if (removedIndex !== null && addedIndex !== null) {
const itemToAdd = this.cards2[removedIndex]
const newCards = [...this.cards2]
newCards.splice(removedIndex, 1)
newCards.splice(addedIndex, 0, itemToAdd)
this.cards2 = newCards
}
},

addCard() {
const newId = this.cards.length + 1
this.cards.push({
id: newId,
title: `Card ${newId}`
})
},

addCard2() {
const newId = this.cards2.length + 201
this.cards2.push({
id: newId,
title: `Card ${newId - 200}`
})
}
}
}
</script>
<template>
<div>
<h3>Example 1: With Auto-refresh</h3>
<p>This container has <code>:should-refresh="true"</code> which automatically refreshes drag animations for dynamically added items (pagination, infinite scroll, etc.).</p>

<Container
:should-refresh="true"
:animation-duration="300"
@drop="onDrop"
:get-child-payload="getChildPayload"
>
<Draggable v-for="card in cards" :key="card.id">
<div class="draggable-item" :style="{ borderLeft: card.id <= 2 ? '5px solid #28a745' : '5px solid #007bff' }">
{{ card.title }}
</div>
</Draggable>
</Container>

<button @click="addCard" style="margin: 10px 0; padding: 8px 12px;">
Add Card
</button>

<hr style="margin: 40px 0;">

<h3>Example 2: Without Auto-refresh</h3>
<p>This container has NO <code>should-refresh</code> prop - dynamically added items won't have proper drag animations until manually refreshed.</p>

<Container
:animation-duration="300"
@drop="onDrop2"
:get-child-payload="getChildPayload2"
>
<Draggable v-for="card in cards2" :key="card.id">
<div class="draggable-item" :style="{ borderLeft: card.id <= 202 ? '5px solid #28a745' : '5px solid #dc3545' }">
{{ card.title }}
</div>
</Draggable>
</Container>

<button @click="addCard2" style="margin: 10px 0; padding: 8px 12px;">
Add Card
</button>
</div>
</template>
1 change: 1 addition & 0 deletions docs/src/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ module.exports = {
"/examples/simple-horizontal",
"/examples/lock-axis",
"/examples/nested",
"/examples/pagination",
"/examples/simple-scroller",
"/examples/simple",
"/examples/simple-tagless",
Expand Down
1 change: 1 addition & 0 deletions docs/src/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ For every example created in this documentation, there are some that uses some h
- [Groups](groups.html)
- [Lock axis](lock-axis.html)
- [Nested](nested.html)
- [Pagination](pagination.html)
- [Simple](simple.html)
- [Simple (no tags)](simple-tagless.html)
- [Simple horizontal](simple-horizontal.html)
Expand Down
27 changes: 27 additions & 0 deletions docs/src/examples/pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Pagination & Dynamic Content


> The `:should-refresh="true"` prop enables automatic registration of newly-added draggable items. Use it whenever the children of a `Container` change after mount — pagination, infinite scroll, reassigning the underlying array, or any other dynamic update.

Without `should-refresh`, items appended to (or replacing) the source array after the first render are not picked up as draggables until the next drag starts, which can leave them without animation transitions and — in some cases — produce a `null` `addedIndex` on drop.


<doc-example title="Pagination" file="pagination" />

:::tip


#### When to enable it:
- **Pagination** — loading new pages of data from an API
- **Infinite scroll** — appending items as the user scrolls
- **Array reassignment** — replacing the source array with a new one (e.g. after a fetch or filter)
- **Dynamic content** — items added after the initial component mount
- **Search results** — items added or refreshed on user input

:::

:::warning

`should-refresh` only reacts to DOM changes while no drag is in progress. If you need to add items *during* an active drag, call the imperative `refreshDraggables()` method on the container instance instead.

:::
1 change: 1 addition & 0 deletions src/Container.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default {
dragClass: String,
dropClass: String,
removeOnDropOut: { type: Boolean, default: false },
shouldRefresh: { type: Boolean, default: false },
'drag-start': Function,
'drag-end': Function,
drop: Function,
Expand Down
56 changes: 56 additions & 0 deletions src/utils/container/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -946,14 +946,70 @@ const vueDndrop = function (element, options) {
const container = containerIniter(options);
element[containerInstance] = container;
Mediator.register(container);

// Optional auto-refresh for pagination/dynamic content
let observer = null;

// Only setup MutationObserver if shouldRefresh is enabled
if (container.getOptions().shouldRefresh) {
observer = new MutationObserver((mutations) => {
// Don't interfere if drag is currently active
if (Mediator.isDragging()) return;

// Check if any elements were added
const hasNewElements = mutations.some(mutation =>
mutation.type === 'childList' &&
mutation.addedNodes.length > 0 &&
Array.from(mutation.addedNodes).some(node => node.nodeType === 1)
);

if (hasNewElements) {
// Allow Vue's DOM update to settle before re-registering
setTimeout(() => {
if (!Mediator.isDragging()) {
// Store existing draggables before refresh
const existingDraggables = new Set(container.draggables);

// Refresh draggables list to include new elements
container.setDraggables();

// Apply animations only to newly added draggables
container.draggables.forEach(draggable => {
if (!existingDraggables.has(draggable) && !draggable.classList.contains('dndrop-ghost')) {
setAnimation(draggable, true, container.getOptions().animationDuration);
}
});
}
}, 100);
}
});

observer.observe(element, {
childList: true,
subtree: false
});
}

return {
dispose () {
if (observer) {
observer.disconnect();
}
Mediator.unregister(container);
container.dispose(container);
},
setOptions (options, merge) {
container.setOptions(options, merge);
},
refreshDraggables () {
container.setDraggables();
container.layout.invalidateRects();
const draggables = container.draggables.filter(element =>
!element.classList.contains('dndrop-ghost'));
draggables.forEach(function (p) {
return setAnimation(p, true, container.getOptions().animationDuration);
});
},
};
};

Expand Down
1 change: 1 addition & 0 deletions src/utils/container/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const defaultOptions = {
autoScrollEnabled: true,
shouldAcceptDrop: undefined,
shouldAnimateDrop: undefined,
shouldRefresh: false, // Enable automatic refresh for dynamic content (pagination, array reassignment)
};