Merge "DatabaseMssql: Don't duplicate body of makeList()"
[mediawiki.git] / resources / src / jquery / jquery.textSelection.js
blob5111930517c67d11bb389f65807d0e25cec40795
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                          * @param options boolean Whether to force a scroll even if the caret position
415                          *  is already visible. Defaults to false
416                          *
417                          * @fixme document the options parameters (function body suggests options.force is a boolean, not options itself)
418                          */
419                         scrollToCaretPosition: function ( options ) {
420                                 function getLineLength( e ) {
421                                         return Math.floor( e.scrollWidth / ( $.client.profile().platform === 'linux' ? 7 : 8 ) );
422                                 }
423                                 function getCaretScrollPosition( e ) {
424                                         // FIXME: This functions sucks and is off by a few lines most
425                                         // of the time. It should be replaced by something decent.
426                                         var i, j,
427                                                 nextSpace,
428                                                 text = e.value.replace( /\r/g, '' ),
429                                                 caret = $( e ).textSelection( 'getCaretPosition' ),
430                                                 lineLength = getLineLength( e ),
431                                                 row = 0,
432                                                 charInLine = 0,
433                                                 lastSpaceInLine = 0;
435                                         for ( i = 0; i < caret; i++ ) {
436                                                 charInLine++;
437                                                 if ( text.charAt( i ) === ' ' ) {
438                                                         lastSpaceInLine = charInLine;
439                                                 } else if ( text.charAt( i ) === '\n' ) {
440                                                         lastSpaceInLine = 0;
441                                                         charInLine = 0;
442                                                         row++;
443                                                 }
444                                                 if ( charInLine > lineLength ) {
445                                                         if ( lastSpaceInLine > 0 ) {
446                                                                 charInLine = charInLine - lastSpaceInLine;
447                                                                 lastSpaceInLine = 0;
448                                                                 row++;
449                                                         }
450                                                 }
451                                         }
452                                         nextSpace = 0;
453                                         for ( j = caret; j < caret + lineLength; j++ ) {
454                                                 if (
455                                                         text.charAt( j ) === ' ' ||
456                                                         text.charAt( j ) === '\n' ||
457                                                         caret === text.length
458                                                 ) {
459                                                         nextSpace = j;
460                                                         break;
461                                                 }
462                                         }
463                                         if ( nextSpace > lineLength && caret <= lineLength ) {
464                                                 charInLine = caret - lastSpaceInLine;
465                                                 row++;
466                                         }
467                                         return ( $.client.profile().platform === 'mac' ? 13 : ( $.client.profile().platform === 'linux' ? 15 : 16 ) ) * row;
468                                 }
469                                 return this.each( function () {
470                                         var scroll, range, savedRange, pos, oldScrollTop;
471                                         // Do nothing if hidden
472                                         if ( !$( this ).is( ':hidden' ) ) {
473                                                 if ( this.selectionStart || this.selectionStart === 0 ) {
474                                                         // Mozilla
475                                                         scroll = getCaretScrollPosition( this );
476                                                         if ( options.force || scroll < $( this ).scrollTop() ||
477                                                                         scroll > $( this ).scrollTop() + $( this ).height() ) {
478                                                                 $( this ).scrollTop( scroll );
479                                                         }
480                                                 } else if ( document.selection && document.selection.createRange ) {
481                                                         // IE / Opera
482                                                         /*
483                                                          * IE automatically scrolls the selected text to the
484                                                          * bottom of the textarea at range.select() time, except
485                                                          * if it was already in view and the cursor position
486                                                          * wasn't changed, in which case it does nothing. To
487                                                          * cover that case, we'll force it to act by moving one
488                                                          * character back and forth.
489                                                          */
490                                                         range = document.body.createTextRange();
491                                                         savedRange = document.selection.createRange();
492                                                         pos = $( this ).textSelection( 'getCaretPosition' );
493                                                         oldScrollTop = this.scrollTop;
494                                                         range.moveToElementText( this );
495                                                         range.collapse();
496                                                         range.move( 'character', pos + 1 );
497                                                         range.select();
498                                                         if ( this.scrollTop !== oldScrollTop ) {
499                                                                 this.scrollTop += range.offsetTop;
500                                                         } else if ( options.force ) {
501                                                                 range.move( 'character', -1 );
502                                                                 range.select();
503                                                         }
504                                                         savedRange.select();
505                                                 }
506                                         }
507                                         $( this ).trigger( 'scrollToPosition' );
508                                 } );
509                         }
510                 };
512                 alternateFn = $( this ).data( 'jquery.textSelection' );
514                 // Apply defaults
515                 switch ( command ) {
516                         // case 'getContents': // no params
517                         // case 'setContents': // no params with defaults
518                         // case 'getSelection': // no params
519                         case 'encapsulateSelection':
520                                 options = $.extend( {
521                                         pre: '', // Text to insert before the cursor/selection
522                                         peri: '', // Text to insert between pre and post and select afterwards
523                                         post: '', // Text to insert after the cursor/selection
524                                         ownline: false, // Put the inserted text on a line of its own
525                                         replace: false, // If there is a selection, replace it with peri instead of leaving it alone
526                                         selectPeri: true, // Select the peri text if it was inserted (but not if there was a selection and replace==false, or if splitlines==true)
527                                         splitlines: false, // If multiple lines are selected, encapsulate each line individually
528                                         selectionStart: undefined, // Position to start selection at
529                                         selectionEnd: undefined // Position to end selection at. Defaults to start
530                                 }, options );
531                                 break;
532                         case 'getCaretPosition':
533                                 options = $.extend( {
534                                         // Return [start, end] instead of just start
535                                         startAndEnd: false
536                                 }, options );
537                                 // FIXME: We may not need character position-based functions if we insert markers in the right places
538                                 break;
539                         case 'setSelection':
540                                 options = $.extend( {
541                                         // Position to start selection at
542                                         start: undefined,
543                                         // Position to end selection at. Defaults to start
544                                         end: undefined
545                                 }, options );
547                                 if ( options.end === undefined ) {
548                                         options.end = options.start;
549                                 }
550                                 // FIXME: We may not need character position-based functions if we insert markers in the right places
551                                 break;
552                         case 'scrollToCaretPosition':
553                                 options = $.extend( {
554                                         force: false // Force a scroll even if the caret position is already visible
555                                 }, options );
556                                 break;
557                         case 'register':
558                                 if ( alternateFn ) {
559                                         throw new Error( 'Another textSelection API was already registered' );
560                                 }
561                                 $( this ).data( 'jquery.textSelection', options );
562                                 // No need to update alternateFn as this command only stores the options.
563                                 // A command that uses it will set it again.
564                                 return;
565                         case 'unregister':
566                                 $( this ).removeData( 'jquery.textSelection' );
567                                 return;
568                 }
570                 context = $( this ).data( 'wikiEditor-context' );
571                 hasWikiEditor = ( context !== undefined && context.$iframe !== undefined );
573                 // IE selection restore voodoo
574                 needSave = false;
575                 if ( hasWikiEditor && context.savedSelection !== null ) {
576                         context.fn.restoreSelection();
577                         needSave = true;
578                 }
579                 retval = ( alternateFn && alternateFn[command] || fn[command] ).call( this, options );
580                 if ( hasWikiEditor && needSave ) {
581                         context.fn.saveSelection();
582                 }
584                 return retval;
585         };
587 }( jQuery ) );