Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/rebuild setup mentions #13

Merged
merged 25 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
152d269
simplify manifest file by using glob patterns for JS and CSS
Mimouss56 Nov 9, 2024
c291a44
chore: update .gitignore to include personal VSCode settings
Mimouss56 Nov 9, 2024
540a9ed
refactor: replace forEach with for-of loops and add type annotations
Mimouss56 Nov 9, 2024
2158d1e
refactor: restructure WWSNB initialization and enhance message checki…
Mimouss56 Nov 10, 2024
e7a937d
Merge branch 'develop' into feature/fix_foreach
Mimouss56 Nov 10, 2024
f86a23b
Merge pull request #1 from Mimouss56/feature/fix_foreach
Mimouss56 Nov 10, 2024
49e29f1
Merge pull request #2 from Mimouss56/feature/fix_initproject
Mimouss56 Nov 10, 2024
ea9fef5
docs: add url of the bbb development server in README
Teyk0o Nov 10, 2024
5a0ce7a
refactor: restructure WWSNB initialization and enhance message checki…
Mimouss56 Nov 10, 2024
9d03baa
Merge branch 'develop' of github.com:Teyk0o/wwsnb into dev
Mimouss56 Nov 10, 2024
eaac316
Merge branch 'dev' of https://github.com/Mimouss56/wwsnb into dev
Mimouss56 Nov 10, 2024
cab7411
feat: implement user caching and utility functions for mentions system
Mimouss56 Nov 10, 2024
e03a1ed
feat: refactor question highlighting and user retrieval logic; add ob…
Mimouss56 Nov 10, 2024
36c1c0e
feat: modularize message handling and enhance moderator message styling
Mimouss56 Nov 10, 2024
7af7d19
feat: export setupReactions function and import user module for enhan…
Mimouss56 Nov 10, 2024
53c3f0f
Merge pull request #3 from Mimouss56/feature/cached_users
Mimouss56 Nov 10, 2024
9c013d1
Merge branch 'develop' of github.com:Teyk0o/wwsnb into dev
Mimouss56 Nov 11, 2024
561febe
Merge branch 'Teyk0o:master' into dev
Mimouss56 Nov 11, 2024
acd5b2e
Merge branch 'develop' of github.com:Teyk0o/wwsnb into dev
Mimouss56 Nov 11, 2024
0262655
Merge branch 'develop' of github.com:Teyk0o/wwsnb into feature/review…
Mimouss56 Nov 11, 2024
8867ece
refactor modo module + apply same structure for question and mention
Mimouss56 Nov 11, 2024
7272d94
Merge branch 'feature/review_moderator'
Mimouss56 Nov 11, 2024
85e954a
Merge pull request #4 from Mimouss56/feature/review_moderator
Mimouss56 Nov 11, 2024
282bb9a
Merge branch 'develop' of github.com:Teyk0o/wwsnb into feature/rebuil…
Mimouss56 Nov 11, 2024
5a43eed
Ajout d'un module de suggestions pour les mentions, incluant la créat…
Mimouss56 Nov 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 1 addition & 301 deletions src/modules/mentions.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
import { getCachedUsers } from "./users/user.module";
import { handleGlobalClick, handleInput, handleKeyDownGlobal } from "./mentions/event.handler";

// Global variables for mentions system
let suggestionsBox = null;
let currentInput = null;
let lastCacheTime = 0;

// Global click handler to close suggestions box when clicking outside
document.addEventListener('click', (e) => {
if (suggestionsBox) {
const isClickInside = suggestionsBox.contains(e.target) || e.target.id === 'message-input';
if (!isClickInside) {
hideSuggestions();
}
}
});

/**
* Initialize the mentions system by setting up event listeners
Expand All @@ -29,291 +16,4 @@ export function setupMentions() {
document.addEventListener('input', handleInput);
document.addEventListener('keydown', handleKeyDownGlobal, true);
document.addEventListener('click', handleGlobalClick, true);

setupInputListener();
}

/**
* Set up the input field listener and handle form submission
*/
function setupInputListener() {
const chatInput = document.getElementById('message-input');
currentInput = chatInput;

if (chatInput && !chatInput.hasAttribute('mention-listener')) {
chatInput.setAttribute('mention-listener', true);

// Prevent form submission when suggestions are open
const form = chatInput.closest('form');
if (form) {
form.addEventListener('submit', (e) => {
if (suggestionsBox) {
e.preventDefault();
e.stopPropagation();
return false;
}
}, true);
}

// Handle send button clicks
const sendButton = document.querySelector('[data-test="sendMessageButton"]');
if (sendButton) {
const originalClickHandler = sendButton.onclick;
sendButton.onclick = (e) => {
if (suggestionsBox) {
e.preventDefault();
e.stopPropagation();
return false;
}
if (originalClickHandler) {
return originalClickHandler.call(sendButton, e);
}
};
}
}
}

