Implement extension registration from an extension.json file
[mediawiki.git] / resources / src / jquery / jquery.textSelection.js
blobbd6518ddf180c1c93170f63479cfaf12a55cfb4b
1 /**
2  * These plugins provide extra functionality for interaction with textareas.
3  */
4 ( function ( $ ) {
5         if ( document.selection && document.selection.createRange ) {
6                 // On IE, patch the focus() method to restore the windows' scroll position
7                 // (bug 32241)
8                 $.fn.extend( {
9                         focus: ( function ( jqFocus ) {
10                                 return function () {
11                                         var $w, state, result;
12                                         if ( arguments.length === 0 ) {
13                                                 $w = $( window );
14                                                 state = { top: $w.scrollTop(), left: $w.scrollLeft() };
15                                                 result = jqFocus.apply( this, arguments );
16                                                 window.scrollTo( state.top, state.left );
17                                                 return result;
18                                         }
19                                         return jqFocus.apply( this, arguments );
20                                 };
21                         }( $.fn.focus ) )
22                 } );
23         }
25         $.fn.textSelection = function ( command, options ) {
26                 var fn,
27                         alternateFn,
28                         context,
29                         hasWikiEditor,
30                         needSave,
31                         retval;
33                 /**
34                  * Helper function to get an IE TextRange object for an element
35                  */
36                 function rangeForElementIE( e ) {
37                         if ( e.nodeName.toLowerCase() === 'input' ) {
38                                 return e.createTextRange();
39                         } else {
40                                 var sel = document.body.createTextRange();
41                                 sel.moveToElementText( e );
42                                 return sel;
43                         }
44                 }
46                 /**
47                  * Helper function for IE for activating the textarea. Called only in the
48                  * IE-specific code paths below; makes use of IE-specific non-standard
49                  * function setActive() if possible to avoid screen flicker.
50                  */
51                 function activateElementOnIE( element ) {
52                         if ( element.setActive ) {
53                                 element.setActive(); // bug 32241: doesn't scroll
54                         } else {
55                                 $( element ).focus(); // may scroll (but we patched it above)
56                         }
57                 }
59                 fn = {
60                         /**
61                          * Get the contents of the textarea
62                          */
63                         getContents: function () {
64                                 return this.val();
65                         },
66                         /**
67                          * Set the contents of the textarea, replacing anything that was there before
68                          */
69                         setContents: function ( content ) {
70                                 this.val( content );
71                         },
72                         /**
73                          * Get the currently selected text in this textarea. Will focus the textarea
74                          * in some browsers (IE/Opera)
75                          */
76                         getSelection: function () {
77                                 var retval, range,
78                                         el = this.get( 0 );
80                                 if ( !el || $( el ).is( ':hidden' ) ) {
81                                         retval = '';
82                                 } else if ( document.selection && document.selection.createRange ) {
83                                         activateElementOnIE( el );
84                                         range = document.selection.createRange();
85                                         retval = range.text;
86                                 } else if ( el.selectionStart || el.selectionStart === 0 ) {
87                                         retval = el.value.substring( el.selectionStart, el.selectionEnd );
88                                 }
90                                 return retval;
91                         },
92                         /**
93                          * Ported from skins/common/edit.js by Trevor Parscal
94                          * (c) 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
95                          *
96                          * Inserts text at the beginning and end of a text selection, optionally
97                          * inserting text at the caret when selection is empty.
98                          *
99                          * @fixme document the options parameters
100                          */
101                         encapsulateSelection: function ( options ) {
102                                 return this.each( function () {
103                                         var selText, scrollTop, insertText,
104                                                 isSample, range, range2, range3, startPos, endPos,
105                                                 pre = options.pre,
106                                                 post = options.post;
108                                         /**
109                                          * Check if the selected text is the same as the insert text
110                                          */
111                                         function checkSelectedText() {
112                                                 if ( !selText ) {
113                                                         selText = options.peri;
114                                                         isSample = true;
115                                                 } else if ( options.replace ) {
116                                                         selText = options.peri;
117                                                 } else {
118                                                         while ( selText.charAt( selText.length - 1 ) === ' ' ) {
119                                                                 // Exclude ending space char
120                                                                 selText = selText.slice( 0, -1 );
121                                                                 post += ' ';
122                                                         }
123                                                         while ( selText.charAt( 0 ) === ' ' ) {
124                                                                 // Exclude prepending space char
125                                                                 selText = selText.slice( 1 );
126                                                                 pre = ' ' + pre;
127                                                         }
128                                                 }
129                                         }
131                                         /**
132                                          * Do the splitlines stuff.
133                                          *
134                                          * Wrap each line of the selected text with pre and post
135                                          */
136                                         function doSplitLines( selText, pre, post ) {
137                                                 var i,
138                                                         insertText = '',
139                                                         selTextArr = selText.split( '\n' );
140                                                 for ( i = 0; i < selTextArr.length; i++ ) {
141                                                         insertText += pre + selTextArr[i] + post;
142                                                         if ( i !== selTextArr.length - 1 ) {
143                                                                 insertText += '\n';
144                                                         }
145                                                 }
146                                                 return insertText;
147                                         }
149                                         isSample = false;
150                                         // Do nothing if display none
151                                         if ( this.style.display !== 'none' ) {
152                                                 if ( document.selection && document.selection.createRange ) {
153                                                         // IE
155                                                         // Note that IE9 will trigger the next section unless we check this first.
156                                                         // See bug 35201.
158                                                         activateElementOnIE( this );
159                                                         if ( context ) {
160                                                                 context.fn.restoreCursorAndScrollTop();
161                                                         }
162                                                         if ( options.selectionStart !== undefined ) {
163                                                                 $( this ).textSelection( 'setSelection', { 'start': options.selectionStart, 'end': options.selectionEnd } );
164                                                         }
166                                                         selText = $( this ).textSelection( 'getSelection' );
167                                                         scrollTop = this.scrollTop;
168                                                         range = document.selection.createRange();
170                                                         checkSelectedText();
171                                                         insertText = pre + selText + post;
172                                                         if ( options.splitlines ) {
173                                                                 insertText = doSplitLines( selText, pre, post );
174                                                         }
175                                                         if ( options.ownline && range.moveStart ) {
176                                                                 range2 = document.selection.createRange();
177                                                                 range2.collapse();
178                                                                 range2.moveStart( 'character', -1 );
179                                                                 // FIXME: Which check is correct?
180                                                                 if ( range2.text !== '\r' && range2.text !== '\n' && range2.text !== '' ) {
181                                                                         insertText = '\n' + insertText;
182                                                                         pre += '\n';
183                                                                 }
184                                                                 range3 = document.selection.createRange();
185                                                                 range3.collapse( false );
186                                                                 range3.moveEnd( 'character', 1 );
187                                                                 if ( range3.text !== '\r' && range3.text !== '\n' && range3.text !== '' ) {
188                                                                         insertText += '\n';
189                                                                         post += '\n';
190                                                                 }
191                                                         }
193                                                         range.text = insertText;
194                                                         if ( isSample && options.selectPeri && range.moveStart ) {
195                                                                 range.moveStart( 'character', -post.length - selText.length );
196                                                                 range.moveEnd( 'character', -post.length );
197                                                         }
198                                                         range.select();
199                                                         // Restore the scroll position
200                                                         this.scrollTop = scrollTop;
201                                                 } else if ( this.selectionStart || this.selectionStart === 0 ) {
202                                                         // Mozilla/Opera
204                                                         $( this ).focus();
205                                                         if ( options.selectionStart !== undefined ) {
206                                                                 $( this ).textSelection( 'setSelection', { 'start': options.selectionStart, 'end': options.selectionEnd } );
207                                                         }
209                                                         selText = $( this ).textSelection( 'getSelection' );
210                                                         startPos = this.selectionStart;
211                                                         endPos = this.selectionEnd;
212                                                         scrollTop = this.scrollTop;
213                                                         checkSelectedText();
214                                                         if ( options.selectionStart !== undefined
215                                                                         && endPos - startPos !== options.selectionEnd - options.selectionStart )
216                                                         {
217                                                                 // This means there is a difference in the selection range returned by browser and what we passed.
218                                                                 // This happens for Chrome in the case of composite characters. Ref bug #30130
219                                                                 // Set the startPos to the correct position.
220                                                                 startPos = options.selectionStart;
221                                                         }
223                                                         insertText = pre + selText + post;
224                                                         if ( options.splitlines ) {
225                                                                 insertText = doSplitLines( selText, pre, post );
226                                                         }
227                                                         if ( options.ownline ) {
228                                                                 if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== '\n' && this.value.charAt( startPos - 1 ) !== '\r' ) {
229                                                                         insertText = '\n' + insertText;
230                                                                         pre += '\n';
231                                                                 }
232                                                                 if ( this.value.charAt( endPos ) !== '\n' && this.value.charAt( endPos ) !== '\r' ) {
233                                                                         insertText += '\n';
234                                                                         post += '\n';
235                                                                 }
236                                                         }
237                                                         this.value = this.value.slice( 0, startPos ) + insertText +
238                                                                 this.value.slice( endPos );
239                                                         // Setting this.value scrolls the textarea to the top, restore the scroll position
240                                                         this.scrollTop = scrollTop;
241                                                         if ( window.opera ) {
242                                                                 pre = pre.replace( /\r?\n/g, '\r\n' );
243                                                                 selText = selText.replace( /\r?\n/g, '\r\n' );
244                                                                 post = post.replace( /\r?\n/g, '\r\n' );
245                                                         }
246                                                         if ( isSample && options.selectPeri && ( !options.splitlines || ( options.splitlines && selText.indexOf( '\n' ) === -1 ) ) ) {
247                                                                 this.selectionStart = startPos + pre.length;
248                                                                 this.selectionEnd = startPos + pre.length + selText.length;
249                                                         } else {
250                                                                 this.selectionStart = startPos + insertText.length;
251                                                                 this.selectionEnd = this.selectionStart;
252                                                         }
253                                                 }
254                                         }
255                                         $( this ).trigger( 'encapsulateSelection', [ options.pre, options.peri, options.post, options.ownline,
256                                                 options.replace, options.spitlines ] );
257                                 } );
258                         },
259                         /**
260                          * Ported from Wikia's LinkSuggest extension
261                          * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
262                          * Some code copied from
263                          * http://www.dedestruct.com/2008/03/22/howto-cross-browser-cursor-position-in-textareas/
264                          *
265                          * Get the position (in resolution of bytes not necessarily characters)
266                          * in a textarea
267                          *
268                          * Will focus the textarea in some browsers (IE/Opera)
269                          *
270                          * @fixme document the options parameters
271                          */
272                         getCaretPosition: function ( options ) {
273                                 function getCaret( e ) {
274                                         var caretPos = 0,
275                                                 endPos = 0,
276                                                 preText, rawPreText, periText,
277                                                 rawPeriText, postText, rawPostText,
278                                                 // IE Support
279                                                 preFinished,
280                                                 periFinished,
281                                                 postFinished,
282                                                 // Range containing text in the selection
283                                                 periRange,
284                                                 // Range containing text before the selection
285                                                 preRange,
286                                                 // Range containing text after the selection
287                                                 postRange;
289                                         if ( e && document.selection && document.selection.createRange ) {
290                                                 // IE doesn't properly report non-selected caret position through
291                                                 // the selection ranges when textarea isn't focused. This can
292                                                 // lead to saving a bogus empty selection, which then screws up
293                                                 // whatever we do later (bug 31847).
294                                                 activateElementOnIE( e );
296                                                 preFinished = false;
297                                                 periFinished = false;
298                                                 postFinished = false;
299                                                 periRange = document.selection.createRange().duplicate();
301                                                 preRange = rangeForElementIE( e );
302                                                 // Move the end where we need it
303                                                 preRange.setEndPoint( 'EndToStart', periRange );
305                                                 postRange = rangeForElementIE( e );
306                                                 // Move the start where we need it
307                                                 postRange.setEndPoint( 'StartToEnd', periRange );
309                                                 // Load the text values we need to compare
310                                                 preText = rawPreText = preRange.text;
311                                                 periText = rawPeriText = periRange.text;
312                                                 postText = rawPostText = postRange.text;
314                                                 /*
315                                                  * Check each range for trimmed newlines by shrinking the range by 1
316                                                  * character and seeing if the text property has changed. If it has
317                                                  * not changed then we know that IE has trimmed a \r\n from the end.
318                                                  */
319                                                 do {
320                                                         if ( !preFinished ) {
321                                                                 if ( preRange.compareEndPoints( 'StartToEnd', preRange ) === 0 ) {
322                                                                         preFinished = true;
323                                                                 } else {
324                                                                         preRange.moveEnd( 'character', -1 );
325                                                                         if ( preRange.text === preText ) {
326                                                                                 rawPreText += '\r\n';
327                                                                         } else {
328                                                                                 preFinished = true;
329                                                                         }
330                                                                 }
331                                                         }
332                                                         if ( !periFinished ) {
333                                                                 if ( periRange.compareEndPoints( 'StartToEnd', periRange ) === 0 ) {
334                                                                         periFinished = true;
335                                                                 } else {
336                                                                         periRange.moveEnd( 'character', -1 );
337                                                                         if ( periRange.text === periText ) {
338                                                                                 rawPeriText += '\r\n';
339                                                                         } else {
340                                                                                 periFinished = true;
341                                                                         }
342                                                                 }
343                                                         }
344                                                         if ( !postFinished ) {
345                                                                 if ( postRange.compareEndPoints( 'StartToEnd', postRange ) === 0 ) {
346                                                                         postFinished = true;
347                                                                 } else {
348                                                                         postRange.moveEnd( 'character', -1 );
349                                                                         if ( postRange.text === postText ) {
350                                                                                 rawPostText += '\r\n';
351                                                                         } else {
352                                                                                 postFinished = true;
353                                                                         }
354                                                                 }
355                                                         }
356                                                 } while ( ( !preFinished || !periFinished || !postFinished ) );
357                                                 caretPos = rawPreText.replace( /\r\n/g, '\n' ).length;
358                                                 endPos = caretPos + rawPeriText.replace( /\r\n/g, '\n' ).length;
359                                         } else if ( e && ( e.selectionStart || e.selectionStart === 0 ) ) {
360                                                 // Firefox support
361                                                 caretPos = e.selectionStart;
362                                                 endPos = e.selectionEnd;
363                                         }
364                                         return options.startAndEnd ? [ caretPos, endPos ] : caretPos;
365                                 }
366                                 return getCaret( this.get( 0 ) );
367                         },
368                         /**
369                          * @fixme document the options parameters
370                          */
371                         setSelection: function ( options ) {
372                                 return this.each( function () {
373                                         var selection, length, newLines;
374                                         // Do nothing if hidden
375                                         if ( !$( this ).is( ':hidden' ) ) {
376                                                 if ( this.selectionStart || this.selectionStart === 0 ) {
377                                                         // Opera 9.0 doesn't allow setting selectionStart past
378                                                         // selectionEnd; any attempts to do that will be ignored
379                                                         // Make sure to set them in the right order
380                                                         if ( options.start > this.selectionEnd ) {
381                                                                 this.selectionEnd = options.end;
382                                                                 this.selectionStart = options.start;
383                                                         } else {
384                                                                 this.selectionStart = options.start;
385                                                                 this.selectionEnd = options.end;
386                                                         }
387                                                 } else if ( document.body.createTextRange ) {
388                                                         selection = rangeForElementIE( this );
389                                                         length = this.value.length;
390                                                         // IE doesn't count \n when computing the offset, so we won't either
391                                                         newLines = this.value.match( /\n/g );
392                                                         if ( newLines ) {
393                                                                 length = length - newLines.length;
394                                                         }
395                                                         selection.moveStart( 'character', options.start );
396                                                         selection.moveEnd( 'character', -length + options.end );
398                                                         // This line can cause an error under certain circumstances (textarea empty, no selection)
399                                                         // Silence that error
400                                                         try {
401                                                                 selection.select();
402                                                         } catch ( e ) { }
403                                                 }
404                                         }
405                                 } );
406                         },
407                         /**
408                          * Ported from Wikia's LinkSuggest extension
409                          * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
410                          *
411                          * Scroll a textarea to the current cursor position. You can set the cursor
412                          * position with setSelection()
413                          * @param options boolean Whether to force a scroll even if the caret position
414                          *  is already visible. Defaults to false
415                          *
416                          * @fixme document the options parameters (function body suggests options.force is a boolean, not options itself)
417                          */
418                         scrollToCaretPosition: function ( options ) {
419                                 function getLineLength( e ) {
420                                         return Math.floor( e.scrollWidth / ( $.client.profile().platform === 'linux' ? 7 : 8 ) );
421                                 }
422                                 function getCaretScrollPosition( e ) {
423                                         // FIXME: This functions sucks and is off by a few lines most
424                                         // of the time. It should be replaced by something decent.
425                                         var i, j,
426                                                 nextSpace,
427                                                 text = e.value.replace( /\r/g, '' ),
428                                                 caret = $( e ).textSelection( 'getCaretPosition' ),
429                                                 lineLength = getLineLength( e ),
430                                                 row = 0,
431                                                 charInLine = 0,
432                                                 lastSpaceInLine = 0;
434                                         for ( i = 0; i < caret; i++ ) {
435                                                 charInLine++;
436                                                 if ( text.charAt( i ) === ' ' ) {
437                                                         lastSpaceInLine = charInLine;
438                                                 } else if ( text.charAt( i ) === '\n' ) {
439                                                         lastSpaceInLine = 0;
440                                                         charInLine = 0;
441                                                         row++;
442                                                 }
443                                                 if ( charInLine > lineLength ) {
444                                                         if ( lastSpaceInLine > 0 ) {
445                                                                 charInLine = charInLine - lastSpaceInLine;
446                                                                 lastSpaceInLine = 0;
447                                                                 row++;
448                                                         }
449                                                 }
450                                         }
451                                         nextSpace = 0;
452                                         for ( j = caret; j < caret + lineLength; j++ ) {
453                                                 if (
454                                                         text.charAt( j ) === ' ' ||
455                                                         text.charAt( j ) === '\n' ||
456                                                         caret === text.length
457                                                 ) {
458                                                         nextSpace = j;
459                                                         break;
460                                                 }
461                                         }
462                                         if ( nextSpace > lineLength && caret <= lineLength ) {
463                                                 charInLine = caret - lastSpaceInLine;
464                                                 row++;
465                                         }
466                                         return ( $.client.profile().platform === 'mac' ? 13 : ( $.client.profile().platform === 'linux' ? 15 : 16 ) ) * row;
467                                 }
468                                 return this.each( function () {
469                                         var scroll, range, savedRange, pos, oldScrollTop;
470                                         // Do nothing if hidden
471                                         if ( !$( this ).is( ':hidden' ) ) {
472                                                 if ( this.selectionStart || this.selectionStart === 0 ) {
473                                                         // Mozilla
474                                                         scroll = getCaretScrollPosition( this );
475                                                         if ( options.force || scroll < $( this ).scrollTop() ||
476                                                                         scroll > $( this ).scrollTop() + $( this ).height() ) {
477                                                                 $( this ).scrollTop( scroll );
478                                                         }
479                                                 } else if ( document.selection && document.selection.createRange ) {
480                                                         // IE / Opera
481                                                         /*
482                                                          * IE automatically scrolls the selected text to the
483                                                          * bottom of the textarea at range.select() time, except
484                                                          * if it was already in view and the cursor position
485                                                          * wasn't changed, in which case it does nothing. To
486                                                          * cover that case, we'll force it to act by moving one
487                                                          * character back and forth.
488                                                          */
489                                                         range = document.body.createTextRange();
490                                                         savedRange = document.selection.createRange();
491                                                         pos = $( this ).textSelection( 'getCaretPosition' );
492                                                         oldScrollTop = this.scrollTop;
493                                                         range.moveToElementText( this );
494                                                         range.collapse();
495                                                         range.move( 'character', pos + 1 );
496                                                         range.select();
497                                                         if ( this.scrollTop !== oldScrollTop ) {
498                                                                 this.scrollTop += range.offsetTop;
499                                                         } else if ( options.force ) {
500                                                                 range.move( 'character', -1 );
501                                                                 range.select();
502                                                         }
503                                                         savedRange.select();
504                                                 }
505                                         }
506                                         $( this ).trigger( 'scrollToPosition' );
507                                 } );
508                         }
509                 };
511                 alternateFn = $( this ).data( 'jquery.textSelection' );
513                 // Apply defaults
514                 switch ( command ) {
515                         //case 'getContents': // no params
516                         //case 'setContents': // no params with defaults
517                         //case 'getSelection': // no params
518                         case 'encapsulateSelection':
519                                 options = $.extend( {
520                                         pre: '', // Text to insert before the cursor/selection
521                                         peri: '', // Text to insert between pre and post and select afterwards
522                                         post: '', // Text to insert after the cursor/selection
523                                         ownline: false, // Put the inserted text on a line of its own
524                                         replace: false, // If there is a selection, replace it with peri instead of leaving it alone
525                                         selectPeri: true, // Select the peri text if it was inserted (but not if there was a selection and replace==false, or if splitlines==true)
526                                         splitlines: false, // If multiple lines are selected, encapsulate each line individually
527                                         selectionStart: undefined, // Position to start selection at
528                                         selectionEnd: undefined // Position to end selection at. Defaults to start
529                                 }, options );
530                                 break;
531                         case 'getCaretPosition':
532                                 options = $.extend( {
533                                         // Return [start, end] instead of just start
534                                         startAndEnd: false
535                                 }, options );
536                                 // FIXME: We may not need character position-based functions if we insert markers in the right places
537                                 break;
538                         case 'setSelection':
539                                 options = $.extend( {
540                                         // Position to start selection at
541                                         start: undefined,
542                                         // Position to end selection at. Defaults to start
543                                         end: undefined
544                                 }, options );
546                                 if ( options.end === undefined ) {
547                                         options.end = options.start;
548                                 }
549                                 // FIXME: We may not need character position-based functions if we insert markers in the right places
550                                 break;
551                         case 'scrollToCaretPosition':
552                                 options = $.extend( {
553                                         force: false // Force a scroll even if the caret position is already visible
554                                 }, options );
555                                 break;
556                         case 'register':
557                                 if ( alternateFn ) {
558                                         throw new Error( 'Another textSelection API was already registered' );
559                                 }
560                                 $( this ).data( 'jquery.textSelection', options );
561                                 // No need to update alternateFn as this command only stores the options.
562                                 // A command that uses it will set it again.
563                                 return;
564                         case 'unregister':
565                                 $( this ).removeData( 'jquery.textSelection' );
566                                 return;
567                 }
569                 context = $( this ).data( 'wikiEditor-context' );
570                 hasWikiEditor = ( context !== undefined && context.$iframe !== undefined );
572                 // IE selection restore voodoo
573                 needSave = false;
574                 if ( hasWikiEditor && context.savedSelection !== null ) {
575                         context.fn.restoreSelection();
576                         needSave = true;
577                 }
578                 retval = ( alternateFn && alternateFn[command] || fn[command] ).call( this, options );
579                 if ( hasWikiEditor && needSave ) {
580                         context.fn.saveSelection();
581                 }
583                 return retval;
584         };
586 }( jQuery ) );