@@ -15,13 +15,16 @@ function selectKanbanDomElements() {
1515 addColumnBtn = document . getElementById ( 'addColumnBtn' ) ;
1616}
1717
18- function escapeHtml ( unsafe ) {
19- return unsafe
20- . replace ( / & / g, "&" )
21- . replace ( / < / g, "<" )
22- . replace ( / > / g, ">" )
23- . replace ( / " / g, """ )
24- . replace ( / ' / g, "'" ) ;
18+ // Helper for generating UUIDs
19+ function generateUUID ( ) {
20+ if ( typeof crypto !== 'undefined' && crypto . randomUUID ) {
21+ return crypto . randomUUID ( ) ;
22+ }
23+ // Fallback for older environments
24+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' . replace ( / [ x y ] / g, function ( c ) {
25+ var r = Math . random ( ) * 16 | 0 , v = c == 'x' ? r : ( r & 0x3 | 0x8 ) ;
26+ return v . toString ( 16 ) ;
27+ } ) ;
2528}
2629
2730export function initKanbanFeatures ( dependencies ) {
@@ -52,6 +55,7 @@ export function initKanbanFeatures(dependencies) {
5255 } ;
5356}
5457
58+ // SECURITY NOTE: using document.createElement prevents XSS
5559export function renderKanbanBoard ( ) {
5660 if ( ! kanbanBoard ) return ;
5761 kanbanBoard . innerHTML = '' ;
@@ -61,72 +65,126 @@ export function renderKanbanBoard() {
6165 const columnDiv = document . createElement ( 'div' ) ;
6266 columnDiv . classList . add ( 'kanban-column' ) ;
6367 columnDiv . dataset . columnId = column . id ;
68+
69+ const header = document . createElement ( 'h3' ) ;
70+ header . textContent = column . title ;
71+
72+ const deleteColBtn = document . createElement ( 'button' ) ;
73+ deleteColBtn . classList . add ( 'delete-column-btn' ) ;
74+ deleteColBtn . dataset . columnId = column . id ;
75+ deleteColBtn . title = "Delete column" ;
76+ deleteColBtn . textContent = "🗑️" ;
77+ deleteColBtn . addEventListener ( 'click' , ( ) => handleDeleteKanbanColumn ( column . id ) ) ;
78+ header . appendChild ( deleteColBtn ) ;
6479
65- let cardsHtml = ( column . cards || [ ] ) . map ( card => {
80+ columnDiv . appendChild ( header ) ;
81+
82+ const cardsContainer = document . createElement ( 'div' ) ;
83+ cardsContainer . classList . add ( 'kanban-cards' ) ;
84+
85+ ( column . cards || [ ] ) . forEach ( card => {
6686 const cardPriority = card . priority || 1 ;
67- const escapedText = escapeHtml ( card . text ) ;
68- return `
69- <div class="kanban-card priority-${ cardPriority } " draggable="true" data-card-id="${ card . id } " data-parent-column-id="${ column . id } " data-priority="${ cardPriority } ">
70- <div class="kanban-card-content">
71- <p>${ escapedText } </p>
72- <select class="kanban-card-priority" data-card-id="${ card . id } " data-column-id="${ column . id } ">
73- <option value="1" ${ cardPriority == 1 ? 'selected' : '' } >Low</option>
74- <option value="2" ${ cardPriority == 2 ? 'selected' : '' } >Medium</option>
75- <option value="3" ${ cardPriority == 3 ? 'selected' : '' } >High</option>
76- <option value="4" ${ cardPriority == 4 ? 'selected' : '' } >Critical</option>
77- </select>
78- </div>
79- <button class="delete-card-btn" data-card-id="${ card . id } " data-column-id="${ column . id } " title="Delete card">❌</button>
80- </div>` ;
81- } ) . join ( '' ) ;
82-
83- let addCardSectionHtml = '' ;
87+
88+ const cardDiv = document . createElement ( 'div' ) ;
89+ cardDiv . classList . add ( 'kanban-card' , `priority-${ cardPriority } ` ) ;
90+ cardDiv . draggable = true ;
91+ cardDiv . dataset . cardId = card . id ;
92+ cardDiv . dataset . parentColumnId = column . id ;
93+ cardDiv . dataset . priority = cardPriority ;
94+
95+ const contentDiv = document . createElement ( 'div' ) ;
96+ contentDiv . classList . add ( 'kanban-card-content' ) ;
97+
98+ const p = document . createElement ( 'p' ) ;
99+ p . textContent = card . text ;
100+ contentDiv . appendChild ( p ) ;
101+
102+ const prioritySelect = document . createElement ( 'select' ) ;
103+ prioritySelect . classList . add ( 'kanban-card-priority' ) ;
104+ prioritySelect . dataset . cardId = card . id ;
105+ prioritySelect . dataset . columnId = column . id ;
106+
107+ const priorities = [
108+ { val : 1 , label : 'Low' } ,
109+ { val : 2 , label : 'Medium' } ,
110+ { val : 3 , label : 'High' } ,
111+ { val : 4 , label : 'Critical' }
112+ ] ;
113+
114+ priorities . forEach ( prio => {
115+ const option = document . createElement ( 'option' ) ;
116+ option . value = prio . val ;
117+ option . textContent = prio . label ;
118+ if ( cardPriority == prio . val ) option . selected = true ;
119+ prioritySelect . appendChild ( option ) ;
120+ } ) ;
121+
122+ prioritySelect . addEventListener ( 'change' , ( e ) => handleUpdateCardPriority ( column . id , card . id , e . target . value ) ) ;
123+ contentDiv . appendChild ( prioritySelect ) ;
124+ cardDiv . appendChild ( contentDiv ) ;
125+
126+ const deleteCardBtn = document . createElement ( 'button' ) ;
127+ deleteCardBtn . classList . add ( 'delete-card-btn' ) ;
128+ deleteCardBtn . dataset . cardId = card . id ;
129+ deleteCardBtn . dataset . columnId = column . id ;
130+ deleteCardBtn . title = "Delete card" ;
131+ deleteCardBtn . textContent = "❌" ;
132+ deleteCardBtn . addEventListener ( 'click' , ( ) => handleDeleteKanbanCard ( column . id , card . id ) ) ;
133+ cardDiv . appendChild ( deleteCardBtn ) ;
134+
135+ cardDiv . addEventListener ( 'dragstart' , handleKanbanDragStart ) ;
136+ cardDiv . addEventListener ( 'dragend' , handleKanbanDragEnd ) ;
137+
138+ cardsContainer . appendChild ( cardDiv ) ;
139+ } ) ;
140+
141+ columnDiv . appendChild ( cardsContainer ) ;
142+
84143 if ( column . id === kanbanCurrentlyAddingCardToColumnId ) {
85- addCardSectionHtml = `
86- <div class="add-card-form">
87- <textarea class="new-card-text-input" placeholder="Enter card text..."></textarea>
88- <div class="add-card-form-actions">
89- <button class="save-new-card-btn" data-column-id="${ column . id } ">Save Card</button>
90- <button class="cancel-add-card-btn" data-column-id="${ column . id } ">Cancel</button>
91- </div>
92- </div>` ;
144+ const formDiv = document . createElement ( 'div' ) ;
145+ formDiv . classList . add ( 'add-card-form' ) ;
146+
147+ const textarea = document . createElement ( 'textarea' ) ;
148+ textarea . classList . add ( 'new-card-text-input' ) ;
149+ textarea . placeholder = "Enter card text..." ;
150+ formDiv . appendChild ( textarea ) ;
151+
152+ const actionsDiv = document . createElement ( 'div' ) ;
153+ actionsDiv . classList . add ( 'add-card-form-actions' ) ;
154+
155+ const saveBtn = document . createElement ( 'button' ) ;
156+ saveBtn . classList . add ( 'save-new-card-btn' ) ;
157+ saveBtn . dataset . columnId = column . id ;
158+ saveBtn . textContent = "Save Card" ;
159+ saveBtn . addEventListener ( 'click' , ( ) => handleSaveNewCard ( column . id ) ) ;
160+
161+ const cancelBtn = document . createElement ( 'button' ) ;
162+ cancelBtn . classList . add ( 'cancel-add-card-btn' ) ;
163+ cancelBtn . dataset . columnId = column . id ;
164+ cancelBtn . textContent = "Cancel" ;
165+ cancelBtn . addEventListener ( 'click' , handleCancelAddCard ) ;
166+
167+ actionsDiv . appendChild ( saveBtn ) ;
168+ actionsDiv . appendChild ( cancelBtn ) ;
169+ formDiv . appendChild ( actionsDiv ) ;
170+ columnDiv . appendChild ( formDiv ) ;
171+
172+ setTimeout ( ( ) => textarea . focus ( ) , 0 ) ;
93173 } else {
94- addCardSectionHtml = `<button class="add-card-btn" data-column-id="${ column . id } ">+ Add Card</button>` ;
174+ const addCardBtn = document . createElement ( 'button' ) ;
175+ addCardBtn . classList . add ( 'add-card-btn' ) ;
176+ addCardBtn . dataset . columnId = column . id ;
177+ addCardBtn . textContent = "+ Add Card" ;
178+ addCardBtn . addEventListener ( 'click' , ( ) => handleShowAddCardForm ( column . id ) ) ;
179+ columnDiv . appendChild ( addCardBtn ) ;
95180 }
96181
97- columnDiv . innerHTML = `
98- <h3>${ escapeHtml ( column . title ) } <button class="delete-column-btn" data-column-id="${ column . id } " title="Delete column">🗑️</button></h3>
99- <div class="kanban-cards">${ cardsHtml } </div>
100- ${ addCardSectionHtml } ` ;
101- kanbanBoard . appendChild ( columnDiv ) ;
102- } ) ;
182+ columnDiv . addEventListener ( 'dragover' , handleKanbanDragOver ) ;
183+ columnDiv . addEventListener ( 'dragleave' , handleKanbanDragLeave ) ;
184+ columnDiv . addEventListener ( 'drop' , handleKanbanDrop ) ;
103185
104- // Event Listeners
105- kanbanBoard . querySelectorAll ( '.add-card-btn' ) . forEach ( btn => btn . addEventListener ( 'click' , ( ) => handleShowAddCardForm ( btn . dataset . columnId ) ) ) ;
106- kanbanBoard . querySelectorAll ( '.save-new-card-btn' ) . forEach ( btn => btn . addEventListener ( 'click' , ( ) => handleSaveNewCard ( btn . dataset . columnId ) ) ) ;
107- kanbanBoard . querySelectorAll ( '.cancel-add-card-btn' ) . forEach ( btn => btn . addEventListener ( 'click' , ( ) => handleCancelAddCard ( ) ) ) ;
108-
109- kanbanBoard . querySelectorAll ( '.delete-column-btn' ) . forEach ( btn => btn . addEventListener ( 'click' , ( ) => handleDeleteKanbanColumn ( btn . dataset . columnId ) ) ) ;
110- kanbanBoard . querySelectorAll ( '.delete-card-btn' ) . forEach ( btn => btn . addEventListener ( 'click' , ( ) => handleDeleteKanbanCard ( btn . dataset . columnId , btn . dataset . cardId ) ) ) ;
111-
112- kanbanBoard . querySelectorAll ( '.kanban-card-priority' ) . forEach ( selectEl => {
113- selectEl . addEventListener ( 'change' , ( e ) => handleUpdateCardPriority ( e . target . dataset . columnId , e . target . dataset . cardId , e . target . value ) ) ;
114- } ) ;
115-
116- kanbanBoard . querySelectorAll ( '.kanban-card' ) . forEach ( card => {
117- card . addEventListener ( 'dragstart' , handleKanbanDragStart ) ;
118- card . addEventListener ( 'dragend' , handleKanbanDragEnd ) ;
119- } ) ;
120- kanbanBoard . querySelectorAll ( '.kanban-column' ) . forEach ( col => {
121- col . addEventListener ( 'dragover' , handleKanbanDragOver ) ;
122- col . addEventListener ( 'dragleave' , handleKanbanDragLeave ) ;
123- col . addEventListener ( 'drop' , handleKanbanDrop ) ;
186+ kanbanBoard . appendChild ( columnDiv ) ;
124187 } ) ;
125-
126- if ( kanbanCurrentlyAddingCardToColumnId ) {
127- const activeFormTextarea = kanbanBoard . querySelector ( `.kanban-column[data-column-id="${ kanbanCurrentlyAddingCardToColumnId } "] .new-card-text-input` ) ;
128- if ( activeFormTextarea ) activeFormTextarea . focus ( ) ;
129- }
130188}
131189
132190export function renderKanbanBoardIfActive ( force = false ) {
@@ -139,21 +197,24 @@ export function renderKanbanBoardIfActive(force = false) {
139197function handleKanbanDragStart ( e ) {
140198 draggedCardElement = e . target ;
141199 const cardPriority = e . target . dataset . priority || 1 ;
200+ const textP = e . target . querySelector ( '.kanban-card-content p' ) ;
142201 draggedCardData = {
143202 id : e . target . dataset . cardId ,
144203 originalColumnId : e . target . dataset . parentColumnId ,
145- text : e . target . querySelector ( '.kanban-card-content p' ) ? e . target . querySelector ( '.kanban-card-content p' ) . textContent : '' ,
204+ text : textP ? textP . textContent : '' ,
146205 priority : parseInt ( cardPriority )
147206 } ;
148207 e . dataTransfer . setData ( 'text/plain' , e . target . dataset . cardId ) ;
149208 e . dataTransfer . effectAllowed = 'move' ;
150209 setTimeout ( ( ) => { if ( draggedCardElement ) draggedCardElement . classList . add ( 'dragging' ) ; } , 0 ) ;
151210}
211+
152212function handleKanbanDragEnd ( e ) {
153213 if ( draggedCardElement ) draggedCardElement . classList . remove ( 'dragging' ) ;
154214 draggedCardElement = null ; draggedCardData = null ;
155215 if ( kanbanBoard ) kanbanBoard . querySelectorAll ( '.kanban-column.drag-over' ) . forEach ( col => col . classList . remove ( 'drag-over' ) ) ;
156216}
217+
157218function handleKanbanDragOver ( e ) {
158219 e . preventDefault ( ) ; e . dataTransfer . dropEffect = 'move' ;
159220 const column = e . target . closest ( '.kanban-column' ) ;
@@ -162,10 +223,12 @@ function handleKanbanDragOver(e) {
162223 column . classList . add ( 'drag-over' ) ;
163224 }
164225}
226+
165227function handleKanbanDragLeave ( e ) {
166228 const column = e . target . closest ( '.kanban-column' ) ;
167229 if ( column && ! column . contains ( e . relatedTarget ) ) column . classList . remove ( 'drag-over' ) ;
168230}
231+
169232function handleKanbanDrop ( e ) {
170233 e . preventDefault ( ) ; if ( ! draggedCardData ) return ;
171234 const targetColumnDiv = e . target . closest ( '.kanban-column' ) ;
@@ -196,7 +259,8 @@ function handleKanbanDrop(e) {
196259function handleAddKanbanColumn ( ) {
197260 if ( ! newColumnNameInput ) return ;
198261 const columnName = newColumnNameInput . value . trim ( ) ; if ( ! columnName ) return ;
199- const newColumn = { id : `col-${ Date . now ( ) } ` , title : columnName , cards : [ ] } ;
262+ // UUID for Column
263+ const newColumn = { id : generateUUID ( ) , title : columnName , cards : [ ] } ;
200264 if ( ! kanbanData . columns ) kanbanData . columns = [ ] ;
201265 kanbanData . columns . push ( newColumn ) ;
202266 if ( sendKanbanUpdateDep ) sendKanbanUpdateDep ( { type : 'addColumn' , column : newColumn } ) ;
@@ -225,8 +289,9 @@ function handleSaveNewCard(columnId) {
225289
226290 const column = kanbanData . columns . find ( col => col . id === columnId ) ;
227291 if ( column ) {
292+ // UUID for Card
228293 const newCard = {
229- id : `card- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 7 ) } ` ,
294+ id : generateUUID ( ) ,
230295 text : cardText ,
231296 priority : 1
232297 } ;
@@ -267,6 +332,7 @@ function handleDeleteKanbanColumn(columnId) {
267332 renderKanbanBoard ( ) ;
268333 if ( getPeerNicknamesDep && Object . keys ( getPeerNicknamesDep ( ) ) . length > 0 && showNotificationDep ) showNotificationDep ( 'kanbanSection' ) ;
269334}
335+
270336function handleDeleteKanbanCard ( columnId , cardId ) {
271337 if ( ! confirm ( "Delete card?" ) ) return ;
272338 const column = kanbanData . columns . find ( col => col . id === columnId ) ;
0 commit comments