Merge "rdbms: apply minimum sanity timeout for all cases of LoadBalancer::doWait()"
[lhc/web/wiklou.git] / resources / src / jquery / jquery.textSelection.js
1 /*!
2 * These plugins provide extra functionality for interaction with textareas.
3 *
4 * - encapsulateSelection: Ported from skins/common/edit.js by Trevor Parscal
5 * © 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
6 * - getCaretPosition, scrollToCaretPosition: Ported from Wikia's LinkSuggest extension
7 * https://github.com/Wikia/app/blob/c0cd8b763/extensions/wikia/LinkSuggest/js/jquery.wikia.linksuggest.js
8 * © 2010 Inez Korczyński (korczynski@gmail.com) & Jesús Martínez Novo (martineznovo@gmail.com) (GPLv2)
9 */
10
11 /**
12 * @class jQuery.plugin.textSelection
13 *
14 * Do things to the selection in a `<textarea>`, or a textarea-like editable element.
15 *
16 * var $textbox = $( '#wpTextbox1' );
17 * $textbox.textSelection( 'setContents', 'This is bold!' );
18 * $textbox.textSelection( 'setSelection', { start: 8, end: 12 } );
19 * $textbox.textSelection( 'encapsulateSelection', { pre: '<b>', post: '</b>' } );
20 * // Result: Textbox contains 'This is <b>bold</b>!', with cursor before the '!'
21 */
22 ( function ( $ ) {
23 /**
24 * Do things to the selection in a `<textarea>`, or a textarea-like editable element.
25 *
26 * var $textbox = $( '#wpTextbox1' );
27 * $textbox.textSelection( 'setContents', 'This is bold!' );
28 * $textbox.textSelection( 'setSelection', { start: 8, end: 12 } );
29 * $textbox.textSelection( 'encapsulateSelection', { pre: '<b>', post: '</b>' } );
30 * // Result: Textbox contains 'This is <b>bold</b>!', with cursor before the '!'
31 *
32 * @param {string} command Command to execute, one of:
33 *
34 * - {@link jQuery.plugin.textSelection#getContents getContents}
35 * - {@link jQuery.plugin.textSelection#setContents setContents}
36 * - {@link jQuery.plugin.textSelection#getSelection getSelection}
37 * - {@link jQuery.plugin.textSelection#encapsulateSelection encapsulateSelection}
38 * - {@link jQuery.plugin.textSelection#getCaretPosition getCaretPosition}
39 * - {@link jQuery.plugin.textSelection#setSelection setSelection}
40 * - {@link jQuery.plugin.textSelection#scrollToCaretPosition scrollToCaretPosition}
41 * - {@link jQuery.plugin.textSelection#register register}
42 * - {@link jQuery.plugin.textSelection#unregister unregister}
43 * @param {Mixed} [options] Options to pass to the command
44 * @return {Mixed} Depending on the command
45 */
46 $.fn.textSelection = function ( command, options ) {
47 var fn,
48 alternateFn,
49 retval;
50
51 fn = {
52 /**
53 * Get the contents of the textarea.
54 *
55 * @private
56 * @return {string}
57 */
58 getContents: function () {
59 return this.val();
60 },
61
62 /**
63 * Set the contents of the textarea, replacing anything that was there before.
64 *
65 * @private
66 * @param {string} content
67 */
68 setContents: function ( content ) {
69 this.val( content );
70 },
71
72 /**
73 * Get the currently selected text in this textarea.
74 *
75 * @private
76 * @return {string}
77 */
78 getSelection: function () {
79 var retval,
80 el = this.get( 0 );
81
82 if ( !el ) {
83 retval = '';
84 } else {
85 retval = el.value.substring( el.selectionStart, el.selectionEnd );
86 }
87
88 return retval;
89 },
90
91 /**
92 * Insert text at the beginning and end of a text selection, optionally
93 * inserting text at the caret when selection is empty.
94 *
95 * Also focusses the textarea.
96 *
97 * @private
98 * @param {Object} [options]
99 * @param {string} [options.pre] Text to insert before the cursor/selection
100 * @param {string} [options.peri] Text to insert between pre and post and select afterwards
101 * @param {string} [options.post] Text to insert after the cursor/selection
102 * @param {boolean} [options.ownline=false] Put the inserted text on a line of its own
103 * @param {boolean} [options.replace=false] If there is a selection, replace it with peri
104 * instead of leaving it alone
105 * @param {boolean} [options.selectPeri=true] Select the peri text if it was inserted (but not
106 * if there was a selection and replace==false, or if splitlines==true)
107 * @param {boolean} [options.splitlines=false] If multiple lines are selected, encapsulate
108 * each line individually
109 * @param {number} [options.selectionStart] Position to start selection at
110 * @param {number} [options.selectionEnd=options.selectionStart] Position to end selection at
111 * @return {jQuery}
112 * @chainable
113 */
114 encapsulateSelection: function ( options ) {
115 return this.each( function () {
116 var selText, scrollTop, insertText,
117 isSample, startPos, endPos,
118 pre = options.pre,
119 post = options.post;
120
121 /**
122 * @ignore
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 * @ignore
147 * Do the splitlines stuff.
148 *
149 * Wrap each line of the selected text with pre and post
150 *
151 * @param {string} selText Selected text
152 * @param {string} pre Text before
153 * @param {string} post Text after
154 * @return {string} Wrapped text
155 */
156 function doSplitLines( selText, pre, post ) {
157 var i,
158 insertText = '',
159 selTextArr = selText.split( '\n' );
160 for ( i = 0; i < selTextArr.length; i++ ) {
161 insertText += pre + selTextArr[ i ] + post;
162 if ( i !== selTextArr.length - 1 ) {
163 insertText += '\n';
164 }
165 }
166 return insertText;
167 }
168
169 isSample = false;
170 $( this ).focus();
171 if ( options.selectionStart !== undefined ) {
172 $( this ).textSelection( 'setSelection', { start: options.selectionStart, end: options.selectionEnd } );
173 }
174
175 selText = $( this ).textSelection( 'getSelection' );
176 startPos = this.selectionStart;
177 endPos = this.selectionEnd;
178 scrollTop = this.scrollTop;
179 checkSelectedText();
180 if (
181 options.selectionStart !== undefined &&
182 endPos - startPos !== options.selectionEnd - options.selectionStart
183 ) {
184 // This means there is a difference in the selection range returned by browser and what we passed.
185 // This happens for Chrome in the case of composite characters. Ref bug #30130
186 // Set the startPos to the correct position.
187 startPos = options.selectionStart;
188 }
189
190 insertText = pre + selText + post;
191 if ( options.splitlines ) {
192 insertText = doSplitLines( selText, pre, post );
193 }
194 if ( options.ownline ) {
195 if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== '\n' && this.value.charAt( startPos - 1 ) !== '\r' ) {
196 insertText = '\n' + insertText;
197 pre += '\n';
198 }
199 if ( this.value.charAt( endPos ) !== '\n' && this.value.charAt( endPos ) !== '\r' ) {
200 insertText += '\n';
201 post += '\n';
202 }
203 }
204 this.value = this.value.slice( 0, startPos ) + insertText +
205 this.value.slice( endPos );
206 // Setting this.value scrolls the textarea to the top, restore the scroll position
207 this.scrollTop = scrollTop;
208 if ( isSample && options.selectPeri && ( !options.splitlines || ( options.splitlines && selText.indexOf( '\n' ) === -1 ) ) ) {
209 this.selectionStart = startPos + pre.length;
210 this.selectionEnd = startPos + pre.length + selText.length;
211 } else {
212 this.selectionStart = startPos + insertText.length;
213 this.selectionEnd = this.selectionStart;
214 }
215 $( this ).trigger( 'encapsulateSelection', [ options.pre, options.peri, options.post, options.ownline,
216 options.replace, options.splitlines ] );
217 } );
218 },
219
220 /**
221 * Get the current cursor position (in UTF-16 code units) in a textarea.
222 *
223 * @private
224 * @param {Object} [options]
225 * @param {Object} [options.startAndEnd=false] Return range of the selection rather than just start
226 * @return {Mixed}
227 * - When `startAndEnd` is `false`: number
228 * - When `startAndEnd` is `true`: array with two numbers, for start and end of selection
229 */
230 getCaretPosition: function ( options ) {
231 function getCaret( e ) {
232 var caretPos = 0,
233 endPos = 0;
234 if ( e ) {
235 caretPos = e.selectionStart;
236 endPos = e.selectionEnd;
237 }
238 return options.startAndEnd ? [ caretPos, endPos ] : caretPos;
239 }
240 return getCaret( this.get( 0 ) );
241 },
242
243 /**
244 * Set the current cursor position (in UTF-16 code units) in a textarea.
245 *
246 * @private
247 * @param {Object} [options]
248 * @param {number} options.start
249 * @param {number} [options.end=options.start]
250 * @return {jQuery}
251 * @chainable
252 */
253 setSelection: function ( options ) {
254 return this.each( function () {
255 // Opera 9.0 doesn't allow setting selectionStart past
256 // selectionEnd; any attempts to do that will be ignored
257 // Make sure to set them in the right order
258 if ( options.start > this.selectionEnd ) {
259 this.selectionEnd = options.end;
260 this.selectionStart = options.start;
261 } else {
262 this.selectionStart = options.start;
263 this.selectionEnd = options.end;
264 }
265 } );
266 },
267
268 /**
269 * Scroll a textarea to the current cursor position. You can set the cursor
270 * position with #setSelection.
271 *
272 * @private
273 * @param {Object} [options]
274 * @param {string} [options.force=false] Whether to force a scroll even if the caret position
275 * is already visible.
276 * @return {jQuery}
277 * @chainable
278 */
279 scrollToCaretPosition: function ( options ) {
280 return this.each( function () {
281 var
282 clientHeight = this.clientHeight,
283 origValue = this.value,
284 origSelectionStart = this.selectionStart,
285 origSelectionEnd = this.selectionEnd,
286 origScrollTop = this.scrollTop,
287 calcScrollTop;
288
289 // Delete all text after the selection and scroll the textarea to the end.
290 // This ensures the selection is visible (aligned to the bottom of the textarea).
291 // Then restore the text we deleted without changing scroll position.
292 this.value = this.value.slice( 0, this.selectionEnd );
293 this.scrollTop = this.scrollHeight;
294 // Chrome likes to adjust scroll position when changing value, so save and re-set later.
295 // Note that this is not equal to scrollHeight, it's scrollHeight minus clientHeight.
296 calcScrollTop = this.scrollTop;
297 this.value = origValue;
298 this.selectionStart = origSelectionStart;
299 this.selectionEnd = origSelectionEnd;
300
301 if ( !options.force ) {
302 // Check if all the scrolling was unnecessary and if so, restore previous position.
303 // If the current position is no more than a screenful above the original,
304 // the selection was previously visible on the screen.
305 if ( calcScrollTop < origScrollTop && origScrollTop - calcScrollTop < clientHeight ) {
306 calcScrollTop = origScrollTop;
307 }
308 }
309
310 this.scrollTop = calcScrollTop;
311
312 $( this ).trigger( 'scrollToPosition' );
313 } );
314 }
315 };
316
317 /**
318 * @method register
319 *
320 * Register an alternative textSelection API for this element.
321 *
322 * @private
323 * @param {Object} functions Functions to replace. Keys are command names (as in #textSelection,
324 * except 'register' and 'unregister'). Values are functions to execute when a given command is
325 * called.
326 */
327
328 /**
329 * @method unregister
330 *
331 * Unregister the alternative textSelection API for this element (see #register).
332 *
333 * @private
334 */
335
336 alternateFn = $( this ).data( 'jquery.textSelection' );
337
338 // Apply defaults
339 switch ( command ) {
340 // case 'getContents': // no params
341 // case 'setContents': // no params with defaults
342 // case 'getSelection': // no params
343 case 'encapsulateSelection':
344 options = $.extend( {
345 pre: '',
346 peri: '',
347 post: '',
348 ownline: false,
349 replace: false,
350 selectPeri: true,
351 splitlines: false,
352 selectionStart: undefined,
353 selectionEnd: undefined
354 }, options );
355 break;
356 case 'getCaretPosition':
357 options = $.extend( {
358 startAndEnd: false
359 }, options );
360 break;
361 case 'setSelection':
362 options = $.extend( {
363 start: undefined,
364 end: undefined
365 }, options );
366 if ( options.end === undefined ) {
367 options.end = options.start;
368 }
369 break;
370 case 'scrollToCaretPosition':
371 options = $.extend( {
372 force: false
373 }, options );
374 break;
375 case 'register':
376 if ( alternateFn ) {
377 throw new Error( 'Another textSelection API was already registered' );
378 }
379 $( this ).data( 'jquery.textSelection', options );
380 // No need to update alternateFn as this command only stores the options.
381 // A command that uses it will set it again.
382 return;
383 case 'unregister':
384 $( this ).removeData( 'jquery.textSelection' );
385 return;
386 }
387
388 retval = ( alternateFn && alternateFn[ command ] || fn[ command ] ).call( this, options );
389
390 return retval;
391 };
392
393 /**
394 * @class jQuery
395 */
396 /**
397 * @method textSelection
398 * @inheritdoc jQuery.plugin.textSelection#textSelection
399 */
400
401 }( jQuery ) );