From 41d4de95f78a8d6a035e33b2a5161a7e77cb18de Mon Sep 17 00:00:00 2001 From: amendx Date: Wed, 20 May 2026 10:27:06 -0300 Subject: [PATCH] feat(core): add should-refresh option for dynamic content Adds an opt-in Container prop. When enabled, a MutationObserver picks up draggable children added after mount (pagination, infinite scroll, array reassignment) and registers them without requiring a manual refresh. Also exposes an imperative method on the container instance for cases where DOM-mutation watching is not enough (e.g. mid-drag additions). Closes #53 Closes #97 --- docs/src/.vuepress/components/events.vue | 26 +++++ docs/src/.vuepress/components/pagination.vue | 117 +++++++++++++++++++ docs/src/.vuepress/config.js | 1 + docs/src/examples/README.md | 1 + docs/src/examples/pagination.md | 27 +++++ src/Container.js | 1 + src/utils/container/container.js | 56 +++++++++ src/utils/container/defaults.js | 1 + 8 files changed, 230 insertions(+) create mode 100644 docs/src/.vuepress/components/pagination.vue create mode 100644 docs/src/examples/pagination.md diff --git a/docs/src/.vuepress/components/events.vue b/docs/src/.vuepress/components/events.vue index afa4820..aef4e07 100644 --- a/docs/src/.vuepress/components/events.vue +++ b/docs/src/.vuepress/components/events.vue @@ -37,6 +37,7 @@ export default { "drag-enter": true, "drag-leave": true, "drop-not-allowed": true, + "should-refresh": false, drop: true, }, logPayload: true, @@ -103,6 +104,7 @@ 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() { @@ -110,6 +112,21 @@ export default { 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); @@ -130,10 +147,19 @@ export default { Animate drop +
+ + +
which automatically refreshes drag animations for dynamically added items (pagination, infinite scroll, etc.).

+ + + +
+ {{ card.title }} +
+
+
+ + + +
+ +

Example 2: Without Auto-refresh

+

This container has NO should-refresh prop - dynamically added items won't have proper drag animations until manually refreshed.

+ + + +
+ {{ card.title }} +
+
+
+ + + + diff --git a/docs/src/.vuepress/config.js b/docs/src/.vuepress/config.js index 47a6fcd..9a68976 100755 --- a/docs/src/.vuepress/config.js +++ b/docs/src/.vuepress/config.js @@ -85,6 +85,7 @@ module.exports = { "/examples/simple-horizontal", "/examples/lock-axis", "/examples/nested", + "/examples/pagination", "/examples/simple-scroller", "/examples/simple", "/examples/simple-tagless", diff --git a/docs/src/examples/README.md b/docs/src/examples/README.md index ba4f0fc..daa5fdb 100644 --- a/docs/src/examples/README.md +++ b/docs/src/examples/README.md @@ -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) diff --git a/docs/src/examples/pagination.md b/docs/src/examples/pagination.md new file mode 100644 index 0000000..1e6b7b0 --- /dev/null +++ b/docs/src/examples/pagination.md @@ -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. + + + + +:::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. + +::: diff --git a/src/Container.js b/src/Container.js index ffa5b65..2eb85c9 100644 --- a/src/Container.js +++ b/src/Container.js @@ -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, diff --git a/src/utils/container/container.js b/src/utils/container/container.js index 0983349..017b6a7 100644 --- a/src/utils/container/container.js +++ b/src/utils/container/container.js @@ -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); + }); + }, }; }; diff --git a/src/utils/container/defaults.js b/src/utils/container/defaults.js index bdd22ac..b6ada40 100644 --- a/src/utils/container/defaults.js +++ b/src/utils/container/defaults.js @@ -7,4 +7,5 @@ export const defaultOptions = { autoScrollEnabled: true, shouldAcceptDrop: undefined, shouldAnimateDrop: undefined, + shouldRefresh: false, // Enable automatic refresh for dynamic content (pagination, array reassignment) };