Merge "objectcache: Remove broken cas() method from WinCacheBagOStuff"
[lhc/web/wiklou.git] / resources / src / jquery / jquery.textSelection.js
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 }
24
25 $.fn.textSelection = function ( command, options ) {
26 var fn,
27 alternateFn,
28 context,
29 hasWikiEditor,
30 needSave,
31 retval;
32
33 /**
34 * Helper function to get an IE TextRange object for an element
35 */
36 function rangeForElementIE( e ) {
37 var sel;
38 if ( e.nodeName.toLowerCase() === 'input' ) {
39 return e.createTextRange();
40 } else {
41 sel = document.body.createTextRange();
42 sel.moveToElementText( e );
43 return sel;
44 }
45 }
46
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 }
59
60 fn = {
61 /**
62 * Get the contents of the textarea
63 */
64 getContents: function () {
65 return this.val();
66 },
67 /**
68 * Set the contents of the textarea, replacing anything that was there before
69 */
70 setContents: function ( content ) {
71 this.val( content );
72 },
73 /**
74 * Get the currently selected text in this textarea. Will focus the textarea
75 * in some browsers (IE/Opera)
76 */
77 getSelection: function () {
78 var retval, range,
79 el = this.get( 0 );
80
81 if ( !el || $( el ).is( ':hidden' ) ) {
82 retval = '';
83 } else if ( document.selection && document.selection.createRange ) {
84 activateElementOnIE( el );
85 range = document.selection.createRange();
86 retval = range.text;
87 } else if ( el.selectionStart || el.selectionStart === 0 ) {
88 retval = el.value.substring( el.selectionStart, el.selectionEnd );
89 }
90
91 return retval;
92 },
93 /**
94 * Ported from skins/common/edit.js by Trevor Parscal
95 * (c) 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
96 *
97 * Inserts text at the beginning and end of a text selection, optionally
98 * inserting text at the caret when selection is empty.
99 *
100 * FIXME document the options parameters
101 */
102 encapsulateSelection: function ( options ) {
103 return this.each( function () {
104 var selText, scrollTop, insertText,
105 isSample, range, range2, range3, startPos, endPos,
106 pre = options.pre,
107 post = options.post;
108
109 /**
110 * Check if the selected text is the same as the insert text
111 */
112 function checkSelectedText() {
113 if ( !selText ) {
114 selText = options.peri;
115 isSample = true;
116 } else if ( options.replace ) {
117 selText = options.peri;
118 } else {
119 while ( selText.charAt( selText.length - 1 ) === ' ' ) {
120 // Exclude ending space char
121 selText = selText.slice( 0, -1 );
122 post += ' ';
123 }
124 while ( selText.charAt( 0 ) === ' ' ) {
125 // Exclude prepending space char
126 selText = selText.slice( 1 );
127 pre = ' ' + pre;
128 }
129 }
130 }
131
132 /**
133 * Do the splitlines stuff.
134 *
135 * Wrap each line of the selected text with pre and post
136 */
137 function doSplitLines( selText, pre, post ) {
138 var i,
139 insertText = '',
140 selTextArr = selText.split( '\n' );
141 for ( i = 0; i < selTextArr.length; i++ ) {
142 insertText += pre + selTextArr[ i ] + post;
143 if ( i !== selTextArr.length - 1 ) {
144 insertText += '\n';
145 }
146 }
147 return insertText;
148 }
149
150 isSample = false;
151 // Do nothing if display none
152 if ( this.style.display !== 'none' ) {
153 if ( document.selection && document.selection.createRange ) {
154 // IE
155
156 // Note that IE9 will trigger the next section unless we check this first.
157 // See bug 35201.
158
159 activateElementOnIE( this );
160 if ( context ) {
161 context.fn.restoreCursorAndScrollTop();
162 }
163 if ( options.selectionStart !== undefined ) {
164 $( this ).textSelection( 'setSelection', { start: options.selectionStart, end: options.selectionEnd } );
165 }
166
167 selText = $( this ).textSelection( 'getSelection' );
168 scrollTop = this.scrollTop;
169 range = document.selection.createRange();
170
171 checkSelectedText();
172 insertText = pre + selText + post;
173 if ( options.splitlines ) {
174 insertText = doSplitLines( selText, pre, post );
175 }
176 if ( options.ownline && range.moveStart ) {
177 range2 = document.selection.createRange();
178 range2.collapse();
179 range2.moveStart( 'character', -1 );
180 // FIXME: Which check is correct?
181 if ( range2.text !== '\r' && range2.text !== '\n' && range2.text !== '' ) {
182 insertText = '\n' + insertText;
183 pre += '\n';
184 }
185 range3 = document.selection.createRange();
186 range3.collapse( false );
187 range3.moveEnd( 'character', 1 );
188 if ( range3.text !== '\r' && range3.text !== '\n' && range3.text !== '' ) {
189 insertText += '\n';
190 post += '\n';
191 }
192 }
193
194 range.text = insertText;
195 if ( isSample && options.selectPeri && range.moveStart ) {
196 range.moveStart( 'character', -post.length - selText.length );
197 range.moveEnd( 'character', -post.length );
198 }
199 range.select();
200 // Restore the scroll position
201 this.scrollTop = scrollTop;
202 } else if ( this.selectionStart || this.selectionStart === 0 ) {
203 // Mozilla/Opera
204
205 $( this ).focus();
206 if ( options.selectionStart !== undefined ) {
207 $( this ).textSelection( 'setSelection', { start: options.selectionStart, end: options.selectionEnd } );
208 }
209
210 selText = $( this ).textSelection( 'getSelection' );
211 startPos = this.selectionStart;
212 endPos = this.selectionEnd;
213 scrollTop = this.scrollTop;
214 checkSelectedText();
215 if (
216 options.selectionStart !== undefined &&
217 endPos - startPos !== options.selectionEnd - options.selectionStart
218 ) {
219 // This means there is a difference in the selection range returned by browser and what we passed.
220 // This happens for Chrome in the case of composite characters. Ref bug #30130
221 // Set the startPos to the correct position.
222 startPos = options.selectionStart;
223 }
224
225 insertText = pre + selText + post;
226 if ( options.splitlines ) {
227 insertText = doSplitLines( selText, pre, post );
228 }
229 if ( options.ownline ) {
230 if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== '\n' && this.value.charAt( startPos - 1 ) !== '\r' ) {
231 insertText = '\n' + insertText;
232 pre += '\n';
233 }
234 if ( this.value.charAt( endPos ) !== '\n' && this.value.charAt( endPos ) !== '\r' ) {
235 insertText += '\n';
236 post += '\n';
237 }
238 }
239 this.value = this.value.slice( 0, startPos ) + insertText +
240 this.value.slice( endPos );
241 // Setting this.value scrolls the textarea to the top, restore the scroll position
242 this.scrollTop = scrollTop;
243 if ( window.opera ) {
244 pre = pre.replace( /\r?\n/g, '\r\n' );
245 selText = selText.replace( /\r?\n/g, '\r\n' );
246 post = post.replace( /\r?\n/g, '\r\n' );
247 }
248 if ( isSample && options.selectPeri && ( !options.splitlines || ( options.splitlines && selText.indexOf( '\n' ) === -1 ) ) ) {
249 this.selectionStart = startPos + pre.length;
250 this.selectionEnd = startPos + pre.length + selText.length;
251 } else {
252 this.selectionStart = startPos + insertText.length;
253 this.selectionEnd = this.selectionStart;
254 }
255 }
256 }
257 $( this ).trigger( 'encapsulateSelection', [ options.pre, options.peri, options.post, options.ownline,
258 options.replace, options.spitlines ] );
259 } );
260 },
261 /**
262 * Ported from Wikia's LinkSuggest extension
263 * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
264 * Some code copied from
265 * http://www.dedestruct.com/2008/03/22/howto-cross-browser-cursor-position-in-textareas/
266 *
267 * Get the position (in resolution of bytes not necessarily characters)
268 * in a textarea
269 *
270 * Will focus the textarea in some browsers (IE/Opera)
271 *
272 * FIXME document the options parameters
273 */
274 getCaretPosition: function ( options ) {
275 function getCaret( e ) {
276 var caretPos = 0,
277 endPos = 0,
278 preText, rawPreText, periText,
279 rawPeriText, postText,
280 // IE Support
281 preFinished,
282 periFinished,
283 postFinished,
284 // Range containing text in the selection
285 periRange,
286 // Range containing text before the selection
287 preRange,
288 // Range containing text after the selection
289 postRange;
290
291 if ( e && document.selection && document.selection.createRange ) {
292 // IE doesn't properly report non-selected caret position through
293 // the selection ranges when textarea isn't focused. This can
294 // lead to saving a bogus empty selection, which then screws up
295 // whatever we do later (bug 31847).
296 activateElementOnIE( e );
297
298 preFinished = false;
299 periFinished = false;
300 postFinished = false;
301 periRange = document.selection.createRange().duplicate();
302
303 preRange = rangeForElementIE( e );
304 // Move the end where we need it
305 preRange.setEndPoint( 'EndToStart', periRange );
306
307 postRange = rangeForElementIE( e );
308 // Move the start where we need it
309 postRange.setEndPoint( 'StartToEnd', periRange );
310
311 // Load the text values we need to compare
312 preText = rawPreText = preRange.text;
313 periText = rawPeriText = periRange.text;
314 postText = postRange.text;
315
316 /*
317 * Check each range for trimmed newlines by shrinking the range by 1
318 * character and seeing if the text property has changed. If it has
319 * not changed then we know that IE has trimmed a \r\n from the end.
320 */
321 do {
322 if ( !preFinished ) {
323 if ( preRange.compareEndPoints( 'StartToEnd', preRange ) === 0 ) {
324 preFinished = true;
325 } else {
326 preRange.moveEnd( 'character', -1 );
327 if ( preRange.text === preText ) {
328 rawPreText += '\r\n';
329 } else {
330 preFinished = true;
331 }
332 }
333 }
334 if ( !periFinished ) {
335 if ( periRange.compareEndPoints( 'StartToEnd', periRange ) === 0 ) {
336 periFinished = true;
337 } else {
338 periRange.moveEnd( 'character', -1 );
339 if ( periRange.text === periText ) {
340 rawPeriText += '\r\n';
341 } else {
342 periFinished = true;
343 }
344 }
345 }
346 if ( !postFinished ) {
347 if ( postRange.compareEndPoints( 'StartToEnd', postRange ) === 0 ) {
348 postFinished = true;
349 } else {
350 postRange.moveEnd( 'character', -1 );
351 if ( postRange.text !== postText ) {
352 postFinished = true;
353 }
354 }
355 }
356 } while ( ( !preFinished || !periFinished || !postFinished ) );
357 caretPos = rawPreText.replace( /\r\n/g, '\n' ).length;
358 endPos = caretPos + rawPeriText.replace( /\r\n/g, '\n' ).length;
359 } else if ( e && ( e.selectionStart || e.selectionStart === 0 ) ) {
360 // Firefox support
361 caretPos = e.selectionStart;
362 endPos = e.selectionEnd;
363 }
364 return options.startAndEnd ? [ caretPos, endPos ] : caretPos;
365 }
366 return getCaret( this.get( 0 ) );
367 },
368 /**
369 * FIXME document the options parameters
370 */
371 setSelection: function ( options ) {
372 return this.each( function () {
373 var selection, length, newLines;
374 // Do nothing if hidden
375 if ( !$( this ).is( ':hidden' ) ) {
376 if ( this.selectionStart || this.selectionStart === 0 ) {
377 // Opera 9.0 doesn't allow setting selectionStart past
378 // selectionEnd; any attempts to do that will be ignored
379 // Make sure to set them in the right order
380 if ( options.start > this.selectionEnd ) {
381 this.selectionEnd = options.end;
382 this.selectionStart = options.start;
383 } else {
384 this.selectionStart = options.start;
385 this.selectionEnd = options.end;
386 }
387 } else if ( document.body.createTextRange ) {
388 selection = rangeForElementIE( this );
389 length = this.value.length;
390 // IE doesn't count \n when computing the offset, so we won't either
391 newLines = this.value.match( /\n/g );
392 if ( newLines ) {
393 length = length - newLines.length;
394 }
395 selection.moveStart( 'character', options.start );
396 selection.moveEnd( 'character', -length + options.end );
397
398 // This line can cause an error under certain circumstances (textarea empty, no selection)
399 // Silence that error
400 try {
401 selection.select();
402 } catch ( e ) { }
403 }
404 }
405 } );
406 },
407 /**
408 * Ported from Wikia's LinkSuggest extension
409 * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
410 *
411 * Scroll a textarea to the current cursor position. You can set the cursor
412 * position with setSelection()
413 *
414 * @param {boolean} options 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;
434
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 };
511
512 alternateFn = $( this ).data( 'jquery.textSelection' );
513
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 );
546
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 }
569
570 context = $( this ).data( 'wikiEditor-context' );
571 hasWikiEditor = ( context !== undefined && context.$iframe !== undefined );
572
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 }
583
584 return retval;
585 };
586
587 }( jQuery ) );