Merge "Added a separate error message for mkdir failures"
[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 context,
29 hasWikiEditor,
30 needSave,
31 retval;
32
33 /**
34 * Helper function to get an IE TextRange object for an element
35 *
36 * @param {HTMLElement} element
37 * @return {TextRange}
38 */
39 function rangeForElementIE( element ) {
40 var sel;
41 if ( element.nodeName.toLowerCase() === 'input' ) {
42 return element.createTextRange();
43 } else {
44 sel = document.body.createTextRange();
45 sel.moveToElementText( element );
46 return sel;
47 }
48 }
49
50 /**
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.
54 *
55 * @param {HTMLElement} element
56 */
57 function activateElementOnIE( element ) {
58 if ( element.setActive ) {
59 element.setActive(); // T34241: doesn't scroll
60 } else {
61 $( element ).focus(); // may scroll (but we patched it above)
62 }
63 }
64
65 fn = {
66 /**
67 * Get the contents of the textarea
68 *
69 * @return {string}
70 */
71 getContents: function () {
72 return this.val();
73 },
74 /**
75 * Set the contents of the textarea, replacing anything that was there before
76 *
77 * @param {string} content
78 */
79 setContents: function ( content ) {
80 this.val( content );
81 },
82 /**
83 * Get the currently selected text in this textarea. Will focus the textarea
84 * in some browsers (IE/Opera)
85 *
86 * @return {string}
87 */
88 getSelection: function () {
89 var retval, range,
90 el = this.get( 0 );
91
92 if ( !el || $( el ).is( ':hidden' ) ) {
93 retval = '';
94 } else if ( document.selection && document.selection.createRange ) {
95 activateElementOnIE( el );
96 range = document.selection.createRange();
97 retval = range.text;
98 } else if ( el.selectionStart || el.selectionStart === 0 ) {
99 retval = el.value.substring( el.selectionStart, el.selectionEnd );
100 }
101
102 return retval;
103 },
104 /**
105 * Ported from skins/common/edit.js by Trevor Parscal
106 * (c) 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
107 *
108 * Inserts text at the beginning and end of a text selection, optionally
109 * inserting text at the caret when selection is empty.
110 *
111 * @param {Object} options Options
112 * FIXME document the options parameters
113 * @return {jQuery}
114 */
115 encapsulateSelection: function ( options ) {
116 return this.each( function () {
117 var selText, scrollTop, insertText,
118 isSample, range, range2, range3, startPos, endPos,
119 pre = options.pre,
120 post = options.post;
121
122 /**
123 * Check if the selected text is the same as the insert text
124 */
125 function checkSelectedText() {
126 if ( !selText ) {
127 selText = options.peri;
128 isSample = true;
129 } else if ( options.replace ) {
130 selText = options.peri;
131 } else {
132 while ( selText.charAt( selText.length - 1 ) === ' ' ) {
133 // Exclude ending space char
134 selText = selText.slice( 0, -1 );
135 post += ' ';
136 }
137 while ( selText.charAt( 0 ) === ' ' ) {
138 // Exclude prepending space char
139 selText = selText.slice( 1 );
140 pre = ' ' + pre;
141 }
142 }
143 }
144
145 /**
146 * Do the splitlines stuff.
147 *
148 * Wrap each line of the selected text with pre and post
149 *
150 * @param {string} selText Selected text
151 * @param {string} pre Text before
152 * @param {string} post Text after
153 * @return {string} Wrapped text
154 */
155 function doSplitLines( selText, pre, post ) {
156 var i,
157 insertText = '',
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 ) {
162 insertText += '\n';
163 }
164 }
165 return insertText;
166 }
167
168 isSample = false;
169 // Do nothing if display none
170 if ( this.style.display !== 'none' ) {
171 if ( document.selection && document.selection.createRange ) {
172 // IE
173
174 // Note that IE9 will trigger the next section unless we check this first.
175 // See bug T37201.
176
177 activateElementOnIE( this );
178 if ( context ) {
179 context.fn.restoreCursorAndScrollTop();
180 }
181 if ( options.selectionStart !== undefined ) {
182 $( this ).textSelection( 'setSelection', { start: options.selectionStart, end: options.selectionEnd } );
183 }
184
185 selText = $( this ).textSelection( 'getSelection' );
186 scrollTop = this.scrollTop;
187 range = document.selection.createRange();
188
189 checkSelectedText();
190 insertText = pre + selText + post;
191 if ( options.splitlines ) {
192 insertText = doSplitLines( selText, pre, post );
193 }
194 if ( options.ownline && range.moveStart ) {
195 range2 = document.selection.createRange();
196 range2.collapse();
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;
201 pre += '\n';
202 }
203 range3 = document.selection.createRange();
204 range3.collapse( false );
205 range3.moveEnd( 'character', 1 );
206 if ( range3.text !== '\r' && range3.text !== '\n' && range3.text !== '' ) {
207 insertText += '\n';
208 post += '\n';
209 }
210 }
211
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 );
216 }
217 range.select();
218 // Restore the scroll position
219 this.scrollTop = scrollTop;
220 } else if ( this.selectionStart || this.selectionStart === 0 ) {
221 // Mozilla/Opera
222
223 $( this ).focus();
224 if ( options.selectionStart !== undefined ) {
225 $( this ).textSelection( 'setSelection', { start: options.selectionStart, end: options.selectionEnd } );
226 }
227
228 selText = $( this ).textSelection( 'getSelection' );
229 startPos = this.selectionStart;
230 endPos = this.selectionEnd;
231 scrollTop = this.scrollTop;
232 checkSelectedText();
233 if (
234 options.selectionStart !== undefined &&
235 endPos - startPos !== options.selectionEnd - options.selectionStart
236 ) {
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;
241 }
242
243 insertText = pre + selText + post;
244 if ( options.splitlines ) {
245 insertText = doSplitLines( selText, pre, post );
246 }
247 if ( options.ownline ) {
248 if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== '\n' && this.value.charAt( startPos - 1 ) !== '\r' ) {
249 insertText = '\n' + insertText;
250 pre += '\n';
251 }
252 if ( this.value.charAt( endPos ) !== '\n' && this.value.charAt( endPos ) !== '\r' ) {
253 insertText += '\n';
254 post += '\n';
255 }
256 }
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' );
265 }
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;
269 } else {
270 this.selectionStart = startPos + insertText.length;
271 this.selectionEnd = this.selectionStart;
272 }
273 }
274 }
275 $( this ).trigger( 'encapsulateSelection', [ options.pre, options.peri, options.post, options.ownline,
276 options.replace, options.spitlines ] );
277 } );
278 },
279 /**
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/
284 *
285 * Get the position (in resolution of bytes not necessarily characters)
286 * in a textarea
287 *
288 * Will focus the textarea in some browsers (IE/Opera)
289 *
290 * @param {Object} options Options
291 * FIXME document the options parameters
292 * @return {number} Position
293 */
294 getCaretPosition: function ( options ) {
295 function getCaret( e ) {
296 var caretPos = 0,
297 endPos = 0,
298 preText, rawPreText, periText,
299 rawPeriText, postText,
300 // IE Support
301 preFinished,
302 periFinished,
303 postFinished,
304 // Range containing text in the selection
305 periRange,
306 // Range containing text before the selection
307 preRange,
308 // Range containing text after the selection
309 postRange;
310
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 );
317
318 preFinished = false;
319 periFinished = false;
320 postFinished = false;
321 periRange = document.selection.createRange().duplicate();
322
323 preRange = rangeForElementIE( e );
324 // Move the end where we need it
325 preRange.setEndPoint( 'EndToStart', periRange );
326
327 postRange = rangeForElementIE( e );
328 // Move the start where we need it
329 postRange.setEndPoint( 'StartToEnd', periRange );
330
331 // Load the text values we need to compare
332 preText = rawPreText = preRange.text;
333 periText = rawPeriText = periRange.text;
334 postText = postRange.text;
335
336 /*
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.
340 */
341 do {
342 if ( !preFinished ) {
343 if ( preRange.compareEndPoints( 'StartToEnd', preRange ) === 0 ) {
344 preFinished = true;
345 } else {
346 preRange.moveEnd( 'character', -1 );
347 if ( preRange.text === preText ) {
348 rawPreText += '\r\n';
349 } else {
350 preFinished = true;
351 }
352 }
353 }
354 if ( !periFinished ) {
355 if ( periRange.compareEndPoints( 'StartToEnd', periRange ) === 0 ) {
356 periFinished = true;
357 } else {
358 periRange.moveEnd( 'character', -1 );
359 if ( periRange.text === periText ) {
360 rawPeriText += '\r\n';
361 } else {
362 periFinished = true;
363 }
364 }
365 }
366 if ( !postFinished ) {
367 if ( postRange.compareEndPoints( 'StartToEnd', postRange ) === 0 ) {
368 postFinished = true;
369 } else {
370 postRange.moveEnd( 'character', -1 );
371 if ( postRange.text !== postText ) {
372 postFinished = true;
373 }
374 }
375 }
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 ) ) {
380 // Firefox support
381 caretPos = e.selectionStart;
382 endPos = e.selectionEnd;
383 }
384 return options.startAndEnd ? [ caretPos, endPos ] : caretPos;
385 }
386 return getCaret( this.get( 0 ) );
387 },
388 /**
389 * @param {Object} options options
390 * FIXME document the options parameters
391 * @return {jQuery}
392 */
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;
405 } else {
406 this.selectionStart = options.start;
407 this.selectionEnd = options.end;
408 }
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 );
414 if ( newLines ) {
415 length = length - newLines.length;
416 }
417 selection.moveStart( 'character', options.start );
418 selection.moveEnd( 'character', -length + options.end );
419
420 // This line can cause an error under certain circumstances (textarea empty, no selection)
421 // Silence that error
422 try {
423 selection.select();
424 } catch ( e ) { }
425 }
426 }
427 } );
428 },
429 /**
430 * Ported from Wikia's LinkSuggest extension
431 * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
432 *
433 * Scroll a textarea to the current cursor position. You can set the cursor
434 * position with setSelection()
435 *
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
440 * @return {jQuery}
441 */
442 scrollToCaretPosition: function ( options ) {
443 function getLineLength( e ) {
444 return Math.floor( e.scrollWidth / ( $.client.profile().platform === 'linux' ? 7 : 8 ) );
445 }
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.
449 var i, j,
450 nextSpace,
451 text = e.value.replace( /\r/g, '' ),
452 caret = $( e ).textSelection( 'getCaretPosition' ),
453 lineLength = getLineLength( e ),
454 row = 0,
455 charInLine = 0,
456 lastSpaceInLine = 0;
457
458 for ( i = 0; i < caret; i++ ) {
459 charInLine++;
460 if ( text.charAt( i ) === ' ' ) {
461 lastSpaceInLine = charInLine;
462 } else if ( text.charAt( i ) === '\n' ) {
463 lastSpaceInLine = 0;
464 charInLine = 0;
465 row++;
466 }
467 if ( charInLine > lineLength ) {
468 if ( lastSpaceInLine > 0 ) {
469 charInLine = charInLine - lastSpaceInLine;
470 lastSpaceInLine = 0;
471 row++;
472 }
473 }
474 }
475 nextSpace = 0;
476 for ( j = caret; j < caret + lineLength; j++ ) {
477 if (
478 text.charAt( j ) === ' ' ||
479 text.charAt( j ) === '\n' ||
480 caret === text.length
481 ) {
482 nextSpace = j;
483 break;
484 }
485 }
486 if ( nextSpace > lineLength && caret <= lineLength ) {
487 charInLine = caret - lastSpaceInLine;
488 row++;
489 }
490 return ( $.client.profile().platform === 'mac' ? 13 : ( $.client.profile().platform === 'linux' ? 15 : 16 ) ) * row;
491 }
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 ) {
497 // Mozilla
498 scroll = getCaretScrollPosition( this );
499 if ( options.force || scroll < $( this ).scrollTop() ||
500 scroll > $( this ).scrollTop() + $( this ).height() ) {
501 $( this ).scrollTop( scroll );
502 }
503 } else if ( document.selection && document.selection.createRange ) {
504 // IE / Opera
505 /*
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.
512 */
513 range = document.body.createTextRange();
514 savedRange = document.selection.createRange();
515 pos = $( this ).textSelection( 'getCaretPosition' );
516 oldScrollTop = this.scrollTop;
517 range.moveToElementText( this );
518 range.collapse();
519 range.move( 'character', pos + 1 );
520 range.select();
521 if ( this.scrollTop !== oldScrollTop ) {
522 this.scrollTop += range.offsetTop;
523 } else if ( options.force ) {
524 range.move( 'character', -1 );
525 range.select();
526 }
527 savedRange.select();
528 }
529 }
530 $( this ).trigger( 'scrollToPosition' );
531 } );
532 }
533 };
534
535 alternateFn = $( this ).data( 'jquery.textSelection' );
536
537 // Apply defaults
538 switch ( command ) {
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
553 }, options );
554 break;
555 case 'getCaretPosition':
556 options = $.extend( {
557 // Return [start, end] instead of just start
558 startAndEnd: false
559 }, options );
560 // FIXME: We may not need character position-based functions if we insert markers in the right places
561 break;
562 case 'setSelection':
563 options = $.extend( {
564 // Position to start selection at
565 start: undefined,
566 // Position to end selection at. Defaults to start
567 end: undefined
568 }, options );
569
570 if ( options.end === undefined ) {
571 options.end = options.start;
572 }
573 // FIXME: We may not need character position-based functions if we insert markers in the right places
574 break;
575 case 'scrollToCaretPosition':
576 options = $.extend( {
577 force: false // Force a scroll even if the caret position is already visible
578 }, options );
579 break;
580 case 'register':
581 if ( alternateFn ) {
582 throw new Error( 'Another textSelection API was already registered' );
583 }
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.
587 return;
588 case 'unregister':
589 $( this ).removeData( 'jquery.textSelection' );
590 return;
591 }
592
593 context = $( this ).data( 'wikiEditor-context' );
594 hasWikiEditor = ( context !== undefined && context.$iframe !== undefined );
595
596 // IE selection restore voodoo
597 needSave = false;
598 if ( hasWikiEditor && context.savedSelection !== null ) {
599 context.fn.restoreSelection();
600 needSave = true;
601 }
602 retval = ( alternateFn && alternateFn[ command ] || fn[ command ] ).call( this, options );
603 if ( hasWikiEditor && needSave ) {
604 context.fn.saveSelection();
605 }
606
607 return retval;
608 };
609
610 }( jQuery ) );