Files
my2dos/core/templates/core/index.html
T
2026-09-07 11:40:37 +02:00

353 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'core/base.html' %}
{% load querystring i18n %}
{% block toolbar_filter %}
<div class="toolbar-filter d-md-none">
<div class="filter-panel">
<div class="fw-semibold filter-panel-title">{% trans "Filter" %}</div>
<div class="filter-row">
<a class="btn btn-sm {% if active_kind == 'todo' %}btn-primary{% else %}btn-outline-primary{% endif %}" href="{% if active_kind == 'todo' %}{% query_replace kind=None %}{% else %}{% query_replace kind='todo' %}{% endif %}">Todos{% if active_kind == 'todo' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_kind == 'note' %}btn-secondary{% else %}btn-outline-secondary{% endif %}" href="{% if active_kind == 'note' %}{% query_replace kind=None %}{% else %}{% query_replace kind='note' %}{% endif %}">{% trans "Notes" %}{% if active_kind == 'note' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_kind == 'link' %}btn-success{% else %}btn-outline-success{% endif %}" href="{% if active_kind == 'link' %}{% query_replace kind=None %}{% else %}{% query_replace kind='link' %}{% endif %}">Links{% if active_kind == 'link' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'due' %}btn-danger{% else %}btn-outline-danger{% endif %}" href="{% if active_status == 'due' %}{% query_replace status=None %}{% else %}{% query_replace status='due' %}{% endif %}">{% trans "Due" %} <span class="badge {% if active_status == 'due' %}text-bg-light{% else %}text-bg-danger{% endif %}">{{ due_count }}</span>{% if active_status == 'due' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'archive' %}btn-dark{% else %}btn-outline-dark{% endif %}" href="{% if active_status == 'archive' %}{% query_replace status=None %}{% else %}{% query_replace status='archive' %}{% endif %}">{% trans "Archive" %}{% if active_status == 'archive' %} ×{% endif %}</a>
<input id="mobile-filter-toggle" class="mobile-filter-toggle" type="checkbox">
<label for="mobile-filter-toggle" class="mobile-filter-more" title="Filter bearbeiten">☰{% if q or active_tags %}<span class="badge text-bg-secondary ms-1">!</span>{% endif %}</label>
<div class="mobile-filter-details">
{% if q or active_tags %}<div class="small text-muted mb-2">{% if q %}„{{ q }}”{% endif %}{% for tag_name in active_tags %} #{{ tag_name }}{% endfor %}</div>{% endif %}
<form class="mb-3 position-relative" method="get" x-data="searchBox([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])">
{% for tag_name in active_tags %}<input type="hidden" name="tag" value="{{ tag_name }}">{% endfor %}
{% if active_tag_mode == 'all' %}<input type="hidden" name="tag_mode" value="all">{% endif %}
{% if active_kind %}<input type="hidden" name="kind" value="{{ active_kind }}">{% endif %}
{% if active_status %}<input type="hidden" name="status" value="{{ active_status }}">{% endif %}
<div class="input-group input-group-sm">
<input x-ref="search" class="form-control" name="q" value="{{ q }}" placeholder="{% trans 'Search (Ctrl+F, # for tags)' %}" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" @input="onInput($event)" @keydown="onKeydown($event)">
{% if q %}<a class="btn btn-outline-secondary" href="{% query_replace q=None %}" title="{% trans 'Clear search' %}">×</a>{% endif %}
</div>
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
<template x-for="(tag, idx) in filtered" :key="tag"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="select(tag)"><strong x-text="'#'+tag"></strong></button></template>
</div>
</form>
{% if active_tags|length > 1 %}<div class="btn-group btn-group-sm mb-2"><a class="btn {% if active_tag_mode == 'any' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode=None %}">{% trans "any" %}</a><a class="btn {% if active_tag_mode == 'all' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode='all' %}">{% trans "all" %}</a></div>{% endif %}
<div class="d-flex flex-wrap gap-2">
{% for tag in tags %}{% if tag.name in active_tags %}<a class="badge rounded-pill text-bg-dark text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=None %}">#{{ tag.name }} ×</a>{% else %}<a class="badge rounded-pill text-bg-light text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=tag.name %}">#{{ tag.name }}</a>{% endif %}{% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="mobile-create-nav gap-2 mb-3">
<a class="btn btn-sm btn-primary flex-fill" href="{% url 'new_item' %}?kind=todo&next={{ request.get_full_path|urlencode }}">{% trans "Task" %}</a>
<a class="btn btn-sm btn-secondary flex-fill" href="{% url 'new_item' %}?kind=note&next={{ request.get_full_path|urlencode }}">{% trans "Note" %}</a>
<a class="btn btn-sm btn-success flex-fill" href="{% url 'new_item' %}?kind=link&next={{ request.get_full_path|urlencode }}">Link</a>
<a class="btn btn-sm btn-info flex-fill" href="{% url 'new_item' %}?kind=journal&next={{ request.get_full_path|urlencode }}">Journal</a>
</div>
<div class="row g-4">
<aside class="col-lg-3 order-lg-2 mobile-filter-sidebar">
<div class="filter-panel">
<div class="fw-semibold mb-2 filter-panel-title">{% trans "Filter" %}</div>
<div class="d-flex flex-wrap gap-2 mb-3 filter-buttons">
<a class="btn btn-sm {% if active_kind == 'todo' %}btn-primary{% else %}btn-outline-primary{% endif %}" href="{% if active_kind == 'todo' %}{% query_replace kind=None %}{% else %}{% query_replace kind='todo' %}{% endif %}">Todos{% if active_kind == 'todo' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_kind == 'note' %}btn-secondary{% else %}btn-outline-secondary{% endif %}" href="{% if active_kind == 'note' %}{% query_replace kind=None %}{% else %}{% query_replace kind='note' %}{% endif %}">{% trans "Notes" %}{% if active_kind == 'note' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_kind == 'link' %}btn-success{% else %}btn-outline-success{% endif %}" href="{% if active_kind == 'link' %}{% query_replace kind=None %}{% else %}{% query_replace kind='link' %}{% endif %}">Links{% if active_kind == 'link' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'due' %}btn-danger{% else %}btn-outline-danger{% endif %}" href="{% if active_status == 'due' %}{% query_replace status=None %}{% else %}{% query_replace status='due' %}{% endif %}">{% trans "Due" %} <span class="badge {% if active_status == 'due' %}text-bg-light{% else %}text-bg-danger{% endif %}">{{ due_count }}</span>{% if active_status == 'due' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'archive' %}btn-dark{% else %}btn-outline-dark{% endif %}" href="{% if active_status == 'archive' %}{% query_replace status=None %}{% else %}{% query_replace status='archive' %}{% endif %}">{% trans "Archive" %}{% if active_status == 'archive' %} ×{% endif %}</a>
</div>
<form class="mb-3 position-relative d-none d-md-block" method="get" x-data="searchBox([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])" x-on:keydown.window="if(($event.ctrlKey||$event.metaKey)&&$event.key==='f'){ $event.preventDefault(); $refs.search.focus(); $refs.search.select(); }">
{% for tag_name in active_tags %}<input type="hidden" name="tag" value="{{ tag_name }}">{% endfor %}
{% if active_tag_mode == 'all' %}<input type="hidden" name="tag_mode" value="all">{% endif %}
{% if active_kind %}<input type="hidden" name="kind" value="{{ active_kind }}">{% endif %}
{% if active_status %}<input type="hidden" name="status" value="{{ active_status }}">{% endif %}
<div class="input-group input-group-sm">
<input x-ref="search" class="form-control" name="q" value="{{ q }}" placeholder="{% trans 'Search (Ctrl+F, # for tags)' %}" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" @input="onInput($event)" @keydown="onKeydown($event)">
{% if q %}<a class="btn btn-outline-secondary" href="{% query_replace q=None %}" title="{% trans 'Clear search' %}">×</a>{% endif %}
</div>
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
<template x-for="(tag, idx) in filtered" :key="tag"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="select(tag)"><strong x-text="'#'+tag"></strong></button></template>
</div>
</form>
<div class="d-none d-md-block">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="fw-semibold">{% trans "Tags" %}</div>
{% if active_tags|length > 1 %}
<div class="btn-group btn-group-sm" title="{% trans 'Tag mode' %}">
<a class="btn {% if active_tag_mode == 'any' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode=None %}">{% trans "any" %}</a>
<a class="btn {% if active_tag_mode == 'all' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode='all' %}">{% trans "all" %}</a>
</div>
{% endif %}
</div>
<div hx-get="{% url 'tag_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" hx-trigger="tagsChanged from:body" hx-target="#tag-list" hx-swap="outerHTML">
{% include 'core/_tags.html' %}
</div>
</div>
<details class="d-md-none mobile-filter-more">
<summary class="btn btn-sm btn-outline-secondary" title="Filter bearbeiten">☰{% if q or active_tags %}<span class="badge text-bg-secondary ms-1">!</span>{% endif %}</summary>
<div class="mobile-filter-details">
{% if q or active_tags %}<div class="small text-muted mb-2">{% if q %}„{{ q }}”{% endif %}{% for tag_name in active_tags %} #{{ tag_name }}{% endfor %}</div>{% endif %}
<form class="mb-3 position-relative" method="get" x-data="searchBox([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])">
{% for tag_name in active_tags %}<input type="hidden" name="tag" value="{{ tag_name }}">{% endfor %}
{% if active_tag_mode == 'all' %}<input type="hidden" name="tag_mode" value="all">{% endif %}
{% if active_kind %}<input type="hidden" name="kind" value="{{ active_kind }}">{% endif %}
{% if active_status %}<input type="hidden" name="status" value="{{ active_status }}">{% endif %}
<div class="input-group input-group-sm">
<input x-ref="search" class="form-control" name="q" value="{{ q }}" placeholder="{% trans 'Search (Ctrl+F, # for tags)' %}" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" @input="onInput($event)" @keydown="onKeydown($event)">
{% if q %}<a class="btn btn-outline-secondary" href="{% query_replace q=None %}" title="{% trans 'Clear search' %}">×</a>{% endif %}
</div>
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
<template x-for="(tag, idx) in filtered" :key="tag"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="select(tag)"><strong x-text="'#'+tag"></strong></button></template>
</div>
</form>
{% if active_tags|length > 1 %}<div class="btn-group btn-group-sm mb-2"><a class="btn {% if active_tag_mode == 'any' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode=None %}">{% trans "any" %}</a><a class="btn {% if active_tag_mode == 'all' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode='all' %}">{% trans "all" %}</a></div>{% endif %}
<div class="d-flex flex-wrap gap-2">
{% for tag in tags %}
{% if tag.name in active_tags %}<a class="badge rounded-pill text-bg-dark text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=None %}">#{{ tag.name }} ×</a>{% else %}<a class="badge rounded-pill text-bg-light text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=tag.name %}">#{{ tag.name }}</a>{% endif %}
{% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %}
</div>
</div>
</details>
</div>
</aside>
<section class="col-lg-9">
<form class="desktop-quick-create position-relative mb-4" method="post" enctype="multipart/form-data" hx-post="{{ request.get_full_path }}" hx-target="#items" hx-swap="afterbegin" x-data="quickComposer([{% for tag in composer_tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])" x-on:htmx:after-request="if($event.detail.successful){let kind=$el.querySelector('[name=default_item_kind]')?.value;text='';open=false;$el.reset();if(kind)$el.querySelector('[name=default_item_kind]').value=kind}">
{% csrf_token %}
<fieldset :disabled="editingItem" :class="{'opacity-50': editingItem}">
<div class="small text-muted mb-2" x-show="editingItem" x-cloak>{% trans "Inline editor is open; quick entry is disabled." %}</div>
<div class="mobile-create-buttons gap-2 mb-2">
<button type="button" class="btn btn-sm btn-primary flex-fill" @click="insert('/todo')">{% trans "Task" %}</button>
<button type="button" class="btn btn-sm btn-secondary flex-fill" @click="$el.querySelector('textarea').focus()">{% trans "Note" %}</button>
<button type="button" class="btn btn-sm btn-success flex-fill" @click="insert('/link')">Link</button>
<button type="button" class="btn btn-sm btn-info flex-fill" @click="insert('/journal')">Journal</button>
</div>
<details class="mobile-tag-picker mb-2"><summary class="btn btn-sm btn-outline-secondary">{% trans "Add tags" %}</summary><div class="mobile-tags flex-wrap gap-2 mt-2">{% for tag in composer_tags %}<button type="button" class="btn btn-sm btn-outline-secondary" @click="addToken('#{{ tag.name|escapejs }}')">#{{ tag.name }}</button>{% endfor %}</div></details>
{{ form.content }}
<div class="slash-menu composer-slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><div><div x-show="cmd.group && (idx === 0 || filtered[idx-1].group !== cmd.group)" class="slash-group-header" x-text="cmd.group"></div><button type="button" class="list-group-item list-group-item-action w-100 text-start" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span></button></div></template>
</div>
{% include 'core/_format_toolbar.html' %}
<div class="d-flex align-items-center gap-2 mt-2 quick-actions">
<input class="form-control desktop-file" type="file" name="files" multiple accept="image/*,.pdf,.txt,.md" x-ref="fileInput" @change="previewFile($event)">
<div class="mobile-media gap-2 w-100"><label class="btn btn-outline-secondary flex-fill mb-0">{% trans "Gallery" %}<input class="d-none" type="file" name="files" multiple accept="image/*" @change="previewFile($event)"></label><label class="btn btn-outline-secondary flex-fill mb-0">{% trans "Photo" %}<input class="d-none" type="file" name="files" accept="image/*" capture="environment" @change="previewFile($event)"></label></div>
<a class="btn btn-outline-secondary" href="{% url 'new_item' %}">{% trans "Editor" %}</a>
<button class="btn btn-dark px-4">{% trans "Save" %}</button>
</div>
<template x-if="preview"><div class="mt-2 small"><span class="text-muted">{% trans "Image from clipboard:" %}</span><br><img :src="preview" class="img-thumbnail mt-1" style="max-height:160px"></div></template>
</fieldset>
</form>
<div id="items" class="vstack gap-3">
{% if active_status == 'due' %}
{% for item in due_now_items %}{% include 'core/_item.html' %}{% empty %}<div class="text-center text-muted py-4">{% trans "No due tasks." %}</div>{% endfor %}
{% if upcoming_due_items %}<div class="small text-muted text-uppercase fw-semibold mt-2">{% trans "Due in the next 7 days" %}</div>{% endif %}
{% for item in upcoming_due_items %}{% include 'core/_item.html' %}{% endfor %}
{% else %}
{% for item in items %}{% include 'core/_item.html' %}{% empty %}<div class="text-center text-muted py-5">{% trans "Nothing here yet." %}</div>{% endfor %}
{% endif %}
</div>
</section>
</div>
<script>
function quickComposer(tags, itemId=null){
return {
open:false,
showFormatting:false,
text:'',
active:0,
query:'',
queryStart:0,
currentEl:null,
preview:null,
linkSuggestions:[],
attachmentSuggestions:[],
pendingFiles:[],
editingItem:false,
itemId:itemId,
tags:tags||[],
init(){
document.body.addEventListener('htmx:afterRequest', (event) => {
if(event.detail.successful && event.target === this.$el){ this.preview=null; }
});
document.body.addEventListener('htmx:afterSwap', () => {
this.editingItem=!!document.querySelector('.inline-edit-form');
});
document.body.addEventListener('tagsChanged', () => {
fetch('{% url "api_tags" %}').then(r=>r.json()).then(data=>{this.tags=data.tags||[]});
});
},
commands:[
{token:'/todo',label:'{% trans "Create task" %}',group:'{% trans "Type" %}'},
{token:'/note',label:'{% trans "Create note" %}',group:'{% trans "Type" %}'},
{token:'/link',label:'{% trans "Save link" %}',group:'{% trans "Type" %}'},
{token:'/journal',label:'{% trans "Journal entry" %}',group:'{% trans "Type" %}'},
{token:'/public',label:'{% trans "public" %}',group:'{% trans "Visibility" %}'},
{token:'/private',label:'{% trans "private" %}',group:'{% trans "Visibility" %}'},
{token:'/datum',label:'{% trans "Due date (DD.MM.YY)" %}',group:'{% trans "Date" %}'},
{token:'/morgen',label:'{% trans "Tomorrow" %}',group:'{% trans "Date" %}'},
{token:'/übermorgen',label:'{% trans "Day after tomorrow" %}',group:'{% trans "Date" %}'},
{token:'/nächstewoche',label:'{% trans "Next week" %}',group:'{% trans "Date" %}'},
{token:'/code',label:'{% trans "Insert Markdown code block" %}',group:'{% trans "Formatting" %}'},
{token:'[[',label:'{% trans "Search internal task/note" %}',group:'{% trans "References" %}'},
{token:'![[',label:'{% trans "Insert image from attachments" %}',group:'{% trans "References" %}'}
],
get filtered(){
let q=this.query.toLowerCase();
let selectedType=(this.text.match(/^\/(todo|note|link|journal)(?=\s)/i)||[])[1]?.toLowerCase();
let typeTokens=['/todo','/note','/link','/journal'];
let dateTokens=['/datum','/morgen','/übermorgen','/nächstewoche'];
let commands=this.commands.filter(command=>{
if(typeTokens.includes(command.token)) return !selectedType&&this.queryStart===0;
if(this.queryStart===0&&!selectedType) return false;
if(selectedType&&selectedType!=='todo'&&dateTokens.includes(command.token)) return false;
return true;
});
let list=q.startsWith('#')
? this.tags.filter(t=>('#'+t).toLowerCase().startsWith(q)).map(t=>({token:'#'+t,label:'Tag',group:'{% trans "Tags" %}'}))
: q.startsWith('![[')
? (this.itemId?this.attachmentSuggestions.map(a=>({token:`![[file:${a.id}]]`,label:`{% trans "Image:" %} ${a.name}`,group:'{% trans "References" %}'})):this.pendingFiles.map(f=>({token:`![[paste:${f.name}]]`,label:`Bild: ${f.name}`,group:'{% trans "References" %}'})))
: q.startsWith('[[')
? this.linkSuggestions.map(i=>({token:`[[${i.id}]]`,label:`${i.kind}: ${i.label}`,group:'{% trans "References" %}'}))
: commands.filter(c=>!q||c.token.toLowerCase().startsWith(q));
return list.length?list:[{token:this.query,label:'neu',group:''}];
},
isContentField(el){return el && (el.name==='content' || el.id==='id_content')},
onInput(e){this.update(e.target)},
handlePaste(e){
let item=[...(e.clipboardData?.items||[])].find(i=>i.kind==='file'&&i.type.startsWith('image/'));
if(!item) return;
let file=item.getAsFile();
let ext=(file.type.split('/')[1]||'png').replace('jpeg','jpg');
let named=new File([file], `paste-${new Date().toISOString().replace(/[:.]/g,'-')}.${ext}`, {type:file.type});
let dt=new DataTransfer();
[...(this.$refs.fileInput.files||[])].forEach(f=>dt.items.add(f));
dt.items.add(named);
this.$refs.fileInput.files=dt.files;
this.pendingFiles=[...dt.files].filter(f=>f.type.startsWith('image/'));
this.preview=URL.createObjectURL(named);
},
previewFile(e){
this.pendingFiles=[...e.target.files].filter(f=>f.type.startsWith('image/'));
let file=this.pendingFiles[0];
this.preview=file?URL.createObjectURL(file):null;
},
onKeydown(e){
this.currentEl=e.target;
let shortcut=(e.ctrlKey||e.metaKey)&&{b:'bold',i:'italic',u:'underline'}[e.key.toLowerCase()];
if(shortcut){
e.preventDefault();
this.formatSelection(shortcut);
return;
}
if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){
e.preventDefault();
this.open=false;
e.target.form.requestSubmit();
return;
}
if(this.open&&e.key==='ArrowDown'){
e.preventDefault();
this.active=(this.active+1)%this.filtered.length;
return;
}
if(this.open&&e.key==='ArrowUp'){
e.preventDefault();
this.active=(this.active-1+this.filtered.length)%this.filtered.length;
return;
}
if(this.open&&(e.key==='Enter'||e.key==='Tab')&&!e.shiftKey){
e.preventDefault();
this.insert(this.filtered[this.active].token);
return;
}
if(e.key==='Escape'){
this.open=false;
return;
}
if(e.key===' '){
this.open=false;
return;
}
setTimeout(()=>this.update(e.target),0);
},
update(el){
this.currentEl=el;
if(this.isContentField(el)) this.text=el.value;
let before=el.value.slice(0,el.selectionStart);
let fragment=(before.match(/(^|\s)([^\s]*)$/)||['','',''])[2];
this.query=fragment;
this.queryStart=el.selectionStart-fragment.length;
this.active=0;
this.open=fragment.startsWith('/')||fragment.startsWith('#')||fragment.startsWith('[[')||fragment.startsWith('![[');
if(fragment.startsWith('![[')&&this.itemId){
fetch(`/api/attachment-suggestions/?item=${this.itemId}`).then(r=>r.json()).then(data=>{this.attachmentSuggestions=data.attachments||[]});
}else if(fragment.startsWith('[[')){
let q=fragment.slice(2);
fetch(`{% url "api_item_suggestions" %}?q=${encodeURIComponent(q)}`).then(r=>r.json()).then(data=>{this.linkSuggestions=data.items||[]});
}
},
formatSelection(kind){
let el=this.currentEl&&this.currentEl.matches('textarea')?this.currentEl:this.$el.querySelector('textarea[name="content"],textarea');
if(!el) return;
let start=el.selectionStart??0;
let end=el.selectionEnd??start;
let selected=el.value.slice(start,end);
let wrappers={bold:['**','**','Text'],italic:['*','*','Text'],underline:['<u>','</u>','Text'],code:['`','`','Code']};
let value,newStart,newEnd;
if(wrappers[kind]){
let [before,after,placeholder]=wrappers[kind];
let inner=selected||placeholder;
value=el.value.slice(0,start)+before+inner+after+el.value.slice(end);
newStart=start+before.length;
newEnd=newStart+inner.length;
}else{
let prefixes={heading:'## ',quote:'> ',list:'- ',numbered:'1. '};
let prefix=prefixes[kind]||'';
let inner=selected||'{% trans "Text" %}';
let formatted=inner.split('\n').map(line=>prefix+line).join('\n');
value=el.value.slice(0,start)+formatted+el.value.slice(end);
newStart=start;
newEnd=start+formatted.length;
}
el.value=value;
if(this.isContentField(el)) this.text=value;
this.$nextTick(()=>{el.focus();el.setSelectionRange(newStart,newEnd)});
},
addToken(token){
let el=this.currentEl || document.getElementById('id_content');
if(!el) return;
let pos=el.selectionStart??el.value.length;
let before=el.value.slice(0,pos);
let after=el.value.slice(pos);
let prefix=before&&!before.endsWith(' ')&&!before.endsWith('\n')?' ':'';
let value=before+prefix+token+' '+after;
if(this.isContentField(el)) this.text=value;
el.value=value;
this.showTags=false;
this.$nextTick(()=>{let p=before.length+prefix.length+token.length+1;el.focus();el.setSelectionRange(p,p)})
},
insert(token){
let el=this.currentEl || document.getElementById('id_content');
if(!el) return;
let isCode=token==='/code';
if(isCode) token='```\n\n```';
let pos=el.selectionStart;
let before=el.value.slice(0,pos);
let after=el.value.slice(pos);
let match=before.match(/(^|\s)([^\s]*)$/);
let start=match?pos-match[2].length:pos;
let needsPrefix=start>0&&el.value[start-1]!=='\n'&&el.value[start-1]!==' ';
let prefix=isCode?(start>0&&el.value[start-1]!=='\n'?'\n':''):(needsPrefix?' ':'');
let suffix=isCode?'':' ';
let value=el.value.slice(0,start)+prefix+token+suffix+after;
if(this.isContentField(el)) this.text=value;
el.value=value;
this.open=false;
this.$nextTick(()=>{
let p=isCode?start+prefix.length+4:start+prefix.length+token.length+suffix.length;
el.focus();
el.setSelectionRange(p,p);
});
}
}
}
</script>
{% endblock %}