PluginProbe
Friends / 4.2.1
Friends v4.2.1
4.3.2 4.3.1 4.3.0 4.2.2 4.2.1 4.2.0 4.1.0 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.1 All 88 releases
friends / templates / mastodon / mastodon.js

mastodon.js in Friends 4.2.1, at templates/mastodon/mastodon.js

517 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Mastodon theme JS for the Friends plugin.
3 *
4 * Keyboard shortcuts (compact mode):
5 * j/k - Select next/previous item (expands it)
6 * n/p - Select next/previous item without opening
7 * space - Page down, or next item if at bottom of current
8 * shift-space - Page up
9 * o/enter - Toggle open/close current item
10 * s - Toggle star reaction
11 * v - Open original in new tab
12 * cmd-k/ctrl-k - Focus search
13 * ? - Show keyboard shortcuts help
14 */
15 ( function( $ ) {
16 var currentIndex = -1;
17
18 function getItems() {
19 return $( 'section.posts article.card' );
20 }
21
22 function isCompactMode() {
23 return $( 'section.posts' ).hasClass( 'all-collapsed' );
24 }
25
26 function scrollToItem( $item ) {
27 if ( ! $item.length ) {
28 return;
29 }
30 $( 'html, body' ).animate( {
31 scrollTop: $item.offset().top - 80
32 }, 150 );
33 }
34
35 function expandItem( $item ) {
36 if ( ! $item.hasClass( 'uncollapsed' ) ) {
37 $item.addClass( 'uncollapsed' );
38 }
39 }
40
41 function collapseItem( $item ) {
42 $item.removeClass( 'uncollapsed' );
43 }
44
45 function collapseAll() {
46 getItems().removeClass( 'uncollapsed' );
47 }
48
49 function selectItem( index, expand ) {
50 var items = getItems();
51 if ( index < 0 || index >= items.length ) {
52 return;
53 }
54
55 if ( expand ) {
56 collapseAll();
57 }
58
59 currentIndex = index;
60 var $item = items.eq( index );
61
62 items.removeClass( 'mast-current' );
63 $item.addClass( 'mast-current' );
64 scrollToItem( $item );
65
66 if ( expand ) {
67 expandItem( $item );
68 }
69 }
70
71 function isItemBottomVisible( $item ) {
72 var itemBottom = $item.offset().top + $item.outerHeight();
73 var viewBottom = $( window ).scrollTop() + $( window ).height();
74 return itemBottom <= viewBottom;
75 }
76
77 // Wrap multiple images in a gallery grid.
78 document.querySelectorAll( '.posts .card-body' ).forEach( function( body ) {
79 var images = body.querySelectorAll( ':scope > .wp-block-image, :scope > p > img' );
80 if ( images.length < 2 ) {
81 return;
82 }
83
84 var gallery = document.createElement( 'div' );
85 gallery.className = 'friends-gallery gallery-' + Math.min( images.length, 4 );
86
87 var imgElements = [];
88 images.forEach( function( el ) {
89 var img = el.tagName === 'IMG' ? el : el.querySelector( 'img' );
90 if ( img ) {
91 imgElements.push( { img: img, wrapper: el.tagName === 'IMG' ? el.parentNode : el } );
92 }
93 } );
94
95 if ( imgElements.length < 2 ) {
96 return;
97 }
98
99 // Insert gallery before the first image wrapper.
100 imgElements[0].wrapper.parentNode.insertBefore( gallery, imgElements[0].wrapper );
101
102 imgElements.forEach( function( item ) {
103 gallery.appendChild( item.img.cloneNode( true ) );
104 // Remove the original wrapper if it's now empty or just contains the image.
105 if ( item.wrapper.parentNode ) {
106 item.wrapper.parentNode.removeChild( item.wrapper );
107 }
108 } );
109 } );
110
111 // Toggle compact/expanded mode.
112 $( document ).on( 'click', 'a.toggle-compact', function() {
113 $( 'section.posts' ).toggleClass( 'all-collapsed' );
114 if ( $( 'section.posts' ).is( '.all-collapsed' ) ) {
115 $( 'a.toggle-compact' ).text( friends.text_expanded_mode );
116 // Collapse all when switching to compact.
117 getItems().removeClass( 'uncollapsed' );
118 } else {
119 $( 'a.toggle-compact' ).text( friends.text_compact_mode );
120 }
121 return false;
122 } );
123
124 function toggleCard( card ) {
125 var items = getItems();
126 var index = items.index( card );
127 var $card = $( card );
128
129 currentIndex = index;
130 items.removeClass( 'mast-current' );
131 $card.addClass( 'mast-current' );
132
133 // Collapse all others.
134 items.not( card ).removeClass( 'uncollapsed' );
135
136 // Toggle this one.
137 $card.toggleClass( 'uncollapsed' );
138 }
139
140 // Block link clicks on collapsed items — expand instead.
141 $( document ).on( 'click', 'section.posts.all-collapsed article.card:not(.uncollapsed) .post-meta a, section.posts.all-collapsed article.card:not(.uncollapsed) .card-title a', function( e ) {
142 e.preventDefault();
143 e.stopPropagation();
144 toggleCard( $( this ).closest( 'article.card' )[0] );
145 return false;
146 } );
147
148 // Accordion click: collapse all others, toggle clicked item.
149 $( document ).on( 'click', 'section.posts.all-collapsed article.card', function( e ) {
150 if ( $( e.target ).closest( 'a, button, input, textarea, form, label, .friends-dropdown' ).length ) {
151 return;
152 }
153
154 toggleCard( this );
155
156 e.stopPropagation();
157 return false;
158 } );
159
160 // Keyboard shortcuts.
161 $( document ).on( 'keydown', function( e ) {
162 // cmd-k / ctrl-k: focus search input (works in any context).
163 if ( ( e.metaKey || e.ctrlKey ) && 75 === e.keyCode ) {
164 e.preventDefault();
165 $( '.mastodon-search-input' ).first().focus();
166 return false;
167 }
168
169 // Escape: blur search input.
170 if ( 27 === e.keyCode && $( e.target ).is( '.mastodon-search-input' ) ) {
171 $( e.target ).blur();
172 return false;
173 }
174
175 if ( $( e.target ).is( 'input, textarea, select, [contenteditable]' ) ) {
176 return;
177 }
178
179 var items = getItems();
180 if ( ! items.length && e.key !== '?' ) {
181 return;
182 }
183
184 // In expanded mode, only handle '?' for help.
185 if ( ! isCompactMode() && e.key !== '?' ) {
186 return;
187 }
188
189 switch ( e.key ) {
190 case 'j': // Next item, expand.
191 e.preventDefault();
192 selectItem( currentIndex + 1, true );
193 break;
194
195 case 'k': // Previous item, expand.
196 e.preventDefault();
197 if ( currentIndex > 0 ) {
198 selectItem( currentIndex - 1, true );
199 }
200 break;
201
202 case 'n': // Next item, don't expand.
203 e.preventDefault();
204 selectItem( currentIndex + 1, false );
205 break;
206
207 case 'p': // Previous item, don't expand.
208 e.preventDefault();
209 if ( currentIndex > 0 ) {
210 selectItem( currentIndex - 1, false );
211 }
212 break;
213
214 case ' ': // Space: page down or next item.
215 e.preventDefault();
216 if ( e.shiftKey ) {
217 // Page up.
218 window.scrollBy( 0, -$( window ).height() * 0.8 );
219 } else if ( currentIndex >= 0 && items.eq( currentIndex ).hasClass( 'uncollapsed' ) && ! isItemBottomVisible( items.eq( currentIndex ) ) ) {
220 // Page down within current expanded item.
221 window.scrollBy( 0, $( window ).height() * 0.8 );
222 } else {
223 // Move to next item.
224 selectItem( currentIndex + 1, true );
225 }
226 break;
227
228 case 'o': // Toggle current item.
229 case 'Enter':
230 if ( currentIndex >= 0 ) {
231 e.preventDefault();
232 var $item = items.eq( currentIndex );
233 if ( $item.hasClass( 'uncollapsed' ) ) {
234 collapseItem( $item );
235 } else {
236 collapseAll();
237 expandItem( $item );
238 }
239 }
240 break;
241
242 case 's': // Star: toggle reaction on current post.
243 if ( currentIndex >= 0 ) {
244 e.preventDefault();
245 var $star = items.eq( currentIndex ).find( 'button.friends-reaction[data-emoji="2b50"], button.friends-reaction-picker[data-emoji="2b50"]' ).first();
246 if ( $star.length ) {
247 $star.trigger( 'click' );
248 }
249 }
250 break;
251
252 case 'v': // Open original in new tab.
253 if ( currentIndex >= 0 ) {
254 e.preventDefault();
255 var $link = items.eq( currentIndex ).find( '.card-title a, h4 a' ).first();
256 if ( $link.length ) {
257 window.open( $link.attr( 'href' ), '_blank' );
258 }
259 }
260 break;
261
262 case '?': // Show shortcuts help.
263 e.preventDefault();
264 var $help = $( '#mast-shortcuts-help' );
265 if ( $help.length ) {
266 $help.toggle();
267 } else {
268 $( 'body' ).append(
269 '<div id="mast-shortcuts-help" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:light-dark(#d9e1e8,#282c37);border:1px solid light-dark(#c8d0db,#393f4f);border-radius:8px;padding:20px;z-index:10000;max-width:500px;font-size:13px;box-shadow:0 4px 12px rgba(0,0,0,.3)">' +
270 '<h3 style="margin:0 0 12px;font-size:16px">Keyboard Shortcuts</h3>' +
271 '<table style="width:100%;border-collapse:collapse">' +
272 '<tr><td style="padding:2px 12px 2px 0"><b>j / k</b></td><td>Next / previous item (expand)</td></tr>' +
273 '<tr><td style="padding:2px 12px 2px 0"><b>n / p</b></td><td>Next / previous without opening</td></tr>' +
274 '<tr><td style="padding:2px 12px 2px 0"><b>space</b></td><td>Page down or next item</td></tr>' +
275 '<tr><td style="padding:2px 12px 2px 0"><b>shift-space</b></td><td>Page up</td></tr>' +
276 '<tr><td style="padding:2px 12px 2px 0"><b>o / enter</b></td><td>Open / close item</td></tr>' +
277 '<tr><td style="padding:2px 12px 2px 0"><b>s</b></td><td>Toggle \u2b50 reaction</td></tr>' +
278 '<tr><td style="padding:2px 12px 2px 0"><b>v</b></td><td>Open original in new tab</td></tr>' +
279 '<tr><td style="padding:2px 12px 2px 0"><b>\u2318K / Ctrl-K</b></td><td>Search</td></tr>' +
280 '<tr><td style="padding:2px 12px 2px 0"><b>?</b></td><td>Show this help</td></tr>' +
281 '</table>' +
282 '<p style="margin:12px 0 0;color:light-dark(#606984,#4a4e69);font-size:11px">Press any key to close</p>' +
283 '</div>'
284 );
285 // Close on any key or click.
286 $( document ).one( 'keydown click', function() {
287 $( '#mast-shortcuts-help' ).remove();
288 } );
289 }
290 return; // Don't let the one() handler fire immediately.
291 }
292 } );
293 } )( jQuery );
294
295 // @ mention autocomplete in the compose box.
296 ( function( $ ) {
297 var nonce = ( typeof friendsMastodon !== 'undefined' ) ? friendsMastodon.mentionNonce : '';
298 var $dropdown = null;
299 var currentTextarea = null;
300 var mentionStart = -1;
301 var activeIndex = -1;
302 var lastFetchedQuery = null;
303 var allResults = [];
304 var debounceTimer = null;
305 var noResultsTimer = null;
306
307 function getDropdown() {
308 if ( ! $dropdown ) {
309 $dropdown = $( '<ul class="mastodon-mention-dropdown"></ul>' ).hide().appendTo( 'body' );
310 }
311 return $dropdown;
312 }
313
314 function closeDropdown() {
315 clearTimeout( noResultsTimer );
316 if ( $dropdown ) {
317 $dropdown.hide();
318 }
319 mentionStart = -1;
320 activeIndex = -1;
321 lastFetchedQuery = null;
322 allResults = [];
323 currentTextarea = null;
324 clearTimeout( debounceTimer );
325 }
326
327 function getMentionContext( textarea ) {
328 var text = textarea.value.substring( 0, textarea.selectionStart );
329 var match = text.match( /@(\w*)$/ );
330 if ( ! match ) {
331 return null;
332 }
333 return { query: match[ 1 ], start: text.length - match[ 0 ].length };
334 }
335
336 function positionDropdown( textarea ) {
337 var $form = $( textarea ).closest( '.mastodon-compose-form' );
338 var offset = $form.offset();
339 getDropdown().css( {
340 top: offset.top + $form.outerHeight(),
341 left: offset.left,
342 width: $form.outerWidth()
343 } );
344 }
345
346 function setActive( index ) {
347 var $items = getDropdown().find( 'li:not(.mastodon-mention-no-results)' );
348 $items.removeClass( 'active' );
349 if ( ! $items.length ) {
350 activeIndex = -1;
351 return;
352 }
353 activeIndex = Math.max( 0, Math.min( index, $items.length - 1 ) );
354 $items.eq( activeIndex ).addClass( 'active' );
355 }
356
357 function acceptActive() {
358 if ( activeIndex < 0 ) {
359 return false;
360 }
361 var $item = $dropdown.find( 'li:not(.mastodon-mention-no-results)' ).eq( activeIndex ).find( 'a' );
362 if ( ! $item.length ) {
363 return false;
364 }
365 $item.trigger( 'mousedown' );
366 return true;
367 }
368
369 function buildItem( item ) {
370 var $li = $( '<li></li>' );
371 var $a = $( '<a href="#"></a>' );
372 if ( item.avatar ) {
373 $a.append( $( '<img>' ).attr( { src: item.avatar, width: 24, height: 24 } ) );
374 }
375 $a.append( $( '<span class="mastodon-mention-name"></span>' ).text( item.display_name ) );
376 $a.append( $( '<span class="mastodon-mention-handle"></span>' ).text( '@' + item.handle ) );
377 $a.on( 'mousedown', function( e ) {
378 e.preventDefault();
379 if ( ! currentTextarea ) {
380 return;
381 }
382 var value = currentTextarea.value;
383 var cursor = currentTextarea.selectionStart;
384 var mention = '@' + item.handle + ' ';
385 currentTextarea.value = value.substring( 0, mentionStart ) + mention + value.substring( cursor );
386 var pos = mentionStart + mention.length;
387 currentTextarea.setSelectionRange( pos, pos );
388 var ta = currentTextarea;
389 closeDropdown();
390 ta.focus();
391 } );
392 $li.append( $a );
393 return $li;
394 }
395
396 function renderResults( results, textarea ) {
397 clearTimeout( noResultsTimer );
398 var $dd = getDropdown();
399 $dd.empty();
400 if ( results.length ) {
401 results.forEach( function( item ) {
402 $dd.append( buildItem( item ) );
403 } );
404 positionDropdown( textarea );
405 $dd.show();
406 activeIndex = -1;
407 } else {
408 $dd.append( '<li class="mastodon-mention-no-results"><span>No results</span></li>' );
409 positionDropdown( textarea );
410 $dd.show();
411 activeIndex = -1;
412 noResultsTimer = setTimeout( closeDropdown, 500 );
413 }
414 }
415
416 function filterAndRender( query, textarea ) {
417 var q = query.toLowerCase();
418 var filtered = allResults.filter( function( item ) {
419 return item.display_name.toLowerCase().indexOf( q ) !== -1 ||
420 item.handle.toLowerCase().indexOf( q ) !== -1;
421 } );
422 renderResults( filtered, textarea );
423 }
424
425 $( document ).on( 'input', '.mastodon-compose-form textarea', function() {
426 var textarea = this;
427 var ctx = getMentionContext( textarea );
428
429 if ( ! ctx ) {
430 closeDropdown();
431 return;
432 }
433
434 currentTextarea = textarea;
435 mentionStart = ctx.start;
436
437 // Client-side filter of already-loaded results while debounce is pending.
438 if ( allResults.length ) {
439 filterAndRender( ctx.query, textarea );
440 }
441
442 clearTimeout( debounceTimer );
443
444 // Only fetch from server when the query extends beyond what we already fetched.
445 if ( lastFetchedQuery !== null && ctx.query.indexOf( lastFetchedQuery ) === 0 ) {
446 return;
447 }
448
449 debounceTimer = setTimeout( function() {
450 var query = ctx.query;
451 wp.ajax.send( 'friends-mention-autocomplete', {
452 data: { _ajax_nonce: nonce, q: query },
453 success: function( results ) {
454 allResults = results || [];
455 lastFetchedQuery = query;
456
457 // Re-read current context in case user typed more since the request fired.
458 var currentCtx = getMentionContext( textarea );
459 if ( currentCtx ) {
460 filterAndRender( currentCtx.query, textarea );
461 } else {
462 closeDropdown();
463 }
464 },
465 error: function() {
466 closeDropdown();
467 }
468 } );
469 }, 200 );
470 } );
471
472 $( document ).on( 'keydown', '.mastodon-compose-form textarea', function( e ) {
473 if ( ! $dropdown || ! $dropdown.is( ':visible' ) ) {
474 return;
475 }
476
477 var count = $dropdown.find( 'li:not(.mastodon-mention-no-results)' ).length;
478
479 if ( 40 === e.keyCode ) { // Down arrow.
480 e.preventDefault();
481 setActive( activeIndex < 0 ? 0 : ( activeIndex + 1 ) % count );
482 } else if ( 38 === e.keyCode ) { // Up arrow.
483 e.preventDefault();
484 setActive( activeIndex < 0 ? count - 1 : ( activeIndex - 1 + count ) % count );
485 } else if ( 9 === e.keyCode ) { // Tab — accept top item if nothing active, else accept active.
486 if ( count ) {
487 e.preventDefault();
488 if ( activeIndex < 0 ) {
489 setActive( 0 );
490 }
491 acceptActive();
492 }
493 } else if ( 13 === e.keyCode && activeIndex >= 0 ) { // Enter.
494 e.preventDefault();
495 acceptActive();
496 } else if ( 27 === e.keyCode ) { // Escape.
497 closeDropdown();
498 }
499 } );
500
501 $( document ).on( 'blur', '.mastodon-compose-form textarea', function() {
502 setTimeout( function() {
503 if ( $dropdown && $dropdown.is( ':visible' ) && ! $dropdown.find( ':focus' ).length ) {
504 closeDropdown();
505 }
506 }, 150 );
507 } );
508
509 $( document ).on( 'click', function( e ) {
510 if ( $dropdown && $dropdown.is( ':visible' ) ) {
511 if ( ! $( e.target ).closest( '.mastodon-compose-form, .mastodon-mention-dropdown' ).length ) {
512 closeDropdown();
513 }
514 }
515 } );
516 } )( jQuery );
517