Merge pull request #152 from gamosoft/bugfix/apostrophes
Bugfix/apostrophes
This commit is contained in:
commit
3481b95b03
139
frontend/app.js
139
frontend/app.js
|
|
@ -1722,28 +1722,108 @@ function noteApp() {
|
||||||
this.folderTree = tree;
|
this.folderTree = tree;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// DATA-ATTRIBUTE BASED HANDLERS
|
||||||
|
// These read path/name/type from data-* attributes, avoiding JS escaping issues
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
// Escape strings for HTML attributes (simpler than JS escaping)
|
||||||
|
escapeHtmlAttr(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Folder handlers - read from dataset
|
||||||
|
handleFolderClick(el) {
|
||||||
|
this.toggleFolder(el.dataset.path);
|
||||||
|
},
|
||||||
|
handleFolderDragStart(el, event) {
|
||||||
|
this.onFolderDragStart(el.dataset.path, event);
|
||||||
|
},
|
||||||
|
handleFolderDragOver(el, event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.dragOverFolder = el.dataset.path;
|
||||||
|
el.classList.add('drag-over');
|
||||||
|
},
|
||||||
|
handleFolderDragLeave(el) {
|
||||||
|
this.dragOverFolder = null;
|
||||||
|
el.classList.remove('drag-over');
|
||||||
|
},
|
||||||
|
handleFolderDrop(el, event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
el.classList.remove('drag-over');
|
||||||
|
this.onFolderDrop(el.dataset.path);
|
||||||
|
},
|
||||||
|
handleNewItemClick(el, event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.dropdownTargetFolder = el.dataset.path;
|
||||||
|
this.toggleNewDropdown(event);
|
||||||
|
},
|
||||||
|
handleRenameFolderClick(el, event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.renameFolder(el.dataset.path, el.dataset.name);
|
||||||
|
},
|
||||||
|
handleDeleteFolderClick(el, event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.deleteFolder(el.dataset.path, el.dataset.name);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Item (note/image) handlers - read from dataset
|
||||||
|
handleItemClick(el) {
|
||||||
|
this.openItem(el.dataset.path, el.dataset.type);
|
||||||
|
},
|
||||||
|
handleItemDragStart(el, event) {
|
||||||
|
this.onNoteDragStart(el.dataset.path, event);
|
||||||
|
},
|
||||||
|
handleItemHover(el, isEnter) {
|
||||||
|
const path = el.dataset.path;
|
||||||
|
if (path !== this.currentNote && path !== this.currentImage) {
|
||||||
|
el.style.backgroundColor = isEnter ? 'var(--bg-hover)' : 'transparent';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleDeleteItemClick(el, event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (el.dataset.type === 'image') {
|
||||||
|
this.deleteImage(el.dataset.path);
|
||||||
|
} else {
|
||||||
|
this.deleteNote(el.dataset.path, el.dataset.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// FOLDER TREE RENDERING
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
// Render folder recursively (helper for deep nesting)
|
// Render folder recursively (helper for deep nesting)
|
||||||
|
// Uses data-* attributes to store path/name, avoiding JS string escaping issues
|
||||||
renderFolderRecursive(folder, level = 0, isTopLevel = false) {
|
renderFolderRecursive(folder, level = 0, isTopLevel = false) {
|
||||||
if (!folder) return '';
|
if (!folder) return '';
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
const isExpanded = this.expandedFolders.has(folder.path);
|
const isExpanded = this.expandedFolders.has(folder.path);
|
||||||
|
const esc = (s) => this.escapeHtmlAttr(s); // Shorthand for HTML escaping
|
||||||
|
|
||||||
// Render this folder's header
|
// Render this folder's header
|
||||||
// Note: Using native event handlers (ondragstart, onclick, etc.) instead of Alpine directives
|
// Note: Using native event handlers with data-* attributes instead of Alpine directives
|
||||||
// because x-html doesn't process Alpine directives in dynamically generated content
|
// because x-html doesn't process Alpine directives in dynamically generated content
|
||||||
const escapedPath = folder.path.replace(/'/g, "\\'").replace(/\\/g, "\\\\");
|
|
||||||
html += `
|
html += `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
data-path="${esc(folder.path)}"
|
||||||
|
data-name="${esc(folder.name)}"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
ondragstart="window.$root.onFolderDragStart('${escapedPath}', event)"
|
ondragstart="window.$root.handleFolderDragStart(this, event)"
|
||||||
ondragend="window.$root.onFolderDragEnd()"
|
ondragend="window.$root.onFolderDragEnd()"
|
||||||
ondragover="event.preventDefault(); window.$root.dragOverFolder = '${escapedPath}'; this.classList.add('drag-over')"
|
ondragover="window.$root.handleFolderDragOver(this, event)"
|
||||||
ondragenter="event.preventDefault(); window.$root.dragOverFolder = '${escapedPath}'; this.classList.add('drag-over')"
|
ondragenter="window.$root.handleFolderDragOver(this, event)"
|
||||||
ondragleave="window.$root.dragOverFolder = null; this.classList.remove('drag-over')"
|
ondragleave="window.$root.handleFolderDragLeave(this)"
|
||||||
ondrop="event.stopPropagation(); this.classList.remove('drag-over'); window.$root.onFolderDrop('${escapedPath}')"
|
ondrop="window.$root.handleFolderDrop(this, event)"
|
||||||
onclick="window.$root.toggleFolder('${escapedPath}')"
|
onclick="window.$root.handleFolderClick(this)"
|
||||||
onmouseover="if(!window.$root.draggedNote && !window.$root.draggedFolder) this.style.backgroundColor='var(--bg-hover)'"
|
onmouseover="if(!window.$root.draggedNote && !window.$root.draggedFolder) this.style.backgroundColor='var(--bg-hover)'"
|
||||||
onmouseout="if(!window.$root.draggedNote && !window.$root.draggedFolder) this.style.backgroundColor='transparent'"
|
onmouseout="if(!window.$root.draggedNote && !window.$root.draggedFolder) this.style.backgroundColor='transparent'"
|
||||||
class="folder-item px-2 py-1 text-sm relative"
|
class="folder-item px-2 py-1 text-sm relative"
|
||||||
|
|
@ -1759,25 +1839,30 @@ function noteApp() {
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="flex items-center gap-1 flex-1" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; pointer-events: none;">
|
<span class="flex items-center gap-1 flex-1" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; pointer-events: none;">
|
||||||
<span>${folder.name}</span>
|
<span>${esc(folder.name)}</span>
|
||||||
${folder.notes.length === 0 && (!folder.children || Object.keys(folder.children).length === 0) ? `<span class="text-xs" style="color: var(--text-tertiary); font-weight: 400;">(${this.t('folders.empty')})</span>` : ''}
|
${folder.notes.length === 0 && (!folder.children || Object.keys(folder.children).length === 0) ? `<span class="text-xs" style="color: var(--text-tertiary); font-weight: 400;">(${this.t('folders.empty')})</span>` : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hover-buttons flex gap-1 transition-opacity absolute right-2 top-1/2 transform -translate-y-1/2" style="opacity: 0; pointer-events: none; background: linear-gradient(to right, transparent, var(--bg-hover) 20%, var(--bg-hover)); padding-left: 20px;" onclick="event.stopPropagation()">
|
<div class="hover-buttons flex gap-1 transition-opacity absolute right-2 top-1/2 transform -translate-y-1/2" style="opacity: 0; pointer-events: none; background: linear-gradient(to right, transparent, var(--bg-hover) 20%, var(--bg-hover)); padding-left: 20px;" onclick="event.stopPropagation()">
|
||||||
<button
|
<button
|
||||||
onclick="event.stopPropagation(); window.$root.dropdownTargetFolder = '${escapedPath}'; window.$root.toggleNewDropdown(event)"
|
data-path="${esc(folder.path)}"
|
||||||
|
onclick="window.$root.handleNewItemClick(this, event)"
|
||||||
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
|
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
|
||||||
style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
|
style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
|
||||||
title="Add item here"
|
title="Add item here"
|
||||||
>+</button>
|
>+</button>
|
||||||
<button
|
<button
|
||||||
onclick="event.stopPropagation(); window.$root.renameFolder('${escapedPath}', '${folder.name.replace(/'/g, "\\'").replace(/\\/g, "\\\\")}')"
|
data-path="${esc(folder.path)}"
|
||||||
|
data-name="${esc(folder.name)}"
|
||||||
|
onclick="window.$root.handleRenameFolderClick(this, event)"
|
||||||
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
|
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
|
||||||
style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
|
style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
|
||||||
title="Rename folder"
|
title="Rename folder"
|
||||||
>✏️</button>
|
>✏️</button>
|
||||||
<button
|
<button
|
||||||
onclick="event.stopPropagation(); window.$root.deleteFolder('${escapedPath}', '${folder.name.replace(/'/g, "\\'").replace(/\\/g, "\\\\")}')"
|
data-path="${esc(folder.path)}"
|
||||||
|
data-name="${esc(folder.name)}"
|
||||||
|
onclick="window.$root.handleDeleteFolderClick(this, event)"
|
||||||
class="px-1 py-0.5 text-xs rounded hover:brightness-110"
|
class="px-1 py-0.5 text-xs rounded hover:brightness-110"
|
||||||
style="color: var(--error);"
|
style="color: var(--error);"
|
||||||
title="Delete folder"
|
title="Delete folder"
|
||||||
|
|
@ -1819,32 +1904,26 @@ function noteApp() {
|
||||||
const shareIcon = isShared ? '<svg title="Shared" style="display: inline-block; width: 12px; height: 12px; vertical-align: middle; margin-right: 2px; opacity: 0.7;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>' : '';
|
const shareIcon = isShared ? '<svg title="Shared" style="display: inline-block; width: 12px; height: 12px; vertical-align: middle; margin-right: 2px; opacity: 0.7;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>' : '';
|
||||||
const icon = isImage ? '🖼️' : '';
|
const icon = isImage ? '🖼️' : '';
|
||||||
|
|
||||||
// Escape paths for use in native event handlers
|
|
||||||
const escapedNotePath = note.path.replace(/'/g, "\\'").replace(/\\/g, "\\\\");
|
|
||||||
const escapedNoteName = note.name.replace(/'/g, "\\'").replace(/\\/g, "\\\\");
|
|
||||||
|
|
||||||
// Click handler
|
|
||||||
const clickHandler = `window.$root.openItem('${escapedNotePath}', '${note.type}')`;
|
|
||||||
|
|
||||||
// Delete handler
|
|
||||||
const deleteHandler = isImage
|
|
||||||
? `event.stopPropagation(); window.$root.deleteImage('${escapedNotePath}')`
|
|
||||||
: `event.stopPropagation(); window.$root.deleteNote('${escapedNotePath}', '${escapedNoteName}')`;
|
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div
|
<div
|
||||||
|
data-path="${esc(note.path)}"
|
||||||
|
data-name="${esc(note.name)}"
|
||||||
|
data-type="${note.type}"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
ondragstart="window.$root.onNoteDragStart('${escapedNotePath}', event)"
|
ondragstart="window.$root.handleItemDragStart(this, event)"
|
||||||
ondragend="window.$root.onNoteDragEnd()"
|
ondragend="window.$root.onNoteDragEnd()"
|
||||||
onclick="${clickHandler}"
|
onclick="window.$root.handleItemClick(this)"
|
||||||
class="note-item px-2 py-1 text-sm relative"
|
class="note-item px-2 py-1 text-sm relative"
|
||||||
style="${isCurrent ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'} ${isImage ? 'opacity: 0.85;' : ''} cursor: pointer;"
|
style="${isCurrent ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'} ${isImage ? 'opacity: 0.85;' : ''} cursor: pointer;"
|
||||||
onmouseover="if('${escapedNotePath}' !== window.$root.currentNote && '${escapedNotePath}' !== window.$root.currentImage) this.style.backgroundColor='var(--bg-hover)'"
|
onmouseover="window.$root.handleItemHover(this, true)"
|
||||||
onmouseout="if('${escapedNotePath}' !== window.$root.currentNote && '${escapedNotePath}' !== window.$root.currentImage) this.style.backgroundColor='transparent'"
|
onmouseout="window.$root.handleItemHover(this, false)"
|
||||||
>
|
>
|
||||||
<span class="truncate" style="display: block; padding-right: 30px;">${shareIcon}${icon}${icon ? ' ' : ''}${note.name}</span>
|
<span class="truncate" style="display: block; padding-right: 30px;">${shareIcon}${icon}${icon ? ' ' : ''}${esc(note.name)}</span>
|
||||||
<button
|
<button
|
||||||
onclick="${deleteHandler}"
|
data-path="${esc(note.path)}"
|
||||||
|
data-name="${esc(note.name)}"
|
||||||
|
data-type="${note.type}"
|
||||||
|
onclick="window.$root.handleDeleteItemClick(this, event)"
|
||||||
class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity"
|
class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity"
|
||||||
style="opacity: 0; color: var(--error);"
|
style="opacity: 0; color: var(--error);"
|
||||||
title="${isImage ? 'Delete image' : 'Delete note'}"
|
title="${isImage ? 'Delete image' : 'Delete note'}"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue