PluginProbe
Social Media Auto Poster – Schedule & Publish to Buffer / 6.2.5
Social Media Auto Poster – Schedule & Publish to Buffer v6.2.5
6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.2 6.1.1 6.1.0 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 All 126 releases
wp-to-buffer / lib / shared / js / autocomplete-tinymce.js

autocomplete-tinymce.js in Social Media Auto Poster – Schedule & Publish to Buffer 6.2.5, at lib/shared/js/autocomplete-tinymce.js

521 lines 17.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Initialises Keyword Autocomplete for TinyMCE instances.
3 *
4 * @package WPZincDashboardWidget
5 * @author WP Zinc
6 */
7
8 ( function () {
9
10 // Define key binds.
11 const DOWN_ARROW_KEY = 40;
12 const UP_ARROW_KEY = 38;
13 const ESC_KEY = 27;
14 const ENTER_KEY = 13;
15 const BACKSPACE = 8;
16
17 // Define key binds to ignore.
18 var keyBindsToIgnore = [
19 DOWN_ARROW_KEY,
20 UP_ARROW_KEY,
21 ESC_KEY,
22 ENTER_KEY
23 ];
24
25 // Define triggers as character codes.
26 var textToOpenAutoComplete = [
27 // Changing this to 219 i.e. left curly brace wipes an entire paragraph, no idea why.
28 '123'
29 ];
30
31 // Iterate through autocompleters.
32 if ( typeof wpzinc_autocomplete !== 'undefined' ) {
33
34 wpzinc_autocomplete.forEach(
35 function ( autocompleter, i ) {
36 autocompleter.triggers.forEach(
37 function ( trigger, j ) {
38
39 // Skip remote autocompleters.
40 if ( 'url' in trigger ) {
41 return;
42 }
43
44 // Create TinyMCE Plugin for this Autocompleter.
45 tinymce.create(
46 'tinymce.plugins.' + trigger.tinyMCEName,
47 {
48
49 init: function ( editor ) {
50
51 var autoCompleteDisplayed = false,
52 autoCompleteContainer = createAutoComplete();
53
54 /**
55 * Creates an unordered list comprising of all keywords that can be
56 * searched and displayed as a list.
57 *
58 * @since 2.0.2
59 */
60 function createAutoComplete() {
61
62 // Define <ul>.
63 var ul = document.createElement( 'ul' );
64 ul.setAttribute( 'class', 'wpzinc-tinymce-autocomplete' );
65
66 // Define <li>'s, appending to <ul>.
67 trigger.values.forEach(
68 function ( value, key ) {
69 var li = document.createElement( 'li' );
70 li.classList.add( 'displayed' );
71 ul.appendChild( li );
72 li.innerHTML = li.innerHTML + value.value;
73 }
74 );
75
76 // Append <ul> to body.
77 document.body.appendChild( ul );
78
79 // Return <ul>.
80 return ul;
81
82 }
83
84 /**
85 * Shows the autocomplete list.
86 *
87 * @since 2.0.2
88 *
89 * @param tinymce editor Editor Object.
90 * @param object autoCompleteContainer Autocomplete <ul> DOM Element.
91 */
92 function showAutoComplete( editor, autoCompleteContainer ) {
93
94 // Get caret position, so we can determine precisely where to show the autocomplete list.
95 var caretPosition = getCaretPosition( editor );
96
97 // Position autocomplete.
98 positionAutoComplete( autoCompleteContainer, caretPosition.top, caretPosition.left );
99
100 // Display autocomplete.
101 autoCompleteContainer.classList.remove( 'displayed' );
102 autoCompleteContainer.classList.add( 'displayed' );
103
104 // Set flag that we're displaying autocomplete.
105 autoCompleteDisplayed = true;
106
107 }
108
109 /**
110 * Get the top and left position of the caret in the active editor, relative
111 * to the browser window.
112 *
113 * @since 2.0.2
114 *
115 * @param tinymce editor Editor Object.
116 * @return object Top and Left Position of Caret
117 */
118 function getCaretPosition( editor ) {
119
120 // Get the editor's container and toolbar elements.
121 var editorContainer = ( editor.getContainer() ? editor.getContainer() : document.getElementById( editor.id ) );
122
123 // Get the editor position within the browser window (top + left), and.
124 var editorPositionWithinWindow = {
125 top: editorContainer.getBoundingClientRect().top + window.scrollY,
126 left: editorContainer.getBoundingClientRect().left + window.scrollX
127 };
128
129 var caretPositionWithinEditor = {
130 top: 0,
131 left: 0
132 };
133
134 // Get the caret position within the editor.
135 if ( editor.selection.getRng().getClientRects().length > 0 ) {
136 caretPositionWithinEditor = {
137 top: editor.selection.getRng().getClientRects()[0].top + 20,
138 left: editor.selection.getRng().getClientRects()[0].left
139 };
140 } else {
141 caretPositionWithinEditor = {
142 top: editor.selection.getNode().getClientRects()[0].top + 20,
143 left: editor.selection.getNode().getClientRects()[0].left
144 };
145 }
146
147 // Get toolbar.
148 var editorToolbar = editorContainer.getElementsByClassName( 'mce-toolbar-grp' )[0];
149
150 // Return position directly on and below the caret, factoring the toolbar position if the toolbar exists.
151 if ( editorToolbar ) {
152 return {
153 top: editorPositionWithinWindow.top + editorToolbar.getBoundingClientRect().height + caretPositionWithinEditor.top,
154 left: editorPositionWithinWindow.left + caretPositionWithinEditor.left
155 }
156 }
157
158 return caretPositionWithinEditor;
159
160 }
161
162 /**
163 * Positions the autocomplete list to the given top and left position,
164 * relative to the browser window.
165 * The autocomplete list's CSS must define position:absolute.
166 *
167 * @since 2.0.2
168 *
169 * @param object autoCompleteContainer Autocomplete <ul> DOM Element.
170 * @param int top Top Position, in pixels.
171 * @param int left Left Position, in pixels.
172 */
173 function positionAutoComplete( autoCompleteContainer, top, left ) {
174
175 autoCompleteContainer.style.marginTop = top + 'px';
176 autoCompleteContainer.style.marginLeft = left + 'px';
177
178 }
179
180 /**
181 * Fetches the search word, from the last occurance of the opening trigger relative
182 * to the current caret / cursor position within the editor..
183 *
184 * @since 2.0.2
185 *
186 * @param tinymce editor Editor Object.
187 * @return string Search Word, including opening trigger
188 */
189 function getSearchWord( editor ) {
190
191 // Get the text from the editor, and the current caret position.
192 var text = ( editor.selection.getSel().focusNode == null ? "" : editor.selection.getSel().focusNode.nodeValue ),
193 caretPosition = editor.selection.getSel().focusOffset,
194 startPosition = 0;
195
196 if ( text == null || text.length == 0 ) {
197 return '';
198 }
199
200 // Go backwards from the current carat position to the start of the editor text, until we find
201 // a trigger that opened the autocomplete list.
202 for ( var i = caretPosition; i >= 0; i-- ) {
203 // If this character matches a trigger that opened the autocomplete list, note the start
204 // position of it and break the loop.
205 if ( textToOpenAutoComplete.indexOf( text.charCodeAt( i ).toString() ) != -1 ) {
206 startPosition = i;
207 break;
208 }
209 }
210
211 // Extract the search from the text between the opening trigger and the current caret position.
212 var search = text.substr( startPosition, caretPosition - startPosition );
213
214 // Return.
215 return {
216 search: search,
217 start: startPosition,
218 end: caretPosition
219 }
220
221 }
222
223 /**
224 * Filters the autocomplete list based on the given search input.
225 *
226 * @since 2.0.2
227 *
228 * @param tinymce editor Editor Object.
229 * @param object autoCompleteContainer Autocomplete <ul> DOM Element.
230 */
231 function filterAutoComplete( search, editor, autoCompleteContainer ) {
232
233 // Iterate through list, showing / hiding options based on whether they match
234 // the given search text.
235 var items = autoCompleteContainer.getElementsByTagName( 'li' ),
236 firstItem = true,
237 length = items.length;
238 for ( var i = 0; i < ( length - 1 ); i++ ) {
239 // Remove higlight class.
240 items.item( i ).classList.remove( 'highlight' );
241
242 if ( items.item( i ).innerText.indexOf( search.search ) == -1 ) {
243 // Hide.
244 items.item( i ).classList.remove( 'displayed' );
245 } else {
246 // Show.
247 items.item( i ).classList.add( 'displayed' );
248
249 // Highlight if this is the first autocomplete suggestion.
250 if ( firstItem ) {
251 items.item( i ).classList.add( 'highlight' );
252 firstItem = false;
253 }
254 }
255 }
256
257 }
258
259 /**
260 * Highlights the next or previous displayed list item in the autocomplete list.
261 *
262 * @since 3.5.7
263 *
264 * @param string direction Item to higlight (next|previous).
265 * @param tinymce editor Editor Object.
266 * @param object autoCompleteContainer Autocomplete <ul> DOM Element.
267 */
268 function changeAutoCompleteItemHighlighted( direction, editor, autoCompleteContainer ) {
269
270 // Iterate through list of displayed items.
271 var items = autoCompleteContainer.querySelectorAll( 'li.displayed' ),
272 length = items.length;
273 for ( var i = 0; i < ( length - 1 ); i++ ) {
274 // Skip if this item isn't highlighted.
275 if ( ! items[ i ].classList.contains( 'highlight' ) ) {
276 continue;
277 }
278
279 // Depending on the direction, add the highlight class to the previous or next item in the list.
280 if ( direction == 'previous' ) {
281 // If the first item is highlighted, do nothing, as we cannot highlight a previous item.
282 if ( i == 0 ) {
283 break;
284 }
285
286 // Change item that has highlight class.
287 items[ i ].classList.remove( 'highlight' );
288 items[ i - 1 ].classList.add( 'highlight' );
289 break;
290 }
291
292 if ( direction == 'next' ) {
293 // If the last item is highlighted, do nothing, as we cannot highlight a next item.
294 if ( i == ( items.length - 1 ) ) {
295 break;
296 }
297
298 // Change item that has highlight class.
299 items[ i ].classList.remove( 'highlight' );
300 items[ i + 1 ].classList.add( 'highlight' );
301 break;
302 }
303 }
304
305 }
306
307 /**
308 * Insert the highlighted autocomplete suggestion from the list into the editor content.
309 *
310 * @since 2.0.2
311 *
312 * @param tinymce editor Editor Object.
313 * @param object autoCompleteContainer Autocomplete <ul> DOM Element.
314 */
315 function insertAutoCompleteItemHighlighted( editor, autoCompleteContainer ) {
316
317 // Fetch highlighted autocomplete suggestion.
318 var text = autoCompleteContainer.querySelectorAll( 'li.highlight' )[0].innerText;
319
320 // Insert the text into the editor.
321 insertAutoCompleteItemByText( text, editor, autoCompleteContainer );
322
323 }
324
325 /**
326 * Insert the text into the editor content.
327 *
328 * @since 2.0.2
329 *
330 * @param string text Text.
331 * @param tinymce editor Editor Object.
332 */
333 function insertAutoCompleteItemByText( text, editor ) {
334
335 // Get search word, editor text and range.
336 var search = getSearchWord( editor ),
337 editorText = editor.selection.getSel().focusNode,
338 editorRange = editor.selection.getRng();
339
340 // Select the search word.
341 editorRange.setStart( editorText, search.start );
342 editorRange.setEnd( editorText, search.end );
343 editor.selection.setRng( editorRange );
344
345 // Insert autocomplete text into editor.
346 // This replaces the selected search word.
347 editor.selection.setContent( text );
348
349 }
350
351 /**
352 * Hides the autocomplete list, resetting anything within the list.
353 *
354 * @since 2.0.2
355 *
356 * @param tinymce editor Editor Object.
357 * @param object autoCompleteContainer Autocomplete <ul> DOM Element.
358 */
359 function hideAutoComplete( editor, autoCompleteContainer ) {
360
361 // Hide autocomplete.
362 autoCompleteContainer.classList.remove( 'displayed' );
363
364 // Set flag that we're not displaying autocomplete.
365 autoCompleteDisplayed = false;
366
367 }
368
369 /**
370 * Shows or hides the autocomplete list, depending on whether the user presses
371 * a character
372 *
373 * @since 2.0.2
374 *
375 * @param tinymce editor Editor Instance.
376 * @param object event Event .
377 */
378 function keyDownEvent( editor, event ) {
379
380 // If the user types a character that will trigger opening autocomplete, and we're not
381 // displaying autocomplete, show it now.
382 if ( trigger.triggerKeyCode == event.keyCode && ! autoCompleteDisplayed ) {
383 if ( trigger.triggerKeyShiftRequired ) {
384 if ( event.shiftKey ) {
385 showAutoComplete( editor, autoCompleteContainer );
386 return;
387 }
388 } else {
389 showAutoComplete( editor, autoCompleteContainer );
390 return;
391 }
392 }
393
394 // If the user types a character that will close the autocomplete, and we're
395 // displaying autocomplete, hide it now.
396 if ( ESC_KEY == event.keyCode && autoCompleteDisplayed ) {
397 hideAutoComplete( editor, autoCompleteContainer );
398 return;
399 }
400
401 // If autocomplete is displayed and the user pressed the up or down key, change the highlighted result
402 // in the list.
403 if ( autoCompleteDisplayed && ( UP_ARROW_KEY == event.keyCode || DOWN_ARROW_KEY == event.keyCode ) ) {
404 var direction = ( UP_ARROW_KEY == event.keyCode ? 'previous' : 'next' );
405 changeAutoCompleteItemHighlighted( direction, editor, autoCompleteContainer );
406 }
407
408 // If autocomplete is displayed and the user presses enter, add the first displayed result in the list
409 // to the editor, and close the autocomplete list.
410 if ( autoCompleteDisplayed && ENTER_KEY == event.keyCode ) {
411 // Prevent the enter event propagating to the editor instance.
412 tinymce.dom.Event.cancel( event );
413
414 // Insert the highlighted autocomplete item into the editor.
415 insertAutoCompleteItemHighlighted( editor, autoCompleteContainer );
416
417 // Hide the autocomplete list.
418 hideAutoComplete( editor, autoCompleteContainer );
419 return;
420 }
421
422 }
423
424 /**
425 * Filters the autocomplete list, if displayed, and the user types
426 *
427 * @since 2.0.2
428 *
429 * @param tinymce editor Editor Instance.
430 * @param object event Event .
431 */
432 function keyUpEvent( editor, event ) {
433
434 // If autocomplete is displayed and the user hasn't typed a character that we're ignoring,
435 // filter the results list.
436 if ( keyBindsToIgnore.indexOf( event.keyCode ) == -1 && autoCompleteDisplayed ) {
437 // Reposition the autocomplete list.
438 showAutoComplete( editor, autoCompleteContainer );
439
440 // Get search word from the opening trigger to the caret.
441 var search = getSearchWord( editor );
442
443 // Filter the autocomplete list based on the search term.
444 filterAutoComplete( search, editor, autoCompleteContainer );
445 }
446
447 }
448
449 /**
450 * Listens to all click events in the editor instance.
451 *
452 * @since 2.0.2
453 *
454 * @param tinymce editor Editor Instance.
455 * @param object event Event .
456 */
457 function clickEventInEditor( editor, event ) {
458
459 // Hide the autocomplete list.
460 hideAutoComplete( editor, autoCompleteContainer );
461
462 }
463
464 /**
465 * Listens to all click events in the DOM, outside of the editor instance.
466 *
467 * @since 2.0.2
468 *
469 * @param object event Event .
470 */
471 function clickEvent( event ) {
472
473 // Hide the autocomplete list if the user clicked outside of it.
474 if ( ! event.target.matches( 'li.displayed' ) ) {
475 hideAutoComplete( editor, autoCompleteContainer );
476 return;
477 }
478
479 // If here, the user clicked an autocomplete list item.
480 // Insert that item's text into the editor.
481 insertAutoCompleteItemByText( event.target.innerText, editor );
482
483 // Hide the autocomplete list.
484 hideAutoComplete( editor, autoCompleteContainer );
485
486 }
487
488 // Bind events to TinyMCE Editor instance.
489 editor.onKeyDown.add( keyUpEvent );
490 editor.onKeyDown.add( keyDownEvent );
491 editor.onClick.add( clickEventInEditor );
492
493 // Bind events to DOM (outside editor).
494 document.addEventListener( 'click', clickEvent );
495
496 },
497
498 getInfo: function () {
499 return {
500 longname: 'Autocomplete',
501 author: 'WP Zinc',
502 version: tinymce.majorVersion + '.' + tinymce.minorVersion
503 };
504 }
505
506 }
507 );
508
509 // Add TinyMCE Plugin.
510 tinymce.PluginManager.add( trigger.tinyMCEName, tinymce.plugins[ trigger.tinyMCEName ] );
511
512 }
513 );
514
515 }
516 );
517
518 }
519
520 } )();
521