540 lines
23 KiB
HTML
540 lines
23 KiB
HTML
{{define "create"}}
|
|
{{template "base" .}}
|
|
{{end}}
|
|
|
|
{{define "create_content"}}
|
|
<div class="max-w-4xl mx-auto p-6">
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h1 class="text-3xl font-bold text-white">Create New Note</h1>
|
|
<div class="flex items-center space-x-3">
|
|
<button id="toggle-preview" type="button" class="btn-secondary">
|
|
<i class="fas fa-eye mr-2"></i>Preview
|
|
</button>
|
|
<button type="submit" form="create-form" class="btn-primary">
|
|
<i class="fas fa-save mr-2"></i>Create
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{{if .folder_path}}
|
|
<p class="text-gray-400">
|
|
<i class="fas fa-folder mr-2"></i>
|
|
Creating in: <span class="text-blue-400">{{.folder_path}}</span>
|
|
</p>
|
|
{{end}}
|
|
</div>
|
|
|
|
<!-- Create Form -->
|
|
<form id="create-form" class="space-y-6">
|
|
<div class="bg-gray-800 rounded-lg p-6">
|
|
<!-- Title Input -->
|
|
<div class="mb-6">
|
|
<label for="title" class="block text-sm font-medium text-gray-300 mb-2">
|
|
Note Title
|
|
</label>
|
|
<input type="text" id="title" name="title" required
|
|
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="Enter note title (e.g., My New Note)">
|
|
<p class="text-xs text-gray-500 mt-1">The .md extension will be added automatically</p>
|
|
</div>
|
|
|
|
<!-- Content Editor with Live Preview -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<label for="content" class="block text-sm font-medium text-gray-300">
|
|
Content
|
|
</label>
|
|
<div class="text-xs text-gray-400">Live Preview</div>
|
|
</div>
|
|
<div id="editor-grid" class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<textarea id="content" name="content" rows="20"
|
|
class="editor-textarea"
|
|
placeholder="# Your Note Title
|
|
|
|
Start writing your note here using Markdown syntax...
|
|
|
|
## Examples
|
|
|
|
- **Bold text**
|
|
- *Italic text*
|
|
- [Links](https://example.com)
|
|
- 
|
|
- `Code snippets`
|
|
|
|
```javascript
|
|
// Code blocks
|
|
console.log('Hello, World!');
|
|
```
|
|
|
|
> Blockquotes
|
|
|
|
| Tables | Work | Too |
|
|
|--------|------|-----|
|
|
| Cell 1 | Cell 2 | Cell 3 |
|
|
"></textarea>
|
|
<div id="live-preview" class="prose prose-invert prose-dark bg-slate-800 border border-gray-700 rounded-lg p-4 overflow-y-auto hidden" style="min-height: 20rem;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Editor Toolbar -->
|
|
<div class="flex items-center justify-between border-t border-gray-700 pt-4">
|
|
<div class="flex items-center flex-wrap gap-2">
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('**', '**')" title="Bold">
|
|
<i class="fas fa-bold"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('*', '*')" title="Italic">
|
|
<i class="fas fa-italic"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('~~', '~~')" title="Strikethrough">
|
|
<i class="fas fa-strikethrough"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('`', '`')" title="Inline code">
|
|
<i class="fas fa-code"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertCodeBlock()" title="Code block">
|
|
<i class="fas fa-file-code"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertMermaid()" title="Mermaid diagram">
|
|
<i class="fas fa-project-diagram"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('[', '](url)')" title="Link">
|
|
<i class="fas fa-link"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('')" title="Image">
|
|
<i class="fas fa-image"></i>
|
|
</button>
|
|
<!-- Headings dropdown -->
|
|
<div class="relative">
|
|
<button type="button" class="btn-secondary text-sm" onclick="toggleHeadingMenu(event)" title="Headings">
|
|
<i class="fas fa-heading"></i>
|
|
</button>
|
|
<div id="heading-menu" class="absolute z-10 hidden bg-slate-800 border border-gray-700 rounded shadow-lg right-0 bottom-full mb-2">
|
|
<button type="button" class="block w-full text-left px-3 py-2 hover:bg-slate-700" onclick="insertHeadingLevel(1)">H1</button>
|
|
<button type="button" class="block w-full text-left px-3 py-2 hover:bg-slate-700" onclick="insertHeadingLevel(2)">H2</button>
|
|
<button type="button" class="block w-full text-left px-3 py-2 hover:bg-slate-700" onclick="insertHeadingLevel(3)">H3</button>
|
|
<button type="button" class="block w-full text-left px-3 py-2 hover:bg-slate-700" onclick="insertHeadingLevel(4)">H4</button>
|
|
<button type="button" class="block w-full text-left px-3 py-2 hover:bg-slate-700" onclick="insertHeadingLevel(5)">H5</button>
|
|
<button type="button" class="block w-full text-left px-3 py-2 hover:bg-slate-700" onclick="insertHeadingLevel(6)">H6</button>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertList()" title="Bulleted list">
|
|
<i class="fas fa-list"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertNumberedList()" title="Numbered list">
|
|
<i class="fas fa-list-ol"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertChecklist()" title="Task list">
|
|
<i class="fas fa-square-check"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertBlockquote()" title="Blockquote">
|
|
<i class="fas fa-quote-left"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertHr()" title="Horizontal rule">
|
|
<i class="fas fa-minus"></i>
|
|
</button>
|
|
<button type="button" class="btn-secondary text-sm" onclick="insertTable()" title="Table">
|
|
<i class="fas fa-table"></i>
|
|
</button>
|
|
</div>
|
|
<div class="text-xs text-gray-500">Press Ctrl+S to create</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "create_scripts"}}
|
|
<script>
|
|
const createForm = document.getElementById('create-form');
|
|
const titleInput = document.getElementById('title');
|
|
const contentTextarea = document.getElementById('content');
|
|
const livePreview = document.getElementById('live-preview');
|
|
const editorGrid = document.getElementById('editor-grid');
|
|
const togglePreviewBtn = document.getElementById('toggle-preview');
|
|
|
|
const imageStorageMode = parseInt('{{.image_storage_mode}}', 10) || 1;
|
|
const imageSubfolderName = "{{.image_subfolder_name}}";
|
|
const currentFolderPath = "{{.folder_path}}";
|
|
|
|
// Preview state (persist for create page)
|
|
const PREVIEW_STATE_KEY = 'previewEnabled:create';
|
|
let previewEnabled = localStorage.getItem(PREVIEW_STATE_KEY) === 'true';
|
|
|
|
// Load Marked for client-side markdown rendering
|
|
(function ensureMarked() {
|
|
if (window.marked) return;
|
|
const s = document.createElement('script');
|
|
s.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
|
|
document.head.appendChild(s);
|
|
})();
|
|
|
|
function buildImageURL(filename) {
|
|
if (imageStorageMode === 2) {
|
|
return window.prefix(`/serve_stored_image/${filename}`);
|
|
}
|
|
let path = filename;
|
|
if (imageStorageMode === 3 && currentFolderPath) {
|
|
path = `${currentFolderPath}/${filename}`;
|
|
} else if (imageStorageMode === 4 && currentFolderPath) {
|
|
path = `${currentFolderPath}/${imageSubfolderName}/${filename}`;
|
|
}
|
|
return window.prefix(`/serve_attached_image/${path}`);
|
|
}
|
|
|
|
function transformObsidianEmbeds(md) {
|
|
return (md || '').replace(/!\[\[([^\]|]+)(?:\|([^\]]*))?\]\]/g, (m, file, alt) => {
|
|
const filename = file.trim();
|
|
const url = buildImageURL(filename);
|
|
const altText = (alt || filename).trim();
|
|
return ``;
|
|
});
|
|
}
|
|
|
|
function applyPreviewState() {
|
|
if (!livePreview || !editorGrid) return;
|
|
if (previewEnabled) {
|
|
livePreview.classList.remove('hidden');
|
|
editorGrid.classList.add('lg:grid-cols-2');
|
|
renderPreview();
|
|
togglePreviewBtn && (togglePreviewBtn.innerHTML = '<i class="fas fa-eye-slash mr-2"></i>Hide Preview');
|
|
} else {
|
|
livePreview.classList.add('hidden');
|
|
editorGrid.classList.remove('lg:grid-cols-2');
|
|
togglePreviewBtn && (togglePreviewBtn.innerHTML = '<i class="fas fa-eye mr-2"></i>Preview');
|
|
}
|
|
}
|
|
|
|
function transformMermaidFences(md) {
|
|
return (md || '').replace(/```mermaid\n([\s\S]*?)```/g, function(_, inner){
|
|
return `<div class=\"mermaid\">${inner}</div>`;
|
|
});
|
|
}
|
|
|
|
function transformMediaEmbeds(md) {
|
|
const lines = (md || '').split(/\n/);
|
|
return lines.map(l => {
|
|
const t = l.trim();
|
|
if (/^https?:\/\/\S+\.(?:mp3|wav|ogg)$/.test(t)) return `<audio controls preload=\"metadata\" src=\"${t}\"></audio>`;
|
|
if (/^https?:\/\/\S+\.(?:mp4|webm|ogg)$/.test(t)) return `<video controls preload=\"metadata\" style=\"max-width:100%\" src=\"${t}\"></video>`;
|
|
if (/^https?:\/\/\S+\.(?:pdf)$/.test(t)) return `<iframe src=\"${t}\" style=\"width:100%;height:70vh;border:1px solid #374151;border-radius:8px\"></iframe>`;
|
|
return l;
|
|
}).join('\n');
|
|
}
|
|
|
|
// Render preview
|
|
function renderPreview() {
|
|
if (!previewEnabled) return;
|
|
if (!livePreview || !window.marked) return;
|
|
let transformed = transformObsidianEmbeds(contentTextarea.value || '');
|
|
transformed = transformMermaidFences(transformed);
|
|
transformed = transformMediaEmbeds(transformed);
|
|
livePreview.innerHTML = marked.parse(transformed);
|
|
// Highlight code blocks
|
|
livePreview.querySelectorAll('pre code').forEach(block => {
|
|
try { hljs.highlightElement(block); } catch (e) {}
|
|
});
|
|
if (window.mermaid) {
|
|
try { window.mermaid.run({ querySelector: '#live-preview .mermaid' }); } catch (e) {}
|
|
}
|
|
}
|
|
|
|
createForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
const title = titleInput.value.trim();
|
|
const content = contentTextarea.value;
|
|
const folderPath = '{{.folder_path}}';
|
|
|
|
if (!title) {
|
|
showNotification('Please enter a note title', 'error');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('title', title);
|
|
formData.append('content', content);
|
|
formData.append('folder_path', folderPath);
|
|
|
|
// CSRF token from cookie
|
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
|
|
|
fetch(window.prefix('/editor/create'), {
|
|
method: 'POST',
|
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showNotification('Note created successfully', 'success');
|
|
if (data.redirect) {
|
|
window.location.href = data.redirect;
|
|
}
|
|
} else {
|
|
throw new Error(data.error || 'Failed to create note');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showNotification('Error: ' + error.message, 'error');
|
|
});
|
|
});
|
|
|
|
// Auto-resize textarea
|
|
contentTextarea.addEventListener('input', function() {
|
|
this.style.height = 'auto';
|
|
this.style.height = (this.scrollHeight) + 'px';
|
|
renderPreview();
|
|
});
|
|
|
|
// Initial preview state
|
|
document.addEventListener('DOMContentLoaded', applyPreviewState);
|
|
|
|
// Toggle preview
|
|
if (togglePreviewBtn) {
|
|
togglePreviewBtn.addEventListener('click', function() {
|
|
previewEnabled = !previewEnabled;
|
|
localStorage.setItem(PREVIEW_STATE_KEY, String(previewEnabled));
|
|
applyPreviewState();
|
|
});
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
contentTextarea.addEventListener('keydown', function(e) {
|
|
// Ctrl+S or Cmd+S to save
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
e.preventDefault();
|
|
createForm.dispatchEvent(new Event('submit'));
|
|
}
|
|
|
|
// Tab for indentation
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
const start = this.selectionStart;
|
|
const end = this.selectionEnd;
|
|
const value = this.value;
|
|
|
|
this.value = value.substring(0, start) + '\t' + value.substring(end);
|
|
this.selectionStart = this.selectionEnd = start + 1;
|
|
renderPreview();
|
|
}
|
|
});
|
|
|
|
// Markdown insertion functions (parity with edit.html)
|
|
function insertMarkdown(before, after) {
|
|
const start = contentTextarea.selectionStart;
|
|
const end = contentTextarea.selectionEnd;
|
|
const text = contentTextarea.value;
|
|
const selectedText = text.substring(start, end);
|
|
const newText = text.substring(0, start) + before + selectedText + after + text.substring(end);
|
|
contentTextarea.value = newText;
|
|
const newPos = start + before.length + selectedText.length;
|
|
contentTextarea.focus();
|
|
contentTextarea.setSelectionRange(newPos, newPos);
|
|
renderPreview();
|
|
}
|
|
|
|
function toggleHeadingMenu(e) {
|
|
const menu = document.getElementById('heading-menu');
|
|
if (!menu) return;
|
|
const wasHidden = menu.classList.contains('hidden');
|
|
menu.classList.remove('hidden');
|
|
menu.classList.remove('top-full','mt-2','bottom-full','mb-2');
|
|
const btn = e.currentTarget;
|
|
const rect = btn.getBoundingClientRect();
|
|
const spaceBelow = window.innerHeight - rect.bottom;
|
|
const spaceAbove = rect.top;
|
|
const needed = Math.max(menu.offsetHeight || 192, 192) + 12;
|
|
if (spaceBelow < needed && spaceAbove >= needed) {
|
|
menu.classList.add('bottom-full','mb-2','right-0');
|
|
} else {
|
|
menu.classList.add('top-full','mt-2','right-0');
|
|
}
|
|
if (wasHidden) {
|
|
const close = (ev) => {
|
|
if (!menu.contains(ev.target) && ev.target !== btn) {
|
|
menu.classList.add('hidden');
|
|
document.removeEventListener('click', close);
|
|
}
|
|
};
|
|
setTimeout(() => document.addEventListener('click', close), 0);
|
|
}
|
|
}
|
|
|
|
function insertHeadingLevel(level) {
|
|
const start = contentTextarea.selectionStart;
|
|
const text = contentTextarea.value;
|
|
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
const hashes = '#'.repeat(Math.min(Math.max(level,1),6)) + ' ';
|
|
const newText = text.substring(0, lineStart) + hashes + text.substring(lineStart);
|
|
contentTextarea.value = newText;
|
|
const newPos = start + hashes.length;
|
|
contentTextarea.focus();
|
|
contentTextarea.setSelectionRange(newPos, newPos);
|
|
const menu = document.getElementById('heading-menu');
|
|
if (menu) menu.classList.add('hidden');
|
|
renderPreview();
|
|
}
|
|
|
|
function insertList() {
|
|
const start = contentTextarea.selectionStart;
|
|
const text = contentTextarea.value;
|
|
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
const newText = text.substring(0, lineStart) + '- ' + text.substring(lineStart);
|
|
contentTextarea.value = newText;
|
|
contentTextarea.focus();
|
|
contentTextarea.setSelectionRange(start + 2, start + 2);
|
|
renderPreview();
|
|
}
|
|
|
|
function insertNumberedList() {
|
|
const start = contentTextarea.selectionStart;
|
|
const text = contentTextarea.value;
|
|
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
const newText = text.substring(0, lineStart) + '1. ' + text.substring(lineStart);
|
|
contentTextarea.value = newText;
|
|
contentTextarea.focus();
|
|
contentTextarea.setSelectionRange(start + 3, start + 3);
|
|
renderPreview();
|
|
}
|
|
|
|
function insertChecklist() {
|
|
const start = contentTextarea.selectionStart;
|
|
const text = contentTextarea.value;
|
|
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
const newText = text.substring(0, lineStart) + '- [ ] ' + text.substring(lineStart);
|
|
contentTextarea.value = newText;
|
|
contentTextarea.focus();
|
|
contentTextarea.setSelectionRange(start + 6, start + 6);
|
|
renderPreview();
|
|
}
|
|
|
|
function insertBlockquote() {
|
|
const start = contentTextarea.selectionStart;
|
|
const text = contentTextarea.value;
|
|
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
const newText = text.substring(0, lineStart) + '> ' + text.substring(lineStart);
|
|
contentTextarea.value = newText;
|
|
contentTextarea.focus();
|
|
contentTextarea.setSelectionRange(start + 2, start + 2);
|
|
renderPreview();
|
|
}
|
|
|
|
function insertHr() {
|
|
const start = contentTextarea.selectionStart;
|
|
const text = contentTextarea.value;
|
|
const insert = '\n\n---\n\n';
|
|
const newText = text.substring(0, start) + insert + text.substring(start);
|
|
contentTextarea.value = newText;
|
|
const pos = start + insert.length;
|
|
contentTextarea.focus();
|
|
contentTextarea.setSelectionRange(pos, pos);
|
|
renderPreview();
|
|
}
|
|
|
|
function insertCodeBlock() {
|
|
const start = contentTextarea.selectionStart;
|
|
const text = contentTextarea.value;
|
|
const insert = '\n```language\n// code\n```\n';
|
|
const newText = text.substring(0, start) + insert + text.substring(start);
|
|
contentTextarea.value = newText;
|
|
const pos = start + 4;
|
|
contentTextarea.focus();
|
|
contentTextarea.setSelectionRange(pos, pos);
|
|
renderPreview();
|
|
}
|
|
|
|
function insertMermaid() {
|
|
const start = contentTextarea.selectionStart;
|
|
const text = contentTextarea.value;
|
|
const tpl = '\n```mermaid\nflowchart TD\n A[Start] --> B{Decision}\n B -- Yes --> C[Do thing]\n B -- No --> D[Something else]\n```\n';
|
|
const newText = text.substring(0, start) + tpl + text.substring(start);
|
|
contentTextarea.value = newText;
|
|
const pos = start + 4;
|
|
contentTextarea.focus();
|
|
contentTextarea.setSelectionRange(pos, pos);
|
|
renderPreview();
|
|
}
|
|
|
|
function insertTable() {
|
|
const start = contentTextarea.selectionStart;
|
|
const text = contentTextarea.value;
|
|
const tpl = '\n| Col 1 | Col 2 | Col 3 |\n|-------|-------|-------|\n| A1 | B1 | C1 |\n| A2 | B2 | C2 |\n';
|
|
const newText = text.substring(0, start) + tpl + text.substring(start);
|
|
contentTextarea.value = newText;
|
|
const pos = start + 3;
|
|
contentTextarea.focus();
|
|
contentTextarea.setSelectionRange(pos, pos);
|
|
renderPreview();
|
|
}
|
|
|
|
// Paste-to-upload image
|
|
contentTextarea.addEventListener('paste', async function(e) {
|
|
const items = (e.clipboardData || window.clipboardData).items || [];
|
|
for (let i = 0; i < items.length; i++) {
|
|
const item = items[i];
|
|
if (item.type && item.type.indexOf('image') === 0) {
|
|
e.preventDefault();
|
|
const blob = item.getAsFile();
|
|
const ext = blob.type.split('/')[1] || 'png';
|
|
const filename = `pasted-${Date.now()}.${ext}`;
|
|
|
|
// Compute upload path hint
|
|
let uploadPath = '';
|
|
if (imageStorageMode === 3 || imageStorageMode === 4) {
|
|
// Use folder path with a dummy file to hint the directory
|
|
if (currentFolderPath) {
|
|
uploadPath = currentFolderPath + '/_new_.md';
|
|
}
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', blob, filename);
|
|
formData.append('path', uploadPath);
|
|
|
|
try {
|
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
|
const resp = await fetch(window.prefix('/editor/upload'), {
|
|
method: 'POST',
|
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
|
body: formData
|
|
});
|
|
const data = await resp.json();
|
|
if (!resp.ok || !data.success) throw new Error(data.error || 'Upload failed');
|
|
|
|
// Build Obsidian link path for current draft
|
|
let obsidianPath = filename;
|
|
if (imageStorageMode === 4 && currentFolderPath) {
|
|
obsidianPath = `${currentFolderPath}/${imageSubfolderName}/${filename}`;
|
|
} else if (imageStorageMode === 3 && currentFolderPath) {
|
|
obsidianPath = `${currentFolderPath}/${filename}`;
|
|
} else if (imageStorageMode === 1) {
|
|
obsidianPath = filename;
|
|
} // mode 2 uses filename only
|
|
|
|
insertAtCursor(contentTextarea, `![[${obsidianPath}]]`);
|
|
renderPreview();
|
|
showNotification('Image pasted and uploaded', 'success');
|
|
} catch (err) {
|
|
showNotification('Paste upload failed: ' + err.message, 'error');
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
function insertAtCursor(textarea, text) {
|
|
const start = textarea.selectionStart;
|
|
const end = textarea.selectionEnd;
|
|
const value = textarea.value;
|
|
textarea.value = value.substring(0, start) + text + value.substring(end);
|
|
const pos = start + text.length;
|
|
textarea.focus();
|
|
textarea.setSelectionRange(pos, pos);
|
|
}
|
|
</script>
|
|
{{end}}
|