/**
* Handle click events on suggestion items
* @param {Event} e Click event
*/
function handleGlobalClick(e) {
if (suggestionsBox) {
const suggestionItem = e.target.closest('.mention-suggestion-item');
if (suggestionItem) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
selectSuggestion(suggestionItem);
return false;
} else if (!suggestionsBox.contains(e.target) && e.target.id !== 'message-input') {
hideSuggestions();
}
}
}

/**
* Handle keyboard events for navigation and selection
* @param {KeyboardEvent} e Keyboard event
*/
function handleKeyDownGlobal(e: KeyboardEvent) {
if (!suggestionsBox) {
return;
}

if (e.target.id === 'message-input') {
if (e.key === 'Enter') {
const selectedItem = suggestionsBox.querySelector('.selected');
if (selectedItem) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
selectSuggestion(selectedItem);
return false;
}
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
navigateSuggestions(e.key === 'ArrowDown' ? 1 : -1);
return false;
} else if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
hideSuggestions();
return false;
}
}
}

/**
* Handle input changes and trigger suggestions display
* @param {Event} e Input event
*/
function handleInput(e: Event) {
if (e.target.id !== 'message-input') {
return;
}

const text = e.target.value;
const cursorPosition = e.target.selectionStart;
const textUpToCursor = text.slice(0, cursorPosition);
const lastAtIndex = textUpToCursor.lastIndexOf('@');

if (lastAtIndex === -1) {
hideSuggestions();
return;
}

const textAfterAt = textUpToCursor.slice(lastAtIndex + 1);
if (textAfterAt.includes(' ')) {
hideSuggestions();
return;
}

// Show all suggestions if just after @
if (lastAtIndex === cursorPosition - 1) {
searchAndShowSuggestions('', e.target, lastAtIndex);
return;
}

searchAndShowSuggestions(textAfterAt, e.target, lastAtIndex);
}

/**
* Search users and display suggestions
* @param {string} query Search query
* @param {HTMLElement} inputElement Input element
* @param {number} atIndex Position of @
*/
async function searchAndShowSuggestions(query: string, inputElement: HTMLElement, atIndex: number) {
try {
const users = getCachedUsers();
const matches = users.filter(user =>
user.name.toLowerCase().startsWith(query.toLowerCase())
);

if (matches.length > 0) {
showSuggestions(matches, inputElement, atIndex);
} else {
hideSuggestions();
}
} catch (error) {
console.error('Error fetching users:', error);
hideSuggestions();
}
}

/**
* Display suggestions box with matched users
* @param {Array} users Matched users
* @param {HTMLElement} inputElement Input element
* @param {number} atIndex Position of @
*/
function showSuggestions(users: [], inputElement: HTMLElement, atIndex: number) {
hideSuggestions();

suggestionsBox = document.createElement('div');
suggestionsBox.className = 'mention-suggestions';

users.forEach((user, index) => {
const item = document.createElement('div');
item.className = 'mention-suggestion-item';
if (index === 0) {
item.classList.add('selected');
}

const avatar = document.createElement('div');
avatar.className = 'mention-avatar';
avatar.style.backgroundColor = user.bgColor;
avatar.textContent = user.initials;

const name = document.createElement('span');
name.textContent = user.name;

item.appendChild(avatar);
item.appendChild(name);

item.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
selectSuggestion(item);
});

item.addEventListener('mouseover', () => {
const selected = suggestionsBox.querySelector('.selected');
if (selected) {
selected.classList.remove('selected');
}
item.classList.add('selected');
});

suggestionsBox.appendChild(item);
});

const inputRect = inputElement.getBoundingClientRect();
suggestionsBox.style.position = 'fixed';
suggestionsBox.style.left = `${inputRect.left}px`;
suggestionsBox.style.width = `${inputRect.width}px`;
suggestionsBox.style.zIndex = '9999';

