Skip to content
Merged
25 changes: 25 additions & 0 deletions packages/inline-repeater-interface/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Changelog

All notable changes to this project will be documented in this file.

## [1.0.1] - 2025-09-23

### Improved
- Enhanced focus styling for better accessibility compliance
- Fixed focus ring positioning and conflicts with global CSS rules

### Fixed
- Fixed expansion state preservation during drag and drop operations
- Expanded items now remain expanded when reordered via drag and drop
- Index-based expansion tracking now properly updates when items are moved

## [1.0.0] - Initial Release

### Added
- Initial implementation of inline repeater interface
- Support for inline editing and reordering of repeatable form fields
- Accordion-style expandable items
- Drag and drop functionality for reordering
- Add/remove item controls
- Template-based header rendering
- Integration with Directus form system
18 changes: 9 additions & 9 deletions packages/inline-repeater-interface/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@directus-labs/inline-repeater-interface",
"type": "module",
"version": "1.0.0",
"version": "1.0.1",
"description": "A powerful interface for managing repeatable form fields within Directus that allows inline editing and reordering of items.",
"license": "MIT",
"keywords": [
Expand All @@ -26,18 +26,18 @@
"validate": "directus-extension validate"
},
"dependencies": {
"@directus/format-title": "^12.0.0",
"lodash": "^4.17.21",
"nanoid": "^5.0.7",
"reka-ui": "1.0.0-alpha.8",
"sass": "^1.77.8",
"vue": "^3.4.38",
"vue-i18n": "^11.0.1",
"@directus/format-title": "12.1.0",
"lodash": "4.17.21",
"reka-ui": "2.5.0",
"sass": "1.77.8",
"vue": "3.5.18",
"vue-i18n": "11.1.11",
"vuedraggable": "4.1.0"
},
"devDependencies": {
"@directus/extensions-sdk": "13.0.0",
"@directus/extensions-sdk": "16.0.2",
"@directus/types": "^13.0.0",
"@types/lodash": "^4.17.20",
"typescript": "^5.5.4"
}
}
63 changes: 59 additions & 4 deletions packages/inline-repeater-interface/src/list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,54 @@ const internalValue = computed({
});

const expandedItems = ref<number[]>([]);
const draggedItemIndex = ref<number | null>(null);
const isDragging = ref(false);

function isExpanded(index: number) {
return expandedItems.value.includes(index);
}

function onDragStart(evt: any) {
draggedItemIndex.value = evt.oldIndex;
isDragging.value = true;
}

function onDragEnd(evt: any) {
if (draggedItemIndex.value !== null && evt.newIndex !== evt.oldIndex) {
const oldIndex = draggedItemIndex.value;
const newIndex = evt.newIndex;

// Update expanded items to reflect the new positions
const updatedExpandedItems = expandedItems.value.map((expandedIndex) => {
// If the dragged item was expanded, update its index to the new position
if (expandedIndex === oldIndex) {
return newIndex;
}

// Adjust other expanded items that were shifted by the drag
if (oldIndex < newIndex) {
// Item moved down: shift items between oldIndex and newIndex up
if (expandedIndex > oldIndex && expandedIndex <= newIndex) {
return expandedIndex - 1;
}
}
else {
// Item moved up: shift items between newIndex and oldIndex down
if (expandedIndex >= newIndex && expandedIndex < oldIndex) {
return expandedIndex + 1;
}
}

return expandedIndex;
});

expandedItems.value = updatedExpandedItems;
}

draggedItemIndex.value = null;
isDragging.value = false;
}

const itemToRemove = ref<number | null>(null);

// eslint-disable-next-line unused-imports/no-unused-vars
Expand Down Expand Up @@ -160,7 +203,7 @@ function addNew() {
// Focus the first input of the last form
nextTick(() => {
const forms = document.querySelectorAll('.list-item-form');
const lastForm = forms.at(-1);
const lastForm = Array.from(forms).at(-1);
const firstInput = lastForm?.querySelector('input, select, textarea');

if (firstInput instanceof HTMLElement) {
Expand Down Expand Up @@ -231,11 +274,14 @@ function discardAndLeave() {
handle=".drag-handle"
v-bind="{ 'force-fallback': true }"
class="v-list"
:class="{ dragging: isDragging }"
@start="onDragStart"
@end="onDragEnd"
@update:model-value="$emit('input', $event)"
>
<template #item="{ element, index }">
<AccordionItem :value="index" as-child>
<v-list-item block grow class="list-item" clickable>
<v-list-item block grow class="list-item">
<div class="list-item-content">
<AccordionTrigger as-child>
<button
Expand Down Expand Up @@ -284,7 +330,7 @@ function discardAndLeave() {
:direction="direction"
primary-key="+"
@update:model-value="
(updatedElement) => {
(updatedElement: Record<string, unknown>) => {
const updatedValue = [...internalValue]
updatedValue[index] = updatedElement
emitValue(updatedValue)
Expand Down Expand Up @@ -365,8 +411,11 @@ function discardAndLeave() {
width: 100%;
margin-bottom: 8px;

&:focus-within:not(:has(.list-item-form:focus-within)) {
&:has(.list-item-header:focus-visible):not(:has(.clear-icon:focus-visible)) {
border-color: var(--v-input-border-color-focus, var(--theme--form--field--input--border-color-focus)) !important;
outline: var(--focus-ring-width) solid var(--focus-ring-color, var(--theme--primary)) !important;
outline-offset: var(--focus-ring-offset) !important;
border-radius: var(--focus-ring-radius) !important;
}
}

Expand All @@ -379,6 +428,12 @@ function discardAndLeave() {
border: none;
background: none;
padding: 0;
transition: opacity 0.2s ease;

&:focus-visible {
outline: none !important;
border-radius: 0 !important;
}
}

.list-item-header-controls {
Expand Down
2 changes: 1 addition & 1 deletion packages/inline-repeater-interface/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2019",
"lib": ["ES2019", "DOM"],
"lib": ["ES2023", "DOM"],
"rootDir": "./src",
"moduleResolution": "node",
"resolveJsonModule": false,
Expand Down
Loading
Loading