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