document.body.appendChild(suggestionsBox);

// Position the suggestions box based on available space
const boxHeight = suggestionsBox.offsetHeight;
const viewportHeight = window.innerHeight;
const spaceBelow = viewportHeight - inputRect.bottom;

if (spaceBelow < boxHeight && inputRect.top > boxHeight) {
suggestionsBox.style.top = `${inputRect.top - boxHeight - 5}px`;
} else {
suggestionsBox.style.top = `${inputRect.bottom + 5}px`;
}
}

/**
* Hide suggestions box
*/
function hideSuggestions() {
if (suggestionsBox) {
suggestionsBox.remove();
suggestionsBox = null;
}
}

/**
* Navigate through suggestions using keyboard
* @param {number} direction 1 for down, -1 for up
*/
function navigateSuggestions(direction: number) {
const items = suggestionsBox.querySelectorAll('.mention-suggestion-item');
const currentIndex = Array.from(items).findIndex(item => item.classList.contains('selected'));
items[currentIndex].classList.remove('selected');

let newIndex = currentIndex + direction;
if (newIndex < 0) {
newIndex = items.length - 1;
}
if (newIndex >= items.length) {
newIndex = 0;
}

items[newIndex].classList.add('selected');
items[newIndex].scrollIntoView({ block: 'nearest' });
}

/**
* Select a suggestion and insert it into the input
* @param {HTMLElement} item Selected suggestion item
*/
function selectSuggestion(item: HTMLElement) {
if (!currentInput || !item) {
return;
}

const text = currentInput.value;
const cursorPos = currentInput.selectionStart;
const textUpToCursor = text.slice(0, cursorPos);
const lastAtIndex = textUpToCursor.lastIndexOf('@');

if (lastAtIndex === -1) {
return;
}

const username = item.querySelector('span').textContent;
if (!username) {
return;
}

const beforeMention = text.slice(0, lastAtIndex);
const afterMention = text.slice(cursorPos);

// Add space after username if not already present
const newText = `${beforeMention}@${username}${afterMention.startsWith(' ') ? '' : ' '}${afterMention}`;
const newCursorPos = lastAtIndex + username.length + 2;

currentInput.value = newText;
currentInput.setSelectionRange(newCursorPos, newCursorPos);

hideSuggestions();
currentInput.focus();
}
68 changes: 68 additions & 0 deletions src/modules/mentions/event.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import suggestionBoxElem from "../suggestion/suggestionbox.element";
import { hideSuggestions, searchAndShowSuggestions, selectSuggestion } from "../suggestion/suggestionBox.module";

/**
* Handle keydown events on the input field
* @param {KeyboardEvent} e Keydown event
*/
export function handleKeyDownGlobal(e: KeyboardEvent): void {
if (e.key === 'Escape') {
hideSuggestions();
}
}

/**
* Handle input changes and trigger suggestions display
* @param {Event} e Input event
*/
export function handleInput(e: Event) {
const target = e.target as HTMLInputElement;

if (target.id !== 'message-input') return;

const text = target.value;
const cursorPosition = target.selectionStart ?? 0;
const textUpToCursor = text.slice(0, cursorPosition);
const lastAtIndex = textUpToCursor.lastIndexOf('@');

if (lastAtIndex === -1) {
hideSuggestions();
return;
}

const textAfterAt = textUpToCursor.slice(lastAtIndex + 1);
if (textAfterAt.includes(' ')) {
hideSuggestions();
return;
}

// Show all suggestions if just after @
if (lastAtIndex === cursorPosition - 1) {
searchAndShowSuggestions('', target, lastAtIndex);
return;
}

searchAndShowSuggestions(textAfterAt, target, lastAtIndex);
}


/**
* Handle click events on suggestion items
* @param {MouseEvent} e Click event
*/
export function handleGlobalClick(e: MouseEvent) {
if (suggestionBoxElem) {
const target = e.target as HTMLElement;

const suggestionItem = target.closest('.mention-suggestion-item');
if (suggestionItem) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
selectSuggestion(suggestionItem as HTMLElement, target as HTMLInputElement);
} else if (!suggestionBoxElem.contains(target) && target.id !== 'message-input') {
hideSuggestions();
}

}
}
Loading
Loading