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