Merge "Use HTTPS instead of HTTP for interwiki"
[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 $.fn.textSelection = function ( command, options ) {
6 var fn,
7 alternateFn,
8 retval;
9
10 fn = {
11 /**
12 * Get the contents of the textarea
13 *
14 * @return {string}
15 */
16 getContents: function () {
17 return this.val();
18 },
19 /**
20 * Set the contents of the textarea, replacing anything that was there before
21 *
22 * @param {string} content
23 */
24 setContents: function ( content ) {
25 this.val( content );
26 },
27 /**
28 * Get the currently selected text in this textarea.
29 *
30 * @return {string}
31 */
32 getSelection: function () {
33 var retval,
34 el = this.get( 0 );
35
36 if ( !el || $( el ).is( ':hidden' ) ) {
37 retval = '';
38 } else if ( el.selectionStart || el.selectionStart === 0 ) {
39 retval = el.value.substring( el.selectionStart, el.selectionEnd );
40 }
41
42 return retval;
43 },
44 /**
45 * Ported from skins/common/edit.js by Trevor Parscal
46 * (c) 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
47 *
48 * Inserts text at the beginning and end of a text selection, optionally
49 * inserting text at the caret when selection is empty.
50 *
51 * @param {Object} options Options
52 * FIXME document the options parameters
53 * @return {jQuery}
54 */
55 encapsulateSelection: function ( options ) {
56 return this.each( function () {
57 var selText, scrollTop, insertText,
58 isSample, startPos, endPos,
59 pre = options.pre,
60 post = options.post;
61
62 /**
63 * Check if the selected text is the same as the insert text
64 */
65 function checkSelectedText() {
66 if ( !selText ) {
67 selText = options.peri;
68 isSample = true;
69 } else if ( options.replace ) {
70 selText = options.peri;
71 } else {
72 while ( selText.charAt( selText.length - 1 ) === ' ' ) {
73 // Exclude ending space char
74 selText = selText.slice( 0, -1 );
75 post += ' ';
76 }
77 while ( selText.charAt( 0 ) === ' ' ) {
78 // Exclude prepending space char
79 selText = selText.slice( 1 );
80 pre = ' ' + pre;
81 }
82 }
83 }
84
85 /**
86 * Do the splitlines stuff.
87 *
88 * Wrap each line of the selected text with pre and post
89 *
90 * @param {string} selText Selected text
91 * @param {string} pre Text before
92 * @param {string} post Text after
93 * @return {string} Wrapped text
94 */
95 function doSplitLines( selText, pre, post ) {
96 var i,
97 insertText = '',
98 selTextArr = selText.split( '\n' );
99 for ( i = 0; i < selTextArr.length; i++ ) {
100 insertText += pre + selTextArr[ i ] + post;
101 if ( i !== selTextArr.length - 1 ) {
102 insertText += '\n';
103 }
104 }
105 return insertText;
106 }
107
108 isSample = false;
109 // Do nothing if display none
110 if ( this.style.display !== 'none' ) {
111 if ( this.selectionStart || this.selectionStart === 0 ) {
112 $( this ).focus();
113 if ( options.selectionStart !== undefined ) {
114 $( this ).textSelection( 'setSelection', { start: options.selectionStart, end: options.selectionEnd } );
115 }
116
117 selText = $( this ).textSelection( 'getSelection' );
118 startPos = this.selectionStart;
119 endPos = this.selectionEnd;
120 scrollTop = this.scrollTop;
121 checkSelectedText();
122 if (
123 options.selectionStart !== undefined &&
124 endPos - startPos !== options.selectionEnd - options.selectionStart
125 ) {
126 // This means there is a difference in the selection range returned by browser and what we passed.
127 // This happens for Chrome in the case of composite characters. Ref bug #30130
128 // Set the startPos to the correct position.
129 startPos = options.selectionStart;
130 }
131
132 insertText = pre + selText + post;
133 if ( options.splitlines ) {
134 insertText = doSplitLines( selText, pre, post );
135 }
136 if ( options.ownline ) {
137 if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== '\n' && this.value.charAt( startPos - 1 ) !== '\r' ) {
138 insertText = '\n' + insertText;
139 pre += '\n';
140 }
141 if ( this.value.charAt( endPos ) !== '\n' && this.value.charAt( endPos ) !== '\r' ) {
142 insertText += '\n';
143 post += '\n';
144 }
145 }
146 this.value = this.value.slice( 0, startPos ) + insertText +
147 this.value.slice( endPos );
148 // Setting this.value scrolls the textarea to the top, restore the scroll position
149 this.scrollTop = scrollTop;
150 if ( window.opera ) {
151 pre = pre.replace( /\r?\n/g, '\r\n' );
152 selText = selText.replace( /\r?\n/g, '\r\n' );
153 post = post.replace( /\r?\n/g, '\r\n' );
154 }
155 if ( isSample && options.selectPeri && ( !options.splitlines || ( options.splitlines && selText.indexOf( '\n' ) === -1 ) ) ) {
156 this.selectionStart = startPos + pre.length;
157 this.selectionEnd = startPos + pre.length + selText.length;
158 } else {
159 this.selectionStart = startPos + insertText.length;
160 this.selectionEnd = this.selectionStart;
161 }
162 }
163 }
164 $( this ).trigger( 'encapsulateSelection', [ options.pre, options.peri, options.post, options.ownline,
165 options.replace, options.splitlines ] );
166 } );
167 },
168 /**
169 * Ported from Wikia's LinkSuggest extension
170 * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
171 *
172 * Get the position (in resolution of bytes not necessarily characters)
173 * in a textarea
174 *
175 * @param {Object} options Options
176 * FIXME document the options parameters
177 * @return {number} Position
178 */
179 getCaretPosition: function ( options ) {
180 function getCaret( e ) {
181 var caretPos = 0,
182 endPos = 0;
183
184 if ( e && ( e.selectionStart || e.selectionStart === 0 ) ) {
185 caretPos = e.selectionStart;
186 endPos = e.selectionEnd;
187 }
188 return options.startAndEnd ? [ caretPos, endPos ] : caretPos;
189 }
190 return getCaret( this.get( 0 ) );
191 },
192 /**
193 * @param {Object} options options
194 * FIXME document the options parameters
195 * @return {jQuery}
196 */
197 setSelection: function ( options ) {
198 return this.each( function () {
199 // Do nothing if hidden
200 if ( !$( this ).is( ':hidden' ) ) {
201 if ( this.selectionStart || this.selectionStart === 0 ) {
202 // Opera 9.0 doesn't allow setting selectionStart past
203 // selectionEnd; any attempts to do that will be ignored
204 // Make sure to set them in the right order
205 if ( options.start > this.selectionEnd ) {
206 this.selectionEnd = options.end;
207 this.selectionStart = options.start;
208 } else {
209 this.selectionStart = options.start;
210 this.selectionEnd = options.end;
211 }
212 }
213 }
214 } );
215 },
216 /**
217 * Ported from Wikia's LinkSuggest extension
218 * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
219 *
220 * Scroll a textarea to the current cursor position. You can set the cursor
221 * position with setSelection()
222 *
223 * @param {Object} options options
224 * @cfg {boolean} [force=false] Whether to force a scroll even if the caret position
225 * is already visible.
226 * FIXME document the options parameters
227 * @return {jQuery}
228 */
229 scrollToCaretPosition: function ( options ) {
230 function getLineLength( e ) {
231 return Math.floor( e.scrollWidth / ( $.client.profile().platform === 'linux' ? 7 : 8 ) );
232 }
233 function getCaretScrollPosition( e ) {
234 // FIXME: This functions sucks and is off by a few lines most
235 // of the time. It should be replaced by something decent.
236 var i, j,
237 nextSpace,
238 text = e.value.replace( /\r/g, '' ),
239 caret = $( e ).textSelection( 'getCaretPosition' ),
240 lineLength = getLineLength( e ),
241 row = 0,
242 charInLine = 0,
243 lastSpaceInLine = 0;
244
245 for ( i = 0; i < caret; i++ ) {
246 charInLine++;
247 if ( text.charAt( i ) === ' ' ) {
248 lastSpaceInLine = charInLine;
249 } else if ( text.charAt( i ) === '\n' ) {
250 lastSpaceInLine = 0;
251 charInLine = 0;
252 row++;
253 }
254 if ( charInLine > lineLength ) {
255 if ( lastSpaceInLine > 0 ) {
256 charInLine = charInLine - lastSpaceInLine;
257 lastSpaceInLine = 0;
258 row++;
259 }
260 }
261 }
262 nextSpace = 0;
263 for ( j = caret; j < caret + lineLength; j++ ) {
264 if (
265 text.charAt( j ) === ' ' ||
266 text.charAt( j ) === '\n' ||
267 caret === text.length
268 ) {
269 nextSpace = j;
270 break;
271 }
272 }
273 if ( nextSpace > lineLength && caret <= lineLength ) {
274 charInLine = caret - lastSpaceInLine;
275 row++;
276 }
277 return ( $.client.profile().platform === 'mac' ? 13 : ( $.client.profile().platform === 'linux' ? 15 : 16 ) ) * row;
278 }
279 return this.each( function () {
280 var scroll;
281 // Do nothing if hidden
282 if ( !$( this ).is( ':hidden' ) ) {
283 if ( this.selectionStart || this.selectionStart === 0 ) {
284 scroll = getCaretScrollPosition( this );
285 if ( options.force || scroll < $( this ).scrollTop() ||
286 scroll > $( this ).scrollTop() + $( this ).height() ) {
287 $( this ).scrollTop( scroll );
288 }
289 }
290 }
291 $( this ).trigger( 'scrollToPosition' );
292 } );
293 }
294 };
295
296 alternateFn = $( this ).data( 'jquery.textSelection' );
297
298 // Apply defaults
299 switch ( command ) {
300 // case 'getContents': // no params
301 // case 'setContents': // no params with defaults
302 // case 'getSelection': // no params
303 case 'encapsulateSelection':
304 options = $.extend( {
305 pre: '', // Text to insert before the cursor/selection
306 peri: '', // Text to insert between pre and post and select afterwards
307 post: '', // Text to insert after the cursor/selection
308 ownline: false, // Put the inserted text on a line of its own
309 replace: false, // If there is a selection, replace it with peri instead of leaving it alone
310 selectPeri: true, // Select the peri text if it was inserted (but not if there was a selection and replace==false, or if splitlines==true)
311 splitlines: false, // If multiple lines are selected, encapsulate each line individually
312 selectionStart: undefined, // Position to start selection at
313 selectionEnd: undefined // Position to end selection at. Defaults to start
314 }, options );
315 break;
316 case 'getCaretPosition':
317 options = $.extend( {
318 // Return [start, end] instead of just start
319 startAndEnd: false
320 }, options );
321 break;
322 case 'setSelection':
323 options = $.extend( {
324 // Position to start selection at
325 start: undefined,
326 // Position to end selection at. Defaults to start
327 end: undefined
328 }, options );
329
330 if ( options.end === undefined ) {
331 options.end = options.start;
332 }
333 break;
334 case 'scrollToCaretPosition':
335 options = $.extend( {
336 force: false // Force a scroll even if the caret position is already visible
337 }, options );
338 break;
339 case 'register':
340 if ( alternateFn ) {
341 throw new Error( 'Another textSelection API was already registered' );
342 }
343 $( this ).data( 'jquery.textSelection', options );
344 // No need to update alternateFn as this command only stores the options.
345 // A command that uses it will set it again.
346 return;
347 case 'unregister':
348 $( this ).removeData( 'jquery.textSelection' );
349 return;
350 }
351
352 retval = ( alternateFn && alternateFn[ command ] || fn[ command ] ).call( this, options );
353
354 return retval;
355 };
356
357 }( jQuery ) );