diff --git a/static/js/mention-autocomplete.js b/static/js/mention-autocomplete.js
new file mode 100644
index 0000000..655e6ff
--- /dev/null
+++ b/static/js/mention-autocomplete.js
@@ -0,0 +1,194 @@
+(function() {
+ 'use strict';
+
+ function nameToMention(name) {
+ return (name || '')
+ .trim()
+ .toLowerCase()
+ .replace(/\s+/g, '.')
+ .replace(/[^\wąćęłńóśźż.\-]/g, '');
+ }
+
+ function createDropdown() {
+ const el = document.createElement('div');
+ el.className = 'mention-autocomplete';
+ el.style.cssText = 'position:absolute;z-index:1500;background:var(--surface,#fff);border:1px solid var(--border,#ddd);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.15);max-height:280px;overflow-y:auto;min-width:260px;display:none;';
+ document.body.appendChild(el);
+ return el;
+ }
+
+ function getCaretCoords(textarea) {
+ const div = document.createElement('div');
+ const style = getComputedStyle(textarea);
+ ['fontFamily','fontSize','fontWeight','letterSpacing','lineHeight','padding','border','boxSizing','whiteSpace','wordWrap','width'].forEach(p => {
+ div.style[p] = style[p];
+ });
+ div.style.position = 'absolute';
+ div.style.visibility = 'hidden';
+ div.style.whiteSpace = 'pre-wrap';
+ div.style.wordWrap = 'break-word';
+ const value = textarea.value.substring(0, textarea.selectionStart);
+ div.textContent = value;
+ const span = document.createElement('span');
+ span.textContent = '\u200b';
+ div.appendChild(span);
+ document.body.appendChild(div);
+ const rect = textarea.getBoundingClientRect();
+ const spanRect = span.getBoundingClientRect();
+ const divRect = div.getBoundingClientRect();
+ const top = rect.top + window.scrollY + (spanRect.top - divRect.top) - textarea.scrollTop + parseInt(style.fontSize, 10) + 4;
+ const left = rect.left + window.scrollX + (spanRect.left - divRect.left) - textarea.scrollLeft;
+ document.body.removeChild(div);
+ return { top: top, left: left };
+ }
+
+ let dropdown = null;
+ let activeTextarea = null;
+ let activeIndex = 0;
+ let currentResults = [];
+ let mentionStart = -1;
+ let searchTimer = null;
+
+ function hideDropdown() {
+ if (dropdown) dropdown.style.display = 'none';
+ currentResults = [];
+ mentionStart = -1;
+ }
+
+ function renderDropdown(results) {
+ if (!dropdown) dropdown = createDropdown();
+ if (!results.length) {
+ dropdown.innerHTML = '
Brak wyników
';
+ dropdown.style.display = 'block';
+ return;
+ }
+ dropdown.innerHTML = results.map((u, i) => {
+ const avatar = u.avatar_url
+ ? `
`
+ : `${(u.name || '?')[0].toUpperCase()}
`;
+ const company = u.company_name ? `${escapeHtml(u.company_name)}
` : '';
+ return `
+ ${avatar}
+
+
${escapeHtml(u.name)}
+ ${company}
+
+
`;
+ }).join('');
+ dropdown.style.display = 'block';
+ Array.from(dropdown.querySelectorAll('.mention-item')).forEach(item => {
+ item.addEventListener('mousedown', function(e) {
+ e.preventDefault();
+ selectUser(parseInt(this.dataset.idx, 10));
+ });
+ item.addEventListener('mouseenter', function() {
+ activeIndex = parseInt(this.dataset.idx, 10);
+ updateActiveItem();
+ });
+ });
+ }
+
+ function escapeHtml(s) {
+ return (s || '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+ }
+
+ function updateActiveItem() {
+ if (!dropdown) return;
+ Array.from(dropdown.querySelectorAll('.mention-item')).forEach((item, i) => {
+ item.style.background = i === activeIndex ? 'var(--primary-light,#eef)' : '';
+ });
+ }
+
+ function selectUser(idx) {
+ if (!activeTextarea || !currentResults[idx]) return;
+ const user = currentResults[idx];
+ const mention = '@' + nameToMention(user.name) + ' ';
+ const textarea = activeTextarea;
+ const before = textarea.value.substring(0, mentionStart);
+ const after = textarea.value.substring(textarea.selectionStart);
+ textarea.value = before + mention + after;
+ const newCaret = before.length + mention.length;
+ textarea.setSelectionRange(newCaret, newCaret);
+ textarea.focus();
+ hideDropdown();
+ }
+
+ function search(query) {
+ clearTimeout(searchTimer);
+ searchTimer = setTimeout(() => {
+ fetch('/api/users/search?q=' + encodeURIComponent(query), { credentials: 'same-origin' })
+ .then(r => r.json())
+ .then(data => {
+ currentResults = Array.isArray(data) ? data.slice(0, 8) : [];
+ activeIndex = 0;
+ if (!activeTextarea || mentionStart < 0) return;
+ const coords = getCaretCoords(activeTextarea);
+ if (!dropdown) dropdown = createDropdown();
+ dropdown.style.top = coords.top + 'px';
+ dropdown.style.left = coords.left + 'px';
+ renderDropdown(currentResults);
+ })
+ .catch(() => hideDropdown());
+ }, 150);
+ }
+
+ function detectMention(textarea) {
+ const pos = textarea.selectionStart;
+ const text = textarea.value.substring(0, pos);
+ const match = text.match(/(?:^|\s)@([\wąćęłńóśźż.\-]{0,30})$/i);
+ if (!match) {
+ hideDropdown();
+ return;
+ }
+ const query = match[1];
+ mentionStart = pos - query.length - 1;
+ activeTextarea = textarea;
+ if (query.length < 1) {
+ hideDropdown();
+ return;
+ }
+ search(query);
+ }
+
+ window.attachMentionAutocomplete = function(textarea) {
+ if (!textarea || textarea.dataset.mentionAttached === '1') return;
+ textarea.dataset.mentionAttached = '1';
+
+ textarea.addEventListener('input', () => detectMention(textarea));
+ textarea.addEventListener('keyup', e => {
+ if (['ArrowUp','ArrowDown','Enter','Escape','Tab'].indexOf(e.key) === -1) detectMention(textarea);
+ });
+ textarea.addEventListener('keydown', e => {
+ if (!dropdown || dropdown.style.display === 'none' || !currentResults.length) return;
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ activeIndex = (activeIndex + 1) % currentResults.length;
+ updateActiveItem();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ activeIndex = (activeIndex - 1 + currentResults.length) % currentResults.length;
+ updateActiveItem();
+ } else if (e.key === 'Enter' || e.key === 'Tab') {
+ e.preventDefault();
+ selectUser(activeIndex);
+ } else if (e.key === 'Escape') {
+ hideDropdown();
+ }
+ });
+ textarea.addEventListener('blur', () => setTimeout(hideDropdown, 150));
+ };
+
+ window.insertMentionTrigger = function(textarea) {
+ if (!textarea) return;
+ textarea.focus();
+ const pos = textarea.selectionStart;
+ const before = textarea.value.substring(0, pos);
+ const after = textarea.value.substring(pos);
+ const needsSpace = before.length > 0 && !/\s$/.test(before);
+ const insert = (needsSpace ? ' ' : '') + '@';
+ textarea.value = before + insert + after;
+ const newPos = pos + insert.length;
+ textarea.setSelectionRange(newPos, newPos);
+ textarea.dispatchEvent(new Event('input'));
+ };
+})();
diff --git a/templates/forum/new_topic.html b/templates/forum/new_topic.html
index 1020a95..cb0fa1b 100755
--- a/templates/forum/new_topic.html
+++ b/templates/forum/new_topic.html
@@ -290,6 +290,9 @@
+
+
+
- Minimum 10 znaków. Im więcej szczegółów, tym lepsze odpowiedzi.
+ Minimum 10 znaków. Wpisz @ aby wspomnieć użytkownika.
+