2 * These plugins provide extra functionality for interaction with textareas.
5 if ( document.selection && document.selection.createRange ) {
6 // On IE, patch the focus() method to restore the windows' scroll position
9 focus: ( function ( jqFocus ) {
11 var $w, state, result;
12 if ( arguments.length === 0 ) {
14 state = { top: $w.scrollTop(), left: $w.scrollLeft() };
15 result = jqFocus.apply( this, arguments );
16 window.scrollTo( state.top, state.left );
19 return jqFocus.apply( this, arguments );
25 $.fn.textSelection = function ( command, options ) {
34 * Helper function to get an IE TextRange object for an element
36 * @param {HTMLElement} element
39 function rangeForElementIE( element ) {
41 if ( element.nodeName.toLowerCase() === 'input' ) {
42 return element.createTextRange();
44 sel = document.body.createTextRange();
45 sel.moveToElementText( element );
51 * Helper function for IE for activating the textarea. Called only in the
52 * IE-specific code paths below; makes use of IE-specific non-standard
53 * function setActive() if possible to avoid screen flicker.
55 * @param {HTMLElement} element
57 function activateElementOnIE( element ) {
58 if ( element.setActive ) {
59 element.setActive(); // T34241: doesn't scroll
61 $( element ).focus(); // may scroll (but we patched it above)
67 * Get the contents of the textarea
71 getContents: function () {
75 * Set the contents of the textarea, replacing anything that was there before
77 * @param {string} content
79 setContents: function ( content ) {
83 * Get the currently selected text in this textarea. Will focus the textarea
84 * in some browsers (IE/Opera)
88 getSelection: function () {
92 if ( !el || $( el ).is( ':hidden' ) ) {
94 } else if ( document.selection && document.selection.createRange ) {
95 activateElementOnIE( el );
96 range = document.selection.createRange();
98 } else if ( el.selectionStart || el.selectionStart === 0 ) {
99 retval = el.value.substring( el.selectionStart, el.selectionEnd );
105 * Ported from skins/common/edit.js by Trevor Parscal
106 * (c) 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
108 * Inserts text at the beginning and end of a text selection, optionally
109 * inserting text at the caret when selection is empty.
111 * @param {Object} options Options
112 * FIXME document the options parameters
115 encapsulateSelection: function ( options ) {
116 return this.each( function () {
117 var selText, scrollTop, insertText,
118 isSample, range, range2, range3, startPos, endPos,
123 * Check if the selected text is the same as the insert text
125 function checkSelectedText() {
127 selText = options.peri;
129 } else if ( options.replace ) {
130 selText = options.peri;
132 while ( selText.charAt( selText.length - 1 ) === ' ' ) {
133 // Exclude ending space char
134 selText = selText.slice( 0, -1 );
137 while ( selText.charAt( 0 ) === ' ' ) {
138 // Exclude prepending space char
139 selText = selText.slice( 1 );
146 * Do the splitlines stuff.
148 * Wrap each line of the selected text with pre and post
150 * @param {string} selText Selected text
151 * @param {string} pre Text before
152 * @param {string} post Text after
153 * @return {string} Wrapped text
155 function doSplitLines( selText, pre, post ) {
158 selTextArr = selText.split( '\n' );
159 for ( i = 0; i < selTextArr.length; i++ ) {
160 insertText += pre + selTextArr[ i ] + post;
161 if ( i !== selTextArr.length - 1 ) {
169 // Do nothing if display none
170 if ( this.style.display !== 'none' ) {
171 if ( document.selection && document.selection.createRange ) {
174 // Note that IE9 will trigger the next section unless we check this first.
177 activateElementOnIE( this );
179 context.fn.restoreCursorAndScrollTop();
181 if ( options.selectionStart !== undefined ) {
182 $( this ).textSelection( 'setSelection', { start: options.selectionStart, end: options.selectionEnd } );
185 selText = $( this ).textSelection( 'getSelection' );
186 scrollTop = this.scrollTop;
187 range = document.selection.createRange();
190 insertText = pre + selText + post;
191 if ( options.splitlines ) {
192 insertText = doSplitLines( selText, pre, post );
194 if ( options.ownline && range.moveStart ) {
195 range2 = document.selection.createRange();
197 range2.moveStart( 'character', -1 );
198 // FIXME: Which check is correct?
199 if ( range2.text !== '\r' && range2.text !== '\n' && range2.text !== '' ) {
200 insertText = '\n' + insertText;
203 range3 = document.selection.createRange();
204 range3.collapse( false );
205 range3.moveEnd( 'character', 1 );
206 if ( range3.text !== '\r' && range3.text !== '\n' && range3.text !== '' ) {
212 range.text = insertText;
213 if ( isSample && options.selectPeri && range.moveStart ) {
214 range.moveStart( 'character', -post.length - selText.length );
215 range.moveEnd( 'character', -post.length );
218 // Restore the scroll position
219 this.scrollTop = scrollTop;
220 } else if ( this.selectionStart || this.selectionStart === 0 ) {
224 if ( options.selectionStart !== undefined ) {
225 $( this ).textSelection( 'setSelection', { start: options.selectionStart, end: options.selectionEnd } );
228 selText = $( this ).textSelection( 'getSelection' );
229 startPos = this.selectionStart;
230 endPos = this.selectionEnd;
231 scrollTop = this.scrollTop;
234 options.selectionStart !== undefined &&
235 endPos - startPos !== options.selectionEnd - options.selectionStart
237 // This means there is a difference in the selection range returned by browser and what we passed.
238 // This happens for Chrome in the case of composite characters. Ref bug #30130
239 // Set the startPos to the correct position.
240 startPos = options.selectionStart;
243 insertText = pre + selText + post;
244 if ( options.splitlines ) {
245 insertText = doSplitLines( selText, pre, post );
247 if ( options.ownline ) {
248 if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== '\n' && this.value.charAt( startPos - 1 ) !== '\r' ) {
249 insertText = '\n' + insertText;
252 if ( this.value.charAt( endPos ) !== '\n' && this.value.charAt( endPos ) !== '\r' ) {
257 this.value = this.value.slice( 0, startPos ) + insertText +
258 this.value.slice( endPos );
259 // Setting this.value scrolls the textarea to the top, restore the scroll position
260 this.scrollTop = scrollTop;
261 if ( window.opera ) {
262 pre = pre.replace( /\r?\n/g, '\r\n' );
263 selText = selText.replace( /\r?\n/g, '\r\n' );
264 post = post.replace( /\r?\n/g, '\r\n' );
266 if ( isSample && options.selectPeri && ( !options.splitlines || ( options.splitlines && selText.indexOf( '\n' ) === -1 ) ) ) {
267 this.selectionStart = startPos + pre.length;
268 this.selectionEnd = startPos + pre.length + selText.length;
270 this.selectionStart = startPos + insertText.length;
271 this.selectionEnd = this.selectionStart;
275 $( this ).trigger( 'encapsulateSelection', [ options.pre, options.peri, options.post, options.ownline,
276 options.replace, options.spitlines ] );
280 * Ported from Wikia's LinkSuggest extension
281 * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
282 * Some code copied from
283 * http://www.dedestruct.com/2008/03/22/howto-cross-browser-cursor-position-in-textareas/
285 * Get the position (in resolution of bytes not necessarily characters)
288 * Will focus the textarea in some browsers (IE/Opera)
290 * @param {Object} options Options
291 * FIXME document the options parameters
292 * @return {number} Position
294 getCaretPosition: function ( options ) {
295 function getCaret( e ) {
298 preText, rawPreText, periText,
299 rawPeriText, postText,
304 // Range containing text in the selection
306 // Range containing text before the selection
308 // Range containing text after the selection
311 if ( e && document.selection && document.selection.createRange ) {
312 // IE doesn't properly report non-selected caret position through
313 // the selection ranges when textarea isn't focused. This can
314 // lead to saving a bogus empty selection, which then screws up
315 // whatever we do later (T33847).
316 activateElementOnIE( e );
319 periFinished = false;
320 postFinished = false;
321 periRange = document.selection.createRange().duplicate();
323 preRange = rangeForElementIE( e );
324 // Move the end where we need it
325 preRange.setEndPoint( 'EndToStart', periRange );
327 postRange = rangeForElementIE( e );
328 // Move the start where we need it
329 postRange.setEndPoint( 'StartToEnd', periRange );
331 // Load the text values we need to compare
332 preText = rawPreText = preRange.text;
333 periText = rawPeriText = periRange.text;
334 postText = postRange.text;
337 * Check each range for trimmed newlines by shrinking the range by 1
338 * character and seeing if the text property has changed. If it has
339 * not changed then we know that IE has trimmed a \r\n from the end.
342 if ( !preFinished ) {
343 if ( preRange.compareEndPoints( 'StartToEnd', preRange ) === 0 ) {
346 preRange.moveEnd( 'character', -1 );
347 if ( preRange.text === preText ) {
348 rawPreText += '\r\n';
354 if ( !periFinished ) {
355 if ( periRange.compareEndPoints( 'StartToEnd', periRange ) === 0 ) {
358 periRange.moveEnd( 'character', -1 );
359 if ( periRange.text === periText ) {
360 rawPeriText += '\r\n';
366 if ( !postFinished ) {
367 if ( postRange.compareEndPoints( 'StartToEnd', postRange ) === 0 ) {
370 postRange.moveEnd( 'character', -1 );
371 if ( postRange.text !== postText ) {
376 } while ( ( !preFinished || !periFinished || !postFinished ) );
377 caretPos = rawPreText.replace( /\r\n/g, '\n' ).length;
378 endPos = caretPos + rawPeriText.replace( /\r\n/g, '\n' ).length;
379 } else if ( e && ( e.selectionStart || e.selectionStart === 0 ) ) {
381 caretPos = e.selectionStart;
382 endPos = e.selectionEnd;
384 return options.startAndEnd ? [ caretPos, endPos ] : caretPos;
386 return getCaret( this.get( 0 ) );
389 * @param {Object} options options
390 * FIXME document the options parameters
393 setSelection: function ( options ) {
394 return this.each( function () {
395 var selection, length, newLines;
396 // Do nothing if hidden
397 if ( !$( this ).is( ':hidden' ) ) {
398 if ( this.selectionStart || this.selectionStart === 0 ) {
399 // Opera 9.0 doesn't allow setting selectionStart past
400 // selectionEnd; any attempts to do that will be ignored
401 // Make sure to set them in the right order
402 if ( options.start > this.selectionEnd ) {
403 this.selectionEnd = options.end;
404 this.selectionStart = options.start;
406 this.selectionStart = options.start;
407 this.selectionEnd = options.end;
409 } else if ( document.body.createTextRange ) {
410 selection = rangeForElementIE( this );
411 length = this.value.length;
412 // IE doesn't count \n when computing the offset, so we won't either
413 newLines = this.value.match( /\n/g );
415 length = length - newLines.length;
417 selection.moveStart( 'character', options.start );
418 selection.moveEnd( 'character', -length + options.end );
420 // This line can cause an error under certain circumstances (textarea empty, no selection)
421 // Silence that error
430 * Ported from Wikia's LinkSuggest extension
431 * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
433 * Scroll a textarea to the current cursor position. You can set the cursor
434 * position with setSelection()
436 * @param {Object} options options
437 * @cfg {boolean} [force=false] Whether to force a scroll even if the caret position
438 * is already visible.
439 * FIXME document the options parameters
442 scrollToCaretPosition: function ( options ) {
443 function getLineLength( e ) {
444 return Math.floor( e.scrollWidth / ( $.client.profile().platform === 'linux' ? 7 : 8 ) );
446 function getCaretScrollPosition( e ) {
447 // FIXME: This functions sucks and is off by a few lines most
448 // of the time. It should be replaced by something decent.
451 text = e.value.replace( /\r/g, '' ),
452 caret = $( e ).textSelection( 'getCaretPosition' ),
453 lineLength = getLineLength( e ),
458 for ( i = 0; i < caret; i++ ) {
460 if ( text.charAt( i ) === ' ' ) {
461 lastSpaceInLine = charInLine;
462 } else if ( text.charAt( i ) === '\n' ) {
467 if ( charInLine > lineLength ) {
468 if ( lastSpaceInLine > 0 ) {
469 charInLine = charInLine - lastSpaceInLine;
476 for ( j = caret; j < caret + lineLength; j++ ) {
478 text.charAt( j ) === ' ' ||
479 text.charAt( j ) === '\n' ||
480 caret === text.length
486 if ( nextSpace > lineLength && caret <= lineLength ) {
487 charInLine = caret - lastSpaceInLine;
490 return ( $.client.profile().platform === 'mac' ? 13 : ( $.client.profile().platform === 'linux' ? 15 : 16 ) ) * row;
492 return this.each( function () {
493 var scroll, range, savedRange, pos, oldScrollTop;
494 // Do nothing if hidden
495 if ( !$( this ).is( ':hidden' ) ) {
496 if ( this.selectionStart || this.selectionStart === 0 ) {
498 scroll = getCaretScrollPosition( this );
499 if ( options.force || scroll < $( this ).scrollTop() ||
500 scroll > $( this ).scrollTop() + $( this ).height() ) {
501 $( this ).scrollTop( scroll );
503 } else if ( document.selection && document.selection.createRange ) {
506 * IE automatically scrolls the selected text to the
507 * bottom of the textarea at range.select() time, except
508 * if it was already in view and the cursor position
509 * wasn't changed, in which case it does nothing. To
510 * cover that case, we'll force it to act by moving one
511 * character back and forth.
513 range = document.body.createTextRange();
514 savedRange = document.selection.createRange();
515 pos = $( this ).textSelection( 'getCaretPosition' );
516 oldScrollTop = this.scrollTop;
517 range.moveToElementText( this );
519 range.move( 'character', pos + 1 );
521 if ( this.scrollTop !== oldScrollTop ) {
522 this.scrollTop += range.offsetTop;
523 } else if ( options.force ) {
524 range.move( 'character', -1 );
530 $( this ).trigger( 'scrollToPosition' );
535 alternateFn = $( this ).data( 'jquery.textSelection' );
539 // case 'getContents': // no params
540 // case 'setContents': // no params with defaults
541 // case 'getSelection': // no params
542 case 'encapsulateSelection':
543 options = $.extend( {
544 pre: '', // Text to insert before the cursor/selection
545 peri: '', // Text to insert between pre and post and select afterwards
546 post: '', // Text to insert after the cursor/selection
547 ownline: false, // Put the inserted text on a line of its own
548 replace: false, // If there is a selection, replace it with peri instead of leaving it alone
549 selectPeri: true, // Select the peri text if it was inserted (but not if there was a selection and replace==false, or if splitlines==true)
550 splitlines: false, // If multiple lines are selected, encapsulate each line individually
551 selectionStart: undefined, // Position to start selection at
552 selectionEnd: undefined // Position to end selection at. Defaults to start
555 case 'getCaretPosition':
556 options = $.extend( {
557 // Return [start, end] instead of just start
560 // FIXME: We may not need character position-based functions if we insert markers in the right places
563 options = $.extend( {
564 // Position to start selection at
566 // Position to end selection at. Defaults to start
570 if ( options.end === undefined ) {
571 options.end = options.start;
573 // FIXME: We may not need character position-based functions if we insert markers in the right places
575 case 'scrollToCaretPosition':
576 options = $.extend( {
577 force: false // Force a scroll even if the caret position is already visible
582 throw new Error( 'Another textSelection API was already registered' );
584 $( this ).data( 'jquery.textSelection', options );
585 // No need to update alternateFn as this command only stores the options.
586 // A command that uses it will set it again.
589 $( this ).removeData( 'jquery.textSelection' );
593 context = $( this ).data( 'wikiEditor-context' );
594 hasWikiEditor = ( context !== undefined && context.$iframe !== undefined );
596 // IE selection restore voodoo
598 if ( hasWikiEditor && context.savedSelection !== null ) {
599 context.fn.restoreSelection();
602 retval = ( alternateFn && alternateFn[ command ] || fn[ command ] ).call( this, options );
603 if ( hasWikiEditor && needSave ) {
604 context.fn.saveSelection();