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