Skip to content

Commit ff5197e

Browse files
added tile screen share/ UUID
1 parent bda7475 commit ff5197e

4 files changed

Lines changed: 259 additions & 181 deletions

File tree

src/document.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ let getPeerNicknamesDep, localGeneratedPeerIdDep, getIsHostDep;
1010
let documentsSection, documentListDiv, newDocBtn, renameDocBtn, deleteDocBtn, collaborativeEditor;
1111
let docBoldBtn, docItalicBtn, docUnderlineBtn, docUlBtn, docOlBtn, downloadTxtBtn, printDocBtn;
1212

13-
1413
function selectDocumentDomElements() {
1514
documentsSection = document.getElementById('documentsSection');
1615
documentListDiv = document.getElementById('documentList');
@@ -27,6 +26,16 @@ function selectDocumentDomElements() {
2726
printDocBtn = document.getElementById('printDocBtn');
2827
}
2928

29+
function generateUUID() {
30+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
31+
return crypto.randomUUID();
32+
}
33+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
34+
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
35+
return v.toString(16);
36+
});
37+
}
38+
3039
function escapeHtml(unsafe) {
3140
return unsafe
3241
.replace(/&/g, "&")
@@ -74,11 +83,15 @@ function printCurrentDocument() {
7483
const printDocument = printFrame.contentDocument || printFrame.contentWindow.document;
7584

7685
printDocument.open();
86+
// SECURITY: Sanitized HTML content before outputting to print frame
87+
const sanitizedName = escapeHtml(activeDoc.name);
88+
const sanitizedContent = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(activeDoc.htmlContent) : activeDoc.htmlContent;
89+
7790
printDocument.write(`
7891
<!DOCTYPE html>
7992
<html>
8093
<head>
81-
<title>${escapeHtml(activeDoc.name)}</title>
94+
<title>${sanitizedName}</title>
8295
<style>
8396
@page { margin: 0.5in; }
8497
body {
@@ -91,8 +104,8 @@ function printCurrentDocument() {
91104
</style>
92105
</head>
93106
<body>
94-
<h1>${escapeHtml(activeDoc.name)}</h1>
95-
${activeDoc.htmlContent}
107+
<h1>${sanitizedName}</h1>
108+
${sanitizedContent}
96109
</body>
97110
</html>
98111
`);
@@ -174,9 +187,13 @@ export function renderDocumentsIfActive(force = false) {
174187
function renderDocumentList() {
175188
if (!documentListDiv) return;
176189
documentListDiv.innerHTML = '';
190+
191+
// Auto-create document if host and list is empty
177192
if (documents.length === 0 && getIsHostDep && getIsHostDep() && sendCreateDocumentDep) {
178-
193+
ensureDefaultDocument();
194+
return;
179195
}
196+
180197
documents.forEach(doc => {
181198
const docItem = document.createElement('span');
182199
docItem.classList.add('document-list-item'); docItem.textContent = doc.name; docItem.dataset.documentId = doc.id;
@@ -221,7 +238,8 @@ function setActiveDocument(documentId) {
221238
function uiActionCreateNewDocument(defaultName = null, defaultContent = null, broadcast = true) {
222239
const docName = defaultName || prompt("Enter name for the new document:", `Document ${documents.length + 1}`);
223240
if (!docName) return;
224-
const newDoc = { id: `doc-${Date.now()}-${Math.random().toString(36).substring(2,7)}`, name: docName, htmlContent: defaultContent || '<p>Start typing...</p>' };
241+
// UUID for Document
242+
const newDoc = { id: generateUUID(), name: docName, htmlContent: defaultContent || '<p>Start typing...</p>' };
225243
documents.push(newDoc);
226244
setActiveDocument(newDoc.id);
227245
if (broadcast && sendCreateDocumentDep) {

src/kanban.js

Lines changed: 134 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ function selectKanbanDomElements() {
1515
addColumnBtn = document.getElementById('addColumnBtn');
1616
}
1717

18-
function escapeHtml(unsafe) {
19-
return unsafe
20-
.replace(/&/g, "&amp;")
21-
.replace(/</g, "&lt;")
22-
.replace(/>/g, "&gt;")
23-
.replace(/"/g, "&quot;")
24-
.replace(/'/g, "&#039;");
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(/[xy]/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

2730
export function initKanbanFeatures(dependencies) {
@@ -52,6 +55,7 @@ export function initKanbanFeatures(dependencies) {
5255
};
5356
}
5457

58+
// SECURITY NOTE: using document.createElement prevents XSS
5559
export 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

132190
export function renderKanbanBoardIfActive(force = false) {
@@ -139,21 +197,24 @@ export function renderKanbanBoardIfActive(force = false) {
139197
function 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+
152212
function 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+
157218
function 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+
165227
function handleKanbanDragLeave(e) {
166228
const column = e.target.closest('.kanban-column');
167229
if (column && !column.contains(e.relatedTarget)) column.classList.remove('drag-over');
168230
}
231+
169232
function handleKanbanDrop(e) {
170233
e.preventDefault(); if (!draggedCardData) return;
171234
const targetColumnDiv = e.target.closest('.kanban-column');
@@ -196,7 +259,8 @@ function handleKanbanDrop(e) {
196259
function 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+
270336
function handleDeleteKanbanCard(columnId, cardId) {
271337
if (!confirm("Delete card?")) return;
272338
const column = kanbanData.columns.find(col => col.id === columnId);

0 commit comments

Comments
 (0)