-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpostmessage.js
232 lines (190 loc) · 10 KB
/
postmessage.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
///////////////////////////////////////////////////////////////////////////////////////
// POST MESSAGE
///////////////////////////////////////////////////////////////////////////////////////
function buildQuoteMessage(messageElement, selection) {
const textArea = document.querySelector('#message_topic');
if (!textArea) return;
const getAuthorFromCitationBtn = (e) => e.querySelector('.bloc-pseudo-msg.text-user').textContent.trim();
const getDateFromCitationBtn = (e) => e.querySelector('.bloc-date-msg').textContent.trim();
const getQuoteHeader = (e) => `> Le ${getDateFromCitationBtn(e)} '''${getAuthorFromCitationBtn(e)}''' a écrit : `;
if (selection?.length) {
const currentContent = textArea.value.length === 0 ? '' : `${textArea.value.trim()}\n\n`;
const quotedText = selection.replaceAll('\n', '\n> ');
textArea.value = `${currentContent}${getQuoteHeader(messageElement)}\n> ${quotedText}\n\n`;
textArea.dispatchEvent(new Event('change'));
textArea.scrollIntoView({ behavior: 'smooth', block: 'center' });
textArea.focus({ preventScroll: true });
textArea.setSelectionRange(textArea.value.length, textArea.value.length);
}
else {
setTimeout(() => {
const date = getDateFromCitationBtn(messageElement);
const regex = new RegExp(`> Le\\s+?${date}\\s+?:`);
textArea.value = textArea.value.replace(regex, getQuoteHeader(messageElement));
}, 600);
}
}
async function suggestAuthors(authorHint) {
// Min 3 char & must be logged in
if (authorHint?.length < 3 || !userPseudo) return undefined;
const url = `/sso/ajax_suggest_pseudo.php?pseudo=${authorHint.trim()}`;
let suggestions = await fetchJson(url);
return suggestions ? [...suggestions.alias.map(s => s.pseudo)] : undefined;
}
function getTextSelection() {
return window.getSelection ? window.getSelection() : document.selection;
}
function getSelectionOffset(container, pointerEvent) {
const selectionRect = getTextSelection().getRangeAt(0).getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const selectionOffsetLeft = selectionRect.left - containerRect.left + selectionRect.width - 9;
const eventOffsetLeft = pointerEvent.pageX - container.offsetLeft - 7;
const offsetLeft = eventOffsetLeft > selectionOffsetLeft ? selectionOffsetLeft : eventOffsetLeft;
const offsetTop = selectionRect.top - containerRect.top + selectionRect.height + 10; // mouseEvent.pageY - container.offsetTop + 14
return { offsetLeft: offsetLeft, offsetTop: offsetTop };
}
function addMessageQuoteEvents(allMessages) {
function buildPartialQuoteButton(message) {
const blocContenu = message.querySelector('.bloc-contenu');
if (!blocContenu) return;
const partialQuoteButton = document.createElement('div');
partialQuoteButton.className = 'deboucled-quote';
partialQuoteButton.innerHTML = '<a class="deboucled-partial-quote-logo"></a>';
blocContenu.appendChild(partialQuoteButton);
return partialQuoteButton;
}
const clearAllQuoteButtons = () => document.querySelectorAll('.deboucled-quote').forEach(e => e.classList.toggle('active', false));
document.onselectionchange = async function () {
await sleep(100);
const selection = getTextSelection().toString();
if (!selection?.length) clearAllQuoteButtons();
};
allMessages.forEach((message) => {
const partialQuoteButton = buildPartialQuoteButton(message); // Partial quote
async function partialQuoteEvent(pointerEvent) {
const selection = getTextSelection().toString();
if (!selection?.length) return;
partialQuoteButton.onclick = () => buildQuoteMessage(message, selection);
const selectionOffset = getSelectionOffset(message, pointerEvent);
partialQuoteButton.style.left = `${selectionOffset.offsetLeft}px`;
partialQuoteButton.style.top = `${selectionOffset.offsetTop}px`;
clearAllQuoteButtons();
await sleep(100);
partialQuoteButton.classList.toggle('active', true);
}
message.onpointerup = (pe) => partialQuoteEvent(pe); // Pointer events = mouse + touch + pen
message.oncontextmenu = (pe) => partialQuoteEvent(pe); // TouchEnd/MouseUp/PointerUp does not fire on mobile (in despite of)
const quoteButton = message.querySelector('.picto-msg-quote');
if (quoteButton) quoteButton.onclick = () => buildQuoteMessage(message); // Full quote
});
}
function addAuthorSuggestionEvent() {
const textArea = document.querySelector('#message_topic');
if (!textArea) return;
const toolbar = document.querySelector('.jv-editor-toolbar');
// Création du container pour les suggestions
const autocompleteElement = document.createElement('div');
autocompleteElement.id = 'deboucled-author-autocomplete';
autocompleteElement.className = 'autocomplete-jv';
autocompleteElement.innerHTML = '<ul class="autocomplete-jv-list"></ul>';
textArea.parentElement.appendChild(autocompleteElement);
// Vide et masque les suggestions
const clearAutocomplete = (elem) => {
elem.innerHTML = '';
elem.parentElement.classList.toggle('on', false);
};
// Choix d'une suggestion dans la liste
function autocompleteOnClick(event) {
let val = textArea.value;
const [bStart, bEnd] = getWordBoundsAtPosition(val, textArea.selectionEnd);
const choosedAuthor = `@${event.target.innerText} `;
textArea.value = `${val.substring(0, bStart)}${choosedAuthor}${val.substring(bEnd, val.length).trim()}`;
clearAutocomplete(this);
textArea.focus();
textArea.selectionStart = bStart + choosedAuthor.length;
textArea.selectionEnd = textArea.selectionStart;
}
// Récupère le mot au curseur
function getWordAtCaret(clearCallback) {
const [bStart, bEnd] = getWordBoundsAtPosition(textArea.value, textArea.selectionEnd);
let wordAtCaret = textArea.value.substring(bStart, bEnd)?.trim();
if (!wordAtCaret?.startsWith('@')) {
clearCallback();
return undefined;
}
wordAtCaret = wordAtCaret.substring(1); // on vire l'arobase pour la recherche
return wordAtCaret;
}
// Efface la sélection de suggestion
function unselectSuggestions(container) {
container.querySelectorAll('.deboucled-author-suggestion.selected')
.forEach(s => s?.classList.toggle('selected', false));
}
// Sélection d'une suggestion
function focusTableChild(element) {
if (!element) return;
element.focus();
element.classList.toggle('selected', true);
}
const getFocusedChild = (table) => table.querySelector('.deboucled-author-suggestion.selected');
// Afficher les suggestions pendant la saisie
textArea.addEventListener('input', async () => {
const autocompleteTable = autocompleteElement.querySelector('.autocomplete-jv-list');
if (!autocompleteTable) return;
autocompleteTable.onclick = autocompleteOnClick;
const clearCallback = () => clearAutocomplete(autocompleteTable);
const wordAtCaret = getWordAtCaret(clearCallback);
if (!wordAtCaret?.length) { clearCallback(); return; }
const suggestions = await suggestAuthors(wordAtCaret);
if (!suggestions?.length) { clearCallback(); return; }
// On construit les éléments de la table avec les suggestions
autocompleteTable.innerHTML = suggestions.map(s => `<li class="deboucled-author-suggestion">${s}</li>`).join('');
autocompleteTable.querySelectorAll('.deboucled-author-suggestion')
.forEach(s => s.onpointerover = () => {
unselectSuggestions(autocompleteTable);
focusTableChild(s);
});
// On place correctement la table
let caret = getCaretCoordinates(textArea, textArea.selectionEnd);
const sLeft = `left:${caret.left + 3}px !important;`;
const sTop = `top:${caret.top + (toolbar ? toolbar.scrollHeight + 15 : 50)}px !important;`;
const sWidth = 'width: auto !important;';
autocompleteElement.style = `${sLeft} ${sTop} ${sWidth}`;
autocompleteElement.classList.toggle('on', true);
if (!getFocusedChild(autocompleteTable)) focusTableChild(autocompleteTable.firstElementChild);
});
// Gestion des flèches haut/bas pour changer la sélection et entrée/tab pour valider
textArea.addEventListener('keydown', (e) => {
if (!autocompleteElement.classList.contains('on')) return;
const autocompleteTable = autocompleteElement.querySelector('.autocomplete-jv-list');
if (!autocompleteTable) return;
let focusedChild = getFocusedChild(autocompleteTable);
switch (e.key) {
case 'Enter':
case 'Tab':
if (focusedChild) focusedChild.click();
e.preventDefault();
break;
case 'ArrowDown':
unselectSuggestions(autocompleteTable);
if (focusedChild) focusTableChild(focusedChild.nextElementSibling);
else focusTableChild(autocompleteTable.firstElementChild);
e.preventDefault();
break;
case 'ArrowUp':
unselectSuggestions(autocompleteTable);
if (focusedChild) focusTableChild(focusedChild.previousElementSibling);
else focusTableChild(autocompleteTable.lastElementChild);
e.preventDefault();
break;
}
});
// Pour changer la sélection de la suggestion au toucher
autocompleteElement.addEventListener('touchmove', (e) => {
if (!autocompleteElement.classList.contains('on')) return;
const autocompleteTable = autocompleteElement.querySelector('.autocomplete-jv-list');
if (!autocompleteTable) return;
unselectSuggestions(autocompleteTable);
focusTableChild(e.target);
});
}