Merge "ForeignStructuredUpload: Depend on mediawiki.widgets.DateInputWidget"
[mediawiki.git] / resources / src / jquery / jquery.textSelection.js
blobb90164249545338da3e4f8f0440912ed0b944448
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 (
215                                                                 options.selectionStart !== undefined &&
216                                                                 endPos - startPos !== options.selectionEnd - options.selectionStart
217                                                         ) {
218                                                                 // This means there is a difference in the selection range returned by browser and what we passed.
219                                                                 // This happens for Chrome in the case of composite characters. Ref bug #30130
220                                                                 // Set the startPos to the correct position.
221                                                                 startPos = options.selectionStart;
222                                                         }
224                                                         insertText = pre + selText + post;
225                                                         if ( options.splitlines ) {
226                                                                 insertText = doSplitLines( selText, pre, post );
227                                                         }
228                                                         if ( options.ownline ) {
229                                                                 if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== '\n' && this.value.charAt( startPos - 1 ) !== '\r' ) {
230                                                                         insertText = '\n' + insertText;
231                                                                         pre += '\n';
232                                                                 }
233                                                                 if ( this.value.charAt( endPos ) !== '\n' && this.value.charAt( endPos ) !== '\r' ) {
234                                                                         insertText += '\n';
235                                                                         post += '\n';
236                                                                 }
237                                                         }
238                                                         this.value = this.value.slice( 0, startPos ) + insertText +
239                                                                 this.value.slice( endPos );
240                                                         // Setting this.value scrolls the textarea to the top, restore the scroll position
241                                                         this.scrollTop = scrollTop;
242                                                         if ( window.opera ) {
243                                                                 pre = pre.replace( /\r?\n/g, '\r\n' );
244                                                                 selText = selText.replace( /\r?\n/g, '\r\n' );
245                                                                 post = post.replace( /\r?\n/g, '\r\n' );
246                                                         }
247                                                         if ( isSample && options.selectPeri && ( !options.splitlines || ( options.splitlines && selText.indexOf( '\n' ) === -1 ) ) ) {
248                                                                 this.selectionStart = startPos + pre.length;
249                                                                 this.selectionEnd = startPos + pre.length + selText.length;
250                                                         } else {
251                                                                 this.selectionStart = startPos + insertText.length;
252                                                                 this.selectionEnd = this.selectionStart;
253                                                         }
254                                                 }
255                                         }
256                                         $( this ).trigger( 'encapsulateSelection', [ options.pre, options.peri, options.post, options.ownline,
257                                                 options.replace, options.spitlines ] );
258                                 } );
259                         },
260                         /**
261                          * Ported from Wikia's LinkSuggest extension
262                          * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
263                          * Some code copied from
264                          * http://www.dedestruct.com/2008/03/22/howto-cross-browser-cursor-position-in-textareas/
265                          *
266                          * Get the position (in resolution of bytes not necessarily characters)
267                          * in a textarea
268                          *
269                          * Will focus the textarea in some browsers (IE/Opera)
270                          *
271                          * @fixme document the options parameters
272                          */
273                         getCaretPosition: function ( options ) {
274                                 function getCaret( e ) {
275                                         var caretPos = 0,
276                                                 endPos = 0,
277                                                 preText, rawPreText, periText,
278                                                 rawPeriText, postText, rawPostText,
279                                                 // IE Support
280                                                 preFinished,
281                                                 periFinished,
282                                                 postFinished,
283                                                 // Range containing text in the selection
284                                                 periRange,
285                                                 // Range containing text before the selection
286                                                 preRange,
287                                                 // Range containing text after the selection
288                                                 postRange;
290                                         if ( e && document.selection && document.selection.createRange ) {
291                                                 // IE doesn't properly report non-selected caret position through
292                                                 // the selection ranges when textarea isn't focused. This can
293                                                 // lead to saving a bogus empty selection, which then screws up
294                                                 // whatever we do later (bug 31847).
295                                                 activateElementOnIE( e );
297                                                 preFinished = false;
298                                                 periFinished = false;
299                                                 postFinished = false;
300                                                 periRange = document.selection.createRange().duplicate();
302                                                 preRange = rangeForElementIE( e );
303                                                 // Move the end where we need it
304                                                 preRange.setEndPoint( 'EndToStart', periRange );
306                                                 postRange = rangeForElementIE( e );
307                                                 // Move the start where we need it
308                                                 postRange.setEndPoint( 'StartToEnd', periRange );
310                                                 // Load the text values we need to compare
311                                                 preText = rawPreText = preRange.text;
312                                                 periText = rawPeriText = periRange.text;
313                                                 postText = rawPostText = postRange.text;
315                                                 /*
316                                                  * Check each range for trimmed newlines by shrinking the range by 1
317                                                  * character and seeing if the text property has changed. If it has
318                                                  * not changed then we know that IE has trimmed a \r\n from the end.
319                                                  */
320                                                 do {
321                                                         if ( !preFinished ) {
322                                                                 if ( preRange.compareEndPoints( 'StartToEnd', preRange ) === 0 ) {
323                                                                         preFinished = true;
324                                                                 } else {
325                                                                         preRange.moveEnd( 'character', -1 );
326                                                                         if ( preRange.text === preText ) {
327                                                                                 rawPreText += '\r\n';
328                                                                         } else {
329                                                                                 preFinished = true;
330                                                                         }
331                                                                 }
332                                                         }
333                                                         if ( !periFinished ) {
334                                                                 if ( periRange.compareEndPoints( 'StartToEnd', periRange ) === 0 ) {
335                                                                         periFinished = true;
336                                                                 } else {
337                                                                         periRange.moveEnd( 'character', -1 );
338                                                                         if ( periRange.text === periText ) {
339                                                                                 rawPeriText += '\r\n';
340                                                                         } else {
341                                                                                 periFinished = true;
342                                                                         }
343                                                                 }
344                                                         }
345                                                         if ( !postFinished ) {
346                                                                 if ( postRange.compareEndPoints( 'StartToEnd', postRange ) === 0 ) {
347                                                                         postFinished = true;
348                                                                 } else {
349                                                                         postRange.moveEnd( 'character', -1 );
350                                                                         if ( postRange.text === postText ) {
351                                                                                 rawPostText += '\r\n';
352                                                                         } else {
353                                                                                 postFinished = true;
354                                                                         }
355                                                                 }
356                                                         }
357                                                 } while ( ( !preFinished || !periFinished || !postFinished ) );
358                                                 caretPos = rawPreText.replace( /\r\n/g, '\n' ).length;
359                                                 endPos = caretPos + rawPeriText.replace( /\r\n/g, '\n' ).length;
360                                         } else if ( e && ( e.selectionStart || e.selectionStart === 0 ) ) {
361                                                 // Firefox support
362                                                 caretPos = e.selectionStart;
363                                                 endPos = e.selectionEnd;
364                                         }
365                                         return options.startAndEnd ? [ caretPos, endPos ] : caretPos;
366                                 }
367                                 return getCaret( this.get( 0 ) );
368                         },
369                         /**
370                          * @fixme document the options parameters
371                          */
372                         setSelection: function ( options ) {
373                                 return this.each( function () {
374                                         var selection, length, newLines;
375                                         // Do nothing if hidden
376                                         if ( !$( this ).is( ':hidden' ) ) {
377                                                 if ( this.selectionStart || this.selectionStart === 0 ) {
378                                                         // Opera 9.0 doesn't allow setting selectionStart past
379                                                         // selectionEnd; any attempts to do that will be ignored
380                                                         // Make sure to set them in the right order
381                                                         if ( options.start > this.selectionEnd ) {
382                                                                 this.selectionEnd = options.end;
383                                                                 this.selectionStart = options.start;
384                                                         } else {
385                                                                 this.selectionStart = options.start;
386                                                                 this.selectionEnd = options.end;
387                                                         }
388                                                 } else if ( document.body.createTextRange ) {
389                                                         selection = rangeForElementIE( this );
390                                                         length = this.value.length;
391                                                         // IE doesn't count \n when computing the offset, so we won't either
392                                                         newLines = this.value.match( /\n/g );
393                                                         if ( newLines ) {
394                                                                 length = length - newLines.length;
395                                                         }
396                                                         selection.moveStart( 'character', options.start );
397                                                         selection.moveEnd( 'character', -length + options.end );
399                                                         // This line can cause an error under certain circumstances (textarea empty, no selection)
400                                                         // Silence that error
401                                                         try {
402                                                                 selection.select();
403                                                         } catch ( e ) { }
404                                                 }
405                                         }
406                                 } );
407                         },
408                         /**
409                          * Ported from Wikia's LinkSuggest extension
410                          * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
411                          *
412                          * Scroll a textarea to the current cursor position. You can set the cursor
413                          * position with setSelection()
414                          *
415                          * @param {boolean} options Whether to force a scroll even if the caret position
416                          *  is already visible. Defaults to false
417                          *
418                          * @fixme document the options parameters (function body suggests options.force is a boolean, not options itself)
419                          */
420                         scrollToCaretPosition: function ( options ) {
421                                 function getLineLength( e ) {
422                                         return Math.floor( e.scrollWidth / ( $.client.profile().platform === 'linux' ? 7 : 8 ) );
423                                 }
424                                 function getCaretScrollPosition( e ) {
425                                         // FIXME: This functions sucks and is off by a few lines most
426                                         // of the time. It should be replaced by something decent.
427                                         var i, j,
428                                                 nextSpace,
429                                                 text = e.value.replace( /\r/g, '' ),
430                                                 caret = $( e ).textSelection( 'getCaretPosition' ),
431                                                 lineLength = getLineLength( e ),
432                                                 row = 0,
433                                                 charInLine = 0,
434                                                 lastSpaceInLine = 0;
436                                         for ( i = 0; i < caret; i++ ) {
437                                                 charInLine++;
438                                                 if ( text.charAt( i ) === ' ' ) {
439                                                         lastSpaceInLine = charInLine;
440                                                 } else if ( text.charAt( i ) === '\n' ) {
441                                                         lastSpaceInLine = 0;
442                                                         charInLine = 0;
443                                                         row++;
444                                                 }
445                                                 if ( charInLine > lineLength ) {
446                                                         if ( lastSpaceInLine > 0 ) {
447                                                                 charInLine = charInLine - lastSpaceInLine;
448                                                                 lastSpaceInLine = 0;
449                                                                 row++;
450                                                         }
451                                                 }
452                                         }
453                                         nextSpace = 0;
454                                         for ( j = caret; j < caret + lineLength; j++ ) {
455                                                 if (
456                                                         text.charAt( j ) === ' ' ||
457                                                         text.charAt( j ) === '\n' ||
458                                                         caret === text.length
459                                                 ) {
460                                                         nextSpace = j;
461                                                         break;
462                                                 }
463                                         }
464                                         if ( nextSpace > lineLength && caret <= lineLength ) {
465                                                 charInLine = caret - lastSpaceInLine;
466                                                 row++;
467                                         }
468                                         return ( $.client.profile().platform === 'mac' ? 13 : ( $.client.profile().platform === 'linux' ? 15 : 16 ) ) * row;
469                                 }
470                                 return this.each( function () {
471                                         var scroll, range, savedRange, pos, oldScrollTop;
472                                         // Do nothing if hidden
473                                         if ( !$( this ).is( ':hidden' ) ) {
474                                                 if ( this.selectionStart || this.selectionStart === 0 ) {
475                                                         // Mozilla
476                                                         scroll = getCaretScrollPosition( this );
477                                                         if ( options.force || scroll < $( this ).scrollTop() ||
478                                                                         scroll > $( this ).scrollTop() + $( this ).height() ) {
479                                                                 $( this ).scrollTop( scroll );
480                                                         }
481                                                 } else if ( document.selection && document.selection.createRange ) {
482                                                         // IE / Opera
483                                                         /*
484                                                          * IE automatically scrolls the selected text to the
485                                                          * bottom of the textarea at range.select() time, except
486                                                          * if it was already in view and the cursor position
487                                                          * wasn't changed, in which case it does nothing. To
488                                                          * cover that case, we'll force it to act by moving one
489                                                          * character back and forth.
490                                                          */
491                                                         range = document.body.createTextRange();
492                                                         savedRange = document.selection.createRange();
493                                                         pos = $( this ).textSelection( 'getCaretPosition' );
494                                                         oldScrollTop = this.scrollTop;
495                                                         range.moveToElementText( this );
496                                                         range.collapse();
497                                                         range.move( 'character', pos + 1 );
498                                                         range.select();
499                                                         if ( this.scrollTop !== oldScrollTop ) {
500                                                                 this.scrollTop += range.offsetTop;
501                                                         } else if ( options.force ) {
502                                                                 range.move( 'character', -1 );
503                                                                 range.select();
504                                                         }
505                                                         savedRange.select();
506                                                 }
507                                         }
508                                         $( this ).trigger( 'scrollToPosition' );
509                                 } );
510                         }
511                 };
513                 alternateFn = $( this ).data( 'jquery.textSelection' );
515                 // Apply defaults
516                 switch ( command ) {
517                         // case 'getContents': // no params
518                         // case 'setContents': // no params with defaults
519                         // case 'getSelection': // no params
520                         case 'encapsulateSelection':
521                                 options = $.extend( {
522                                         pre: '', // Text to insert before the cursor/selection
523                                         peri: '', // Text to insert between pre and post and select afterwards
524                                         post: '', // Text to insert after the cursor/selection
525                                         ownline: false, // Put the inserted text on a line of its own
526                                         replace: false, // If there is a selection, replace it with peri instead of leaving it alone
527                                         selectPeri: true, // Select the peri text if it was inserted (but not if there was a selection and replace==false, or if splitlines==true)
528                                         splitlines: false, // If multiple lines are selected, encapsulate each line individually
529                                         selectionStart: undefined, // Position to start selection at
530                                         selectionEnd: undefined // Position to end selection at. Defaults to start
531                                 }, options );
532                                 break;
533                         case 'getCaretPosition':
534                                 options = $.extend( {
535                                         // Return [start, end] instead of just start
536                                         startAndEnd: false
537                                 }, options );
538                                 // FIXME: We may not need character position-based functions if we insert markers in the right places
539                                 break;
540                         case 'setSelection':
541                                 options = $.extend( {
542                                         // Position to start selection at
543                                         start: undefined,
544                                         // Position to end selection at. Defaults to start
545                                         end: undefined
546                                 }, options );
548                                 if ( options.end === undefined ) {
549                                         options.end = options.start;
550                                 }
551                                 // FIXME: We may not need character position-based functions if we insert markers in the right places
552                                 break;
553                         case 'scrollToCaretPosition':
554                                 options = $.extend( {
555                                         force: false // Force a scroll even if the caret position is already visible
556                                 }, options );
557                                 break;
558                         case 'register':
559                                 if ( alternateFn ) {
560                                         throw new Error( 'Another textSelection API was already registered' );
561                                 }
562                                 $( this ).data( 'jquery.textSelection', options );
563                                 // No need to update alternateFn as this command only stores the options.
564                                 // A command that uses it will set it again.
565                                 return;
566                         case 'unregister':
567                                 $( this ).removeData( 'jquery.textSelection' );
568                                 return;
569                 }
571                 context = $( this ).data( 'wikiEditor-context' );
572                 hasWikiEditor = ( context !== undefined && context.$iframe !== undefined );
574                 // IE selection restore voodoo
575                 needSave = false;
576                 if ( hasWikiEditor && context.savedSelection !== null ) {
577                         context.fn.restoreSelection();
578                         needSave = true;
579                 }
580                 retval = ( alternateFn && alternateFn[ command ] || fn[ command ] ).call( this, options );
581                 if ( hasWikiEditor && needSave ) {
582                         context.fn.saveSelection();
583                 }
585                 return retval;
586         };
588 }( jQuery ) );