Skip to content

Commit 990e218

Browse files
committed
Upgrade
1 parent ffb8ca1 commit 990e218

File tree

8 files changed

+387
-423
lines changed

8 files changed

+387
-423
lines changed

resources/css/flowforge.css

Whitespace-only changes.

resources/dist/flowforge.js

Lines changed: 0 additions & 22 deletions
This file was deleted.

resources/js/flowforge.js

Lines changed: 351 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,364 @@
11
// Flowforge Kanban Board JavaScript
22

3+
/**
4+
* Alpine.js component for Kanban Board functionality
5+
*/
36
document.addEventListener('alpine:init', () => {
4-
Alpine.data('kanbanDragDrop', (columnId) => ({
5-
draggedItem: null,
6-
dropColumn: columnId,
7+
Alpine.store('kanbanFilters', {
8+
searchTerm: '',
9+
showColumn: {},
10+
initialize(columns) {
11+
columns.forEach(column => {
12+
this.showColumn[column] = true;
13+
});
14+
},
15+
isColumnVisible(column) {
16+
return this.showColumn[column] || false;
17+
},
18+
toggleColumn(column) {
19+
this.showColumn[column] = !this.showColumn[column];
20+
}
21+
});
22+
23+
Alpine.data('kanbanBoard', () => ({
24+
columns: {},
25+
columnCounts: {},
26+
draggedCardId: null,
27+
sourceColumn: null,
28+
loading: false,
29+
searchTerm: '',
30+
boardElement: null,
31+
32+
/**
33+
* Initialize the kanban board
34+
*/
35+
initialize() {
36+
this.boardElement = this.$el;
37+
this.initializeColumnData();
38+
this.setupEventListeners();
39+
},
40+
41+
/**
42+
* Initialize column data from DOM
43+
*/
44+
initializeColumnData() {
45+
const columnElements = this.boardElement.querySelectorAll('.flowforge-kanban-column');
46+
47+
// Extract column values
48+
const columnValues = Array.from(columnElements).map(el => el.dataset.columnValue);
49+
50+
// Initialize Alpine store
51+
Alpine.store('kanbanFilters').initialize(columnValues);
52+
53+
// Initialize column counts
54+
columnElements.forEach(column => {
55+
const columnValue = column.dataset.columnValue;
56+
const cardsCount = column.querySelectorAll('.flowforge-card').length;
57+
this.columnCounts[columnValue] = cardsCount;
58+
});
59+
},
60+
61+
/**
62+
* Setup event listeners for kanban board
63+
*/
64+
setupEventListeners() {
65+
// Livewire events for refreshing data
66+
Livewire.on('kanban-items-loaded', () => {
67+
this.loading = false;
68+
this.initializeColumnData();
69+
this.filterCards();
70+
});
71+
72+
Livewire.on('kanban-item-updated', ({ itemId, oldStatus, newStatus }) => {
73+
// Update column counts
74+
if (this.columnCounts[oldStatus]) {
75+
this.columnCounts[oldStatus]--;
76+
}
77+
78+
if (this.columnCounts[newStatus] !== undefined) {
79+
this.columnCounts[newStatus]++;
80+
}
81+
82+
// Flash animation on success
83+
this.flashSuccessAnimation(itemId);
84+
});
85+
86+
Livewire.on('kanban-count-updated', ({ oldColumn, newColumn, oldCount, newCount }) => {
87+
this.columnCounts[oldColumn] = oldCount;
88+
this.columnCounts[newColumn] = newCount;
89+
});
90+
91+
// Enhanced drag effect
92+
document.addEventListener('dragover', this.enhancedDragEffect.bind(this));
93+
},
94+
95+
/**
96+
* Enhanced drag visual effect
97+
* @param {DragEvent} event
98+
*/
99+
enhancedDragEffect(event) {
100+
if (!this.draggedCardId) return;
101+
102+
// Find all column elements
103+
const columns = document.querySelectorAll('.flowforge-kanban-column');
104+
105+
// Add/remove drag-over class for visual feedback
106+
columns.forEach(column => {
107+
const rect = column.getBoundingClientRect();
108+
const isOver = (
109+
event.clientX >= rect.left &&
110+
event.clientX <= rect.right &&
111+
event.clientY >= rect.top &&
112+
event.clientY <= rect.bottom
113+
);
114+
115+
column.classList.toggle('drag-over', isOver);
116+
});
117+
},
118+
119+
/**
120+
* Get column count
121+
* @param {string} columnValue
122+
* @returns {number}
123+
*/
124+
getColumnCount(columnValue) {
125+
return this.columnCounts[columnValue] || 0;
126+
},
7127

8-
handleDragStart(event, itemId) {
9-
event.dataTransfer.setData('text/plain', itemId);
128+
/**
129+
* Check if column is empty
130+
* @param {string} columnValue
131+
* @returns {boolean}
132+
*/
133+
isColumnEmpty(columnValue) {
134+
const columnElement = document.getElementById(`column-${columnValue}`);
135+
if (!columnElement) return true;
136+
137+
// Check visible cards
138+
const visibleCards = Array.from(columnElement.querySelectorAll('.flowforge-card'))
139+
.filter(card => !card.classList.contains('hidden'));
140+
141+
return visibleCards.length === 0;
142+
},
143+
144+
/**
145+
* Start dragging a card
146+
* @param {DragEvent} event
147+
* @param {string} cardId
148+
* @param {string} columnValue
149+
*/
150+
dragStart(event, cardId, columnValue) {
151+
this.draggedCardId = cardId;
152+
this.sourceColumn = columnValue;
153+
154+
// Add dragging class
155+
event.target.classList.add('dragging');
156+
157+
// Create custom drag image for better UX
158+
this.createDragImage(event);
159+
160+
// Set data transfer
10161
event.dataTransfer.effectAllowed = 'move';
11-
this.draggedItem = itemId;
162+
event.dataTransfer.setData('text/plain', cardId);
12163
},
13164

14-
handleDragOver(event) {
15-
event.preventDefault();
16-
event.dataTransfer.dropEffect = 'move';
165+
/**
166+
* Create custom drag image
167+
* @param {DragEvent} event
168+
*/
169+
createDragImage(event) {
170+
const card = event.target;
171+
const rect = card.getBoundingClientRect();
172+
173+
// Create a clone for drag image
174+
const clone = card.cloneNode(true);
175+
clone.style.width = `${rect.width}px`;
176+
clone.style.opacity = '0.8';
177+
clone.style.position = 'absolute';
178+
clone.style.top = '-1000px';
179+
clone.style.background = 'white';
180+
clone.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)';
181+
182+
document.body.appendChild(clone);
183+
184+
// Set drag image with offset
185+
event.dataTransfer.setDragImage(clone, rect.width / 2, 20);
186+
187+
// Clean up clone after dragging
188+
setTimeout(() => {
189+
document.body.removeChild(clone);
190+
}, 0);
17191
},
18192

19-
handleDrop(event, livewireComponent) {
193+
/**
194+
* End dragging a card
195+
* @param {DragEvent} event
196+
*/
197+
dragEnd(event) {
198+
// Remove dragging class
199+
event.target.classList.remove('dragging');
200+
201+
// Remove drag-over class from all columns
202+
document.querySelectorAll('.flowforge-kanban-column').forEach(column => {
203+
column.classList.remove('drag-over');
204+
});
205+
206+
// Clear drag data
207+
this.draggedCardId = null;
208+
this.sourceColumn = null;
209+
},
210+
211+
/**
212+
* Handle card drop
213+
* @param {DragEvent} event
214+
* @param {string} targetColumn
215+
*/
216+
dropCard(event, targetColumn) {
20217
event.preventDefault();
21-
const itemId = event.dataTransfer.getData('text/plain');
22-
livewireComponent.updateItemStatus(itemId, this.dropColumn);
218+
219+
// Get card ID from data transfer
220+
const cardId = event.dataTransfer.getData('text/plain') || this.draggedCardId;
221+
222+
if (!cardId || !this.sourceColumn) return;
223+
224+
// Don't do anything if dropping in the same column
225+
if (this.sourceColumn === targetColumn) return;
226+
227+
// Show loading state
228+
this.loading = true;
229+
230+
// Create visual placeholder for better UX
231+
this.createDropPlaceholder(event, targetColumn);
232+
233+
// Call Livewire method to update item status
234+
this.$wire.updateItemStatus(cardId, targetColumn);
235+
},
236+
237+
/**
238+
* Create visual placeholder when dropping
239+
* @param {DragEvent} event
240+
* @param {string} targetColumn
241+
*/
242+
createDropPlaceholder(event, targetColumn) {
243+
const columnElement = document.getElementById(`column-${targetColumn}`);
244+
if (!columnElement) return;
245+
246+
// Create placeholder element
247+
const placeholder = document.createElement('div');
248+
placeholder.className = 'flowforge-drop-placeholder';
249+
placeholder.setAttribute('data-placeholder-for', this.draggedCardId);
250+
251+
// Add placeholder to column
252+
columnElement.appendChild(placeholder);
253+
254+
// Remove placeholder after animation completes
255+
setTimeout(() => {
256+
if (placeholder.parentNode) {
257+
placeholder.parentNode.removeChild(placeholder);
258+
}
259+
}, 1500);
260+
},
261+
262+
/**
263+
* Add success animation to card
264+
* @param {string} cardId
265+
*/
266+
flashSuccessAnimation(cardId) {
267+
setTimeout(() => {
268+
const card = document.getElementById(`card-${cardId}`);
269+
if (!card) return;
270+
271+
card.classList.add('flash-success');
272+
273+
setTimeout(() => {
274+
card.classList.remove('flash-success');
275+
}, 1000);
276+
}, 100);
277+
},
278+
279+
/**
280+
* Filter cards based on search term
281+
*/
282+
filterCards() {
283+
const searchTerm = this.searchTerm.toLowerCase().trim();
284+
285+
// Get all cards
286+
const cards = document.querySelectorAll('.flowforge-card');
287+
288+
cards.forEach(card => {
289+
const searchContent = card.dataset.searchContent || '';
290+
const isVisible = !searchTerm || searchContent.toLowerCase().includes(searchTerm);
291+
292+
// Toggle visibility
293+
card.classList.toggle('hidden', !isVisible);
294+
});
295+
296+
// Update empty states
297+
this.updateEmptyStates();
298+
},
299+
300+
/**
301+
* Update empty state visibility
302+
*/
303+
updateEmptyStates() {
304+
document.querySelectorAll('.flowforge-kanban-column').forEach(column => {
305+
const columnValue = column.dataset.columnValue;
306+
const empty = this.isColumnEmpty(columnValue);
307+
308+
const emptyState = column.querySelector('.flowforge-empty-state');
309+
if (emptyState) {
310+
emptyState.style.display = empty ? 'flex' : 'none';
311+
}
312+
});
313+
},
314+
315+
/**
316+
* Refresh the board
317+
*/
318+
refreshBoard() {
319+
// Show loading state
320+
this.loading = true;
321+
322+
// Add refreshing animation to button
323+
const refreshBtn = this.boardElement.querySelector('.flowforge-refresh-btn');
324+
if (refreshBtn) {
325+
refreshBtn.classList.add('refreshing');
326+
327+
setTimeout(() => {
328+
refreshBtn.classList.remove('refreshing');
329+
}, 2000);
330+
}
331+
332+
// Call Livewire refresh method
333+
this.$wire.loadItems();
23334
}
24335
}));
25336
});
337+
338+
/**
339+
* Add keyboard navigation for improved accessibility
340+
*/
341+
document.addEventListener('DOMContentLoaded', () => {
342+
// Allow keyboard navigation for cards
343+
document.addEventListener('keydown', event => {
344+
if (event.key === 'Enter' || event.key === ' ') {
345+
const focused = document.activeElement;
346+
if (focused && focused.classList.contains('flowforge-card')) {
347+
// Toggle card selection
348+
focused.classList.toggle('selected');
349+
}
350+
}
351+
});
352+
});
353+
354+
// Add CSS classes for different priority levels
355+
document.addEventListener('kanban-items-loaded', () => {
356+
// Example: Add priority classes based on some attribute
357+
document.querySelectorAll('.flowforge-card').forEach(card => {
358+
const priorityElement = card.querySelector('[data-priority]');
359+
if (priorityElement) {
360+
const priority = priorityElement.dataset.priority;
361+
card.classList.add(`priority-${priority}`);
362+
}
363+
});
364+
});

0 commit comments

Comments
 (0)