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