Allow uploading of files and folders.
This commit is contained in:
@@ -19,6 +19,10 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn btn-secondary" id="help-toggle">
|
||||
<span class="icon"><i class="ti ti-help"></i></span>
|
||||
Help
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="settings-toggle">
|
||||
<span class="icon"><i class="ti ti-settings"></i></span>
|
||||
Settings
|
||||
@@ -132,6 +136,59 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Panel -->
|
||||
<div class="help-panel" id="help-panel" style="display: none;">
|
||||
<div class="help-card">
|
||||
<h3 class="help-title">
|
||||
<span class="icon"><i class="ti ti-help"></i></span>
|
||||
Markdown & Wiki Syntax Help
|
||||
</h3>
|
||||
<div class="help-content">
|
||||
<div class="help-section">
|
||||
<h4>Wiki-style Links</h4>
|
||||
<ul class="help-list">
|
||||
<li><code>[[Note Title]]</code> - Create a link to another note</li>
|
||||
<li><code>![[Note Title]]</code> - Embed another note's content</li>
|
||||
</ul>
|
||||
<p class="help-note">Wiki links work with note titles or slugs. If a note isn't found, it will show as a broken link.</p>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h4>Basic Markdown</h4>
|
||||
<ul class="help-list">
|
||||
<li><code># Heading 1</code> / <code>## Heading 2</code> / <code>### Heading 3</code></li>
|
||||
<li><code>**bold**</code> / <code>*italic*</code> / <code>`code`</code></li>
|
||||
<li><code>[Link text](url)</code> - Regular links</li>
|
||||
<li><code></code> - Images</li>
|
||||
<li><code>- Item</code> - Bullet lists</li>
|
||||
<li><code>1. Item</code> - Numbered lists</li>
|
||||
<li><code>- [ ] Task</code> - Checklists</li>
|
||||
<li><code>> Quote</code> - Blockquotes</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h4>Advanced Markdown</h4>
|
||||
<ul class="help-list">
|
||||
<li><code>```language<br>code block<br>```</code> - Code blocks with syntax highlighting</li>
|
||||
<li><code>| Header | Header |<br>|--------|--------|<br>| Cell | Cell |</code> - Tables</li>
|
||||
<li><code>---</code> - Horizontal rule</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h4>Tips</h4>
|
||||
<ul class="help-list">
|
||||
<li>Use frontmatter (YAML at the top) to set metadata</li>
|
||||
<li>Press <kbd>Ctrl</kbd>+<kbd>S</kbd> to save quickly</li>
|
||||
<li>The preview updates in real-time as you type</li>
|
||||
<li>Wiki embeds show a preview of the linked note</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor Form -->
|
||||
<form method="POST" id="note-form">
|
||||
<input type="hidden" id="title" name="title" value="{{ note.title if note else '' }}" required>
|
||||
@@ -222,12 +279,33 @@
|
||||
<i class="ti ti-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('[[', ']]')" title="Wiki Link - Link to another note">
|
||||
<i class="ti ti-file-symlink"></i>
|
||||
</button>
|
||||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('![[', ']]')" title="Wiki Embed - Embed another note">
|
||||
<i class="ti ti-file-import"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ace Editor Container -->
|
||||
<div id="ace-editor" class="ace-editor-container">{{ note.content if note else '# New Note\n\nStart writing here...' }}</div>
|
||||
<textarea id="content" name="content" style="display: none;" required>{{ note.content if note else '# New Note\n\nStart writing here...' }}</textarea>
|
||||
|
||||
<!-- Wiki Autocomplete Popup -->
|
||||
<div id="wiki-autocomplete" class="wiki-autocomplete" style="display: none;">
|
||||
<div class="autocomplete-search">
|
||||
<input type="text" id="autocomplete-query" class="autocomplete-input" placeholder="Search notes...">
|
||||
</div>
|
||||
<div id="autocomplete-results" class="autocomplete-results">
|
||||
<!-- Results will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor Footer -->
|
||||
<div class="editor-footer">
|
||||
<div class="editor-stats">
|
||||
@@ -601,6 +679,196 @@
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Help Panel */
|
||||
.help-panel {
|
||||
margin-bottom: 2rem;
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.help-card {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.help-title {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1.25rem;
|
||||
color: #2c3e50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.help-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
background: #f8f9fa;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.help-section h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #495057;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.help-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.help-list li {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.help-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.help-list code {
|
||||
background: #fff;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
color: #e83e8c;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.help-note {
|
||||
margin: 1rem 0 0 0;
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 rgba(0,0,0,0.2);
|
||||
display: inline-block;
|
||||
font-size: 0.85em;
|
||||
font-family: monospace;
|
||||
line-height: 1;
|
||||
padding: 2px 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Wiki Autocomplete */
|
||||
.wiki-autocomplete {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
width: 300px;
|
||||
max-height: 250px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.autocomplete-search {
|
||||
padding: 0.375rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8125rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.autocomplete-input:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.15rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.autocomplete-results {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 180px;
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
|
||||
.autocomplete-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.autocomplete-item:hover,
|
||||
.autocomplete-item.selected {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.autocomplete-item.selected {
|
||||
background: #e7f3ff;
|
||||
}
|
||||
|
||||
.autocomplete-item-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.autocomplete-item-meta {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.6875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.autocomplete-item-folder,
|
||||
.autocomplete-item-visibility {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.autocomplete-item-folder i,
|
||||
.autocomplete-item-visibility i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.autocomplete-item-preview {
|
||||
display: none; /* Hide preview to save space */
|
||||
}
|
||||
|
||||
.autocomplete-empty {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.autocomplete-loading {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
@@ -952,7 +1220,8 @@ function initializeAceEditor() {
|
||||
useSoftTabs: true,
|
||||
wrap: true,
|
||||
showInvisibles: false,
|
||||
scrollPastEnd: 0.5
|
||||
scrollPastEnd: 0.5,
|
||||
behavioursEnabled: true // Keep auto-closing brackets enabled
|
||||
});
|
||||
|
||||
// Set initial content from hidden textarea
|
||||
@@ -1057,17 +1326,37 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Settings toggle
|
||||
const settingsBtn = document.getElementById('settings-toggle');
|
||||
const settingsPanel = document.getElementById('settings-panel');
|
||||
const helpBtn = document.getElementById('help-toggle');
|
||||
const helpPanel = document.getElementById('help-panel');
|
||||
|
||||
settingsBtn.addEventListener('click', function() {
|
||||
if (settingsPanel.style.display === 'none' || !settingsPanel.style.display) {
|
||||
settingsPanel.style.display = 'block';
|
||||
this.classList.add('active');
|
||||
// Close help panel if open
|
||||
helpPanel.style.display = 'none';
|
||||
helpBtn.classList.remove('active');
|
||||
} else {
|
||||
settingsPanel.style.display = 'none';
|
||||
this.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Help toggle
|
||||
|
||||
helpBtn.addEventListener('click', function() {
|
||||
if (helpPanel.style.display === 'none' || !helpPanel.style.display) {
|
||||
helpPanel.style.display = 'block';
|
||||
this.classList.add('active');
|
||||
// Close settings panel if open
|
||||
settingsPanel.style.display = 'none';
|
||||
settingsBtn.classList.remove('active');
|
||||
} else {
|
||||
helpPanel.style.display = 'none';
|
||||
this.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Preview toggle
|
||||
const previewToggle = document.getElementById('preview-toggle');
|
||||
const previewPanel = document.getElementById('preview-panel');
|
||||
@@ -1130,6 +1419,285 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
syncContentAndUpdatePreview(false);
|
||||
syncSettingsToHiddenFields(); // Sync to hidden fields
|
||||
});
|
||||
|
||||
// Wiki Autocomplete functionality
|
||||
let autocompleteActive = false;
|
||||
let autocompleteType = ''; // 'link' or 'embed'
|
||||
let autocompleteStartPos = null;
|
||||
let selectedIndex = -1;
|
||||
let autocompleteResults = [];
|
||||
|
||||
const autocompletePopup = document.getElementById('wiki-autocomplete');
|
||||
const autocompleteInput = document.getElementById('autocomplete-query');
|
||||
const autocompleteResultsDiv = document.getElementById('autocomplete-results');
|
||||
|
||||
// Listen for [[ or ![[ triggers in ACE editor
|
||||
aceEditor.commands.on("afterExec", function(e) {
|
||||
if (e.command.name === "insertstring") {
|
||||
const cursor = aceEditor.getCursorPosition();
|
||||
const line = aceEditor.session.getLine(cursor.row);
|
||||
const textBeforeCursor = line.substring(0, cursor.column);
|
||||
|
||||
// Check for [[ or ![[ triggers
|
||||
if (textBeforeCursor.endsWith('[[')) {
|
||||
// Regular link
|
||||
autocompleteType = 'link';
|
||||
autocompleteStartPos = {row: cursor.row, column: cursor.column};
|
||||
|
||||
// Check if ACE auto-closed the brackets
|
||||
const textAfterCursor = line.substring(cursor.column);
|
||||
if (textAfterCursor.startsWith(']]')) {
|
||||
// Move cursor back inside the brackets
|
||||
aceEditor.moveCursorToPosition({row: cursor.row, column: cursor.column});
|
||||
}
|
||||
|
||||
showAutocomplete();
|
||||
} else if (textBeforeCursor.endsWith('![[')) {
|
||||
// Embed
|
||||
autocompleteType = 'embed';
|
||||
autocompleteStartPos = {row: cursor.row, column: cursor.column};
|
||||
|
||||
// Check if ACE auto-closed the brackets
|
||||
const textAfterCursor = line.substring(cursor.column);
|
||||
if (textAfterCursor.startsWith(']]')) {
|
||||
// Move cursor back inside the brackets
|
||||
aceEditor.moveCursorToPosition({row: cursor.row, column: cursor.column});
|
||||
}
|
||||
|
||||
showAutocomplete();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function showAutocomplete() {
|
||||
autocompleteActive = true;
|
||||
selectedIndex = -1;
|
||||
|
||||
// Get cursor position in the editor
|
||||
const cursorPixelPos = aceEditor.renderer.textToScreenCoordinates(
|
||||
autocompleteStartPos.row,
|
||||
autocompleteStartPos.column
|
||||
);
|
||||
|
||||
// Get editor container position
|
||||
const editorContainer = aceEditor.container;
|
||||
const editorRect = editorContainer.getBoundingClientRect();
|
||||
|
||||
// Calculate position relative to the editor container
|
||||
// The popup should appear right below the cursor
|
||||
const lineHeight = aceEditor.renderer.lineHeight;
|
||||
const left = cursorPixelPos.pageX - editorRect.left - window.scrollX;
|
||||
const top = cursorPixelPos.pageY - editorRect.top + lineHeight + 5 - window.scrollY;
|
||||
|
||||
// Make sure popup doesn't go off the right edge
|
||||
const maxLeft = editorRect.width - 300; // 300px is popup width
|
||||
autocompletePopup.style.left = Math.min(left, maxLeft) + 'px';
|
||||
autocompletePopup.style.top = top + 'px';
|
||||
autocompletePopup.style.display = 'block';
|
||||
|
||||
// Focus input and load initial results
|
||||
autocompleteInput.value = '';
|
||||
autocompleteInput.focus();
|
||||
searchNotes('');
|
||||
}
|
||||
|
||||
function hideAutocomplete() {
|
||||
autocompleteActive = false;
|
||||
autocompletePopup.style.display = 'none';
|
||||
aceEditor.focus();
|
||||
}
|
||||
|
||||
function searchNotes(query) {
|
||||
// Show loading state
|
||||
autocompleteResultsDiv.innerHTML = '<div class="autocomplete-loading">Loading...</div>';
|
||||
|
||||
// Fetch notes from API
|
||||
fetch(`/api/notes/autocomplete?q=${encodeURIComponent(query)}&limit=20`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
autocompleteResults = data.results;
|
||||
renderResults(data.results);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching notes:', error);
|
||||
autocompleteResultsDiv.innerHTML = '<div class="autocomplete-empty">Error loading notes</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderResults(results) {
|
||||
if (results.length === 0) {
|
||||
autocompleteResultsDiv.innerHTML = '<div class="autocomplete-empty">No notes found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
results.forEach((note, index) => {
|
||||
const visibilityIcon = note.visibility === 'Private' ? 'lock' :
|
||||
note.visibility === 'Team' ? 'users' : 'building';
|
||||
|
||||
html += `
|
||||
<div class="autocomplete-item ${index === selectedIndex ? 'selected' : ''}"
|
||||
data-index="${index}">
|
||||
<div class="autocomplete-item-title">${escapeHtml(note.title)}</div>
|
||||
<div class="autocomplete-item-meta">
|
||||
${note.folder ? `
|
||||
<div class="autocomplete-item-folder">
|
||||
<i class="ti ti-folder"></i>
|
||||
${escapeHtml(note.folder)}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="autocomplete-item-visibility">
|
||||
<i class="ti ti-${visibilityIcon}"></i>
|
||||
${note.visibility}
|
||||
</div>
|
||||
</div>
|
||||
${note.preview ? `
|
||||
<div class="autocomplete-item-preview">${escapeHtml(note.preview)}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
autocompleteResultsDiv.innerHTML = html;
|
||||
|
||||
// Add click handlers
|
||||
document.querySelectorAll('.autocomplete-item').forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
const index = parseInt(this.dataset.index);
|
||||
selectNote(autocompleteResults[index]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function selectNote(note) {
|
||||
if (!note) return;
|
||||
|
||||
// Get current cursor position
|
||||
const cursor = aceEditor.getCursorPosition();
|
||||
const line = aceEditor.session.getLine(cursor.row);
|
||||
|
||||
// Check if there are already closing brackets after cursor
|
||||
const textAfterCursor = line.substring(cursor.column);
|
||||
const hasClosingBrackets = textAfterCursor.startsWith(']]');
|
||||
|
||||
// Insert only the title if closing brackets already exist
|
||||
const insertText = hasClosingBrackets ? note.title : note.title + ']]';
|
||||
|
||||
// Insert the text
|
||||
aceEditor.session.insert(cursor, insertText);
|
||||
|
||||
// If we didn't add closing brackets, move cursor past existing ones
|
||||
if (hasClosingBrackets) {
|
||||
const newPos = {
|
||||
row: cursor.row,
|
||||
column: cursor.column + note.title.length + 2 // +2 for ]]
|
||||
};
|
||||
aceEditor.moveCursorToPosition(newPos);
|
||||
}
|
||||
|
||||
// Hide autocomplete
|
||||
hideAutocomplete();
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Handle input changes
|
||||
let searchTimeout;
|
||||
autocompleteInput.addEventListener('input', function(e) {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
searchNotes(e.target.value);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// Handle keyboard navigation
|
||||
autocompleteInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (selectedIndex < autocompleteResults.length - 1) {
|
||||
selectedIndex++;
|
||||
updateSelection();
|
||||
}
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
updateSelection();
|
||||
}
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (selectedIndex >= 0 && selectedIndex < autocompleteResults.length) {
|
||||
selectNote(autocompleteResults[selectedIndex]);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
hideAutocomplete();
|
||||
}
|
||||
});
|
||||
|
||||
function updateSelection() {
|
||||
document.querySelectorAll('.autocomplete-item').forEach((item, index) => {
|
||||
if (index === selectedIndex) {
|
||||
item.classList.add('selected');
|
||||
item.scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
item.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close autocomplete when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (autocompleteActive && !autocompletePopup.contains(e.target)) {
|
||||
hideAutocomplete();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle closing brackets and escape
|
||||
aceEditor.on('change', function(e) {
|
||||
if (!autocompleteActive) return;
|
||||
|
||||
if (e.action === 'insert') {
|
||||
const cursor = aceEditor.getCursorPosition();
|
||||
const line = aceEditor.session.getLine(cursor.row);
|
||||
|
||||
// Check if user typed ]] to close the link
|
||||
if (e.lines[0].includes(']')) {
|
||||
const textBeforeCursor = line.substring(0, cursor.column);
|
||||
if (textBeforeCursor.endsWith(']]')) {
|
||||
hideAutocomplete();
|
||||
}
|
||||
}
|
||||
} else if (e.action === 'remove') {
|
||||
// Check if user deleted the opening [[
|
||||
const cursor = aceEditor.getCursorPosition();
|
||||
const line = aceEditor.session.getLine(cursor.row);
|
||||
const textBeforeCursor = line.substring(0, cursor.column);
|
||||
|
||||
if (!textBeforeCursor.includes('[[') && !textBeforeCursor.includes('![[')) {
|
||||
hideAutocomplete();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle escape key in editor
|
||||
aceEditor.commands.addCommand({
|
||||
name: 'closeAutocomplete',
|
||||
bindKey: {win: 'Escape', mac: 'Escape'},
|
||||
exec: function() {
|
||||
if (autocompleteActive) {
|
||||
hideAutocomplete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -152,9 +152,31 @@
|
||||
|
||||
<!-- Note Content -->
|
||||
<div class="content-card">
|
||||
{% if note.is_file_based and note.file_type == 'document' and note.original_filename.endswith('.pdf') %}
|
||||
<!-- PDF Preview -->
|
||||
<div class="pdf-preview-container">
|
||||
<div class="pdf-toolbar">
|
||||
<button class="btn btn-sm btn-secondary" onclick="pdfZoomIn()">
|
||||
<i class="ti ti-zoom-in"></i> Zoom In
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="pdfZoomOut()">
|
||||
<i class="ti ti-zoom-out"></i> Zoom Out
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="pdfZoomReset()">
|
||||
<i class="ti ti-zoom-reset"></i> Reset
|
||||
</button>
|
||||
<a href="{{ note.file_url }}" class="btn btn-sm btn-primary" download>
|
||||
<i class="ti ti-download"></i> Download PDF
|
||||
</a>
|
||||
</div>
|
||||
<iframe id="pdf-viewer" src="{{ note.file_url }}" class="pdf-viewer"></iframe>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Regular Content -->
|
||||
<div class="markdown-content">
|
||||
{{ note.render_html()|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Linked Notes Section -->
|
||||
@@ -857,9 +879,181 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Wiki-style Links */
|
||||
.wiki-link {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--primary-color);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.wiki-link:hover {
|
||||
border-bottom-style: solid;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wiki-link-broken {
|
||||
color: #dc3545;
|
||||
text-decoration: line-through;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Wiki-style Embeds */
|
||||
.wiki-embed {
|
||||
margin: 1.5rem 0;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.wiki-embed-header {
|
||||
padding: 0.75rem 1rem;
|
||||
background: white;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wiki-embed-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wiki-embed-title:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.wiki-embed-content {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wiki-embed-content .markdown-content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wiki-embed-error {
|
||||
margin: 1rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 6px;
|
||||
color: #721c24;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Nested embeds have reduced padding */
|
||||
.wiki-embed .wiki-embed {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.wiki-embed .wiki-embed .wiki-embed-content {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
/* Font families based on user preferences */
|
||||
{% if g.user.preferences and g.user.preferences.note_preview_font %}
|
||||
{% set font = g.user.preferences.note_preview_font %}
|
||||
{% if font == 'sans-serif' %}
|
||||
.markdown-content { font-family: Arial, Helvetica, sans-serif; }
|
||||
{% elif font == 'serif' %}
|
||||
.markdown-content { font-family: "Times New Roman", Times, serif; }
|
||||
{% elif font == 'monospace' %}
|
||||
.markdown-content { font-family: "Courier New", Courier, monospace; }
|
||||
{% elif font == 'georgia' %}
|
||||
.markdown-content { font-family: Georgia, serif; }
|
||||
{% elif font == 'palatino' %}
|
||||
.markdown-content { font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif; }
|
||||
{% elif font == 'garamond' %}
|
||||
.markdown-content { font-family: Garamond, serif; }
|
||||
{% elif font == 'bookman' %}
|
||||
.markdown-content { font-family: "Bookman Old Style", serif; }
|
||||
{% elif font == 'comic-sans' %}
|
||||
.markdown-content { font-family: "Comic Sans MS", cursive; }
|
||||
{% elif font == 'trebuchet' %}
|
||||
.markdown-content { font-family: "Trebuchet MS", sans-serif; }
|
||||
{% elif font == 'arial-black' %}
|
||||
.markdown-content { font-family: "Arial Black", sans-serif; }
|
||||
{% elif font == 'impact' %}
|
||||
.markdown-content { font-family: Impact, sans-serif; }
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
/* PDF Preview Styles */
|
||||
.pdf-preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.pdf-toolbar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.pdf-viewer {
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
border: none;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Responsive PDF viewer */
|
||||
@media (max-width: 768px) {
|
||||
.pdf-viewer {
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.pdf-toolbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// PDF viewer controls
|
||||
let pdfZoom = 1.0;
|
||||
|
||||
function pdfZoomIn() {
|
||||
pdfZoom += 0.1;
|
||||
updatePdfZoom();
|
||||
}
|
||||
|
||||
function pdfZoomOut() {
|
||||
if (pdfZoom > 0.5) {
|
||||
pdfZoom -= 0.1;
|
||||
updatePdfZoom();
|
||||
}
|
||||
}
|
||||
|
||||
function pdfZoomReset() {
|
||||
pdfZoom = 1.0;
|
||||
updatePdfZoom();
|
||||
}
|
||||
|
||||
function updatePdfZoom() {
|
||||
const viewer = document.getElementById('pdf-viewer');
|
||||
if (viewer) {
|
||||
viewer.style.transform = `scale(${pdfZoom})`;
|
||||
viewer.style.transformOrigin = 'top center';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Download dropdown functionality
|
||||
const downloadBtn = document.getElementById('downloadDropdown');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -301,6 +301,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note Preferences Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">
|
||||
<span class="icon"><i class="ti ti-notes"></i></span>
|
||||
Note Preferences
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('update_note_preferences') }}" class="modern-form">
|
||||
<div class="form-group">
|
||||
<label for="note_preview_font" class="form-label">Preview Font</label>
|
||||
<select id="note_preview_font" name="note_preview_font" class="form-control">
|
||||
<option value="system" {% if not g.user.preferences or g.user.preferences.note_preview_font == 'system' %}selected{% endif %}>System Default</option>
|
||||
<option value="sans-serif" {% if g.user.preferences and g.user.preferences.note_preview_font == 'sans-serif' %}selected{% endif %}>Sans-serif (Arial, Helvetica)</option>
|
||||
<option value="serif" {% if g.user.preferences and g.user.preferences.note_preview_font == 'serif' %}selected{% endif %}>Serif (Times, Georgia)</option>
|
||||
<option value="monospace" {% if g.user.preferences and g.user.preferences.note_preview_font == 'monospace' %}selected{% endif %}>Monospace (Courier, Consolas)</option>
|
||||
<option value="georgia" {% if g.user.preferences and g.user.preferences.note_preview_font == 'georgia' %}selected{% endif %}>Georgia</option>
|
||||
<option value="palatino" {% if g.user.preferences and g.user.preferences.note_preview_font == 'palatino' %}selected{% endif %}>Palatino</option>
|
||||
<option value="garamond" {% if g.user.preferences and g.user.preferences.note_preview_font == 'garamond' %}selected{% endif %}>Garamond</option>
|
||||
<option value="bookman" {% if g.user.preferences and g.user.preferences.note_preview_font == 'bookman' %}selected{% endif %}>Bookman</option>
|
||||
<option value="comic-sans" {% if g.user.preferences and g.user.preferences.note_preview_font == 'comic-sans' %}selected{% endif %}>Comic Sans MS</option>
|
||||
<option value="trebuchet" {% if g.user.preferences and g.user.preferences.note_preview_font == 'trebuchet' %}selected{% endif %}>Trebuchet MS</option>
|
||||
<option value="arial-black" {% if g.user.preferences and g.user.preferences.note_preview_font == 'arial-black' %}selected{% endif %}>Arial Black</option>
|
||||
<option value="impact" {% if g.user.preferences and g.user.preferences.note_preview_font == 'impact' %}selected{% endif %}>Impact</option>
|
||||
</select>
|
||||
<span class="form-hint">Choose your preferred font for note content</span>
|
||||
</div>
|
||||
|
||||
<div class="font-preview-section">
|
||||
<label class="form-label">Preview</label>
|
||||
<div id="font-preview" class="font-preview">
|
||||
<h3>Sample Heading</h3>
|
||||
<p>This is how your notes will appear with the selected font. The quick brown fox jumps over the lazy dog.</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||
<ul>
|
||||
<li>First item in a list</li>
|
||||
<li>Second item with <strong>bold text</strong></li>
|
||||
<li>Third item with <em>italic text</em></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="icon"><i class="ti ti-check"></i></span>
|
||||
Save Preferences
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -905,6 +956,57 @@
|
||||
.card:nth-child(3) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.card:nth-child(4) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
/* Font Preview Section */
|
||||
.font-preview-section {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.font-preview {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: #f8f9fa;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.font-preview h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.font-preview p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.font-preview ul {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.font-preview li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Font families for preview */
|
||||
.font-system { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
||||
.font-sans-serif { font-family: Arial, Helvetica, sans-serif; }
|
||||
.font-serif { font-family: "Times New Roman", Times, serif; }
|
||||
.font-monospace { font-family: "Courier New", Courier, monospace; }
|
||||
.font-georgia { font-family: Georgia, serif; }
|
||||
.font-palatino { font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif; }
|
||||
.font-garamond { font-family: Garamond, serif; }
|
||||
.font-bookman { font-family: "Bookman Old Style", serif; }
|
||||
.font-comic-sans { font-family: "Comic Sans MS", cursive; }
|
||||
.font-trebuchet { font-family: "Trebuchet MS", sans-serif; }
|
||||
.font-arial-black { font-family: "Arial Black", sans-serif; }
|
||||
.font-impact { font-family: Impact, sans-serif; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -1032,6 +1134,33 @@ function isValidUrl(string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Font preview update
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fontSelect = document.getElementById('note_preview_font');
|
||||
const fontPreview = document.getElementById('font-preview');
|
||||
|
||||
if (fontSelect && fontPreview) {
|
||||
fontSelect.addEventListener('change', function() {
|
||||
updateFontPreview(this.value);
|
||||
});
|
||||
|
||||
// Set initial font
|
||||
updateFontPreview(fontSelect.value);
|
||||
}
|
||||
|
||||
function updateFontPreview(fontValue) {
|
||||
// Remove all font classes
|
||||
fontPreview.className = 'font-preview';
|
||||
|
||||
// Add the selected font class
|
||||
if (fontValue !== 'system') {
|
||||
fontPreview.classList.add('font-' + fontValue);
|
||||
} else {
|
||||
fontPreview.classList.add('font-system');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function resetAvatar() {
|
||||
if (confirm('Reset to your default avatar? This will remove any custom avatar.')) {
|
||||
const form = document.createElement('form');
|
||||
|
||||
Reference in New Issue
Block a user