2 * These plugins provide extra functionality for interaction with textareas.
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)
12 * @class jQuery.plugin.textSelection
14 * Do things to the selection in a `<textarea>`, or a textarea-like editable element.
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 '!'
24 * Do things to the selection in a `<textarea>`, or a textarea-like editable element.
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 '!'
32 * @param {string} command Command to execute, one of:
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#replaceSelection replaceSelection}
38 * - {@link jQuery.plugin.textSelection#encapsulateSelection encapsulateSelection}
39 * - {@link jQuery.plugin.textSelection#getCaretPosition getCaretPosition}
40 * - {@link jQuery.plugin.textSelection#setSelection setSelection}
41 * - {@link jQuery.plugin.textSelection#scrollToCaretPosition scrollToCaretPosition}
42 * - {@link jQuery.plugin.textSelection#register register}
43 * - {@link jQuery.plugin.textSelection#unregister unregister}
44 * @param {Mixed} [options] Options to pass to the command
45 * @return {Mixed} Depending on the command
47 $.fn
.textSelection = function ( command
, options
) {
54 * Get the contents of the textarea.
59 getContents: function () {
64 * Set the contents of the textarea, replacing anything that was there before.
67 * @param {string} content
71 setContents: function ( content
) {
72 return this.each( function () {
73 var scrollTop
= this.scrollTop
;
74 $( this ).val( content
);
75 // Setting this.value may scroll the textarea, restore the scroll position
76 this.scrollTop
= scrollTop
;
81 * Get the currently selected text in this textarea.
86 getSelection: function () {
93 retval
= el
.value
.substring( el
.selectionStart
, el
.selectionEnd
);
100 * Replace the selected text in the textarea with the given text, or insert it at the cursor.
103 * @param {string} value
107 replaceSelection: function ( value
) {
108 return this.each( function () {
109 var allText
, currSelection
, startPos
, endPos
;
111 allText
= $( this ).textSelection( 'getContents' );
112 currSelection
= $( this ).textSelection( 'getCaretPosition', { startAndEnd
: true } );
113 startPos
= currSelection
[ 0 ];
114 endPos
= currSelection
[ 1 ];
116 $( this ).textSelection( 'setContents', allText
.slice( 0, startPos
) + value
+
117 allText
.slice( endPos
) );
118 $( this ).textSelection( 'setSelection', {
120 end
: startPos
+ value
.length
126 * Insert text at the beginning and end of a text selection, optionally
127 * inserting text at the caret when selection is empty.
129 * Also focusses the textarea.
132 * @param {Object} [options]
133 * @param {string} [options.pre] Text to insert before the cursor/selection
134 * @param {string} [options.peri] Text to insert between pre and post and select afterwards
135 * @param {string} [options.post] Text to insert after the cursor/selection
136 * @param {boolean} [options.ownline=false] Put the inserted text on a line of its own
137 * @param {boolean} [options.replace=false] If there is a selection, replace it with peri
138 * instead of leaving it alone
139 * @param {boolean} [options.selectPeri=true] Select the peri text if it was inserted (but not
140 * if there was a selection and replace==false, or if splitlines==true)
141 * @param {boolean} [options.splitlines=false] If multiple lines are selected, encapsulate
142 * each line individually
143 * @param {number} [options.selectionStart] Position to start selection at
144 * @param {number} [options.selectionEnd=options.selectionStart] Position to end selection at
148 encapsulateSelection: function ( options
) {
149 return this.each( function () {
150 var selText
, allText
, currSelection
, insertText
,
151 combiningCharSelectionBug
= false,
152 isSample
, startPos
, endPos
,
158 * Check if the selected text is the same as the insert text
160 function checkSelectedText() {
162 selText
= options
.peri
;
164 } else if ( options
.replace
) {
165 selText
= options
.peri
;
167 while ( selText
.charAt( selText
.length
- 1 ) === ' ' ) {
168 // Exclude ending space char
169 selText
= selText
.slice( 0, -1 );
172 while ( selText
.charAt( 0 ) === ' ' ) {
173 // Exclude prepending space char
174 selText
= selText
.slice( 1 );
182 * Do the splitlines stuff.
184 * Wrap each line of the selected text with pre and post
186 * @param {string} selText Selected text
187 * @param {string} pre Text before
188 * @param {string} post Text after
189 * @return {string} Wrapped text
191 function doSplitLines( selText
, pre
, post
) {
194 selTextArr
= selText
.split( '\n' );
195 for ( i
= 0; i
< selTextArr
.length
; i
++ ) {
196 insertText
+= pre
+ selTextArr
[ i
] + post
;
197 if ( i
!== selTextArr
.length
- 1 ) {
206 if ( options
.selectionStart
!== undefined ) {
207 $( this ).textSelection( 'setSelection', { start
: options
.selectionStart
, end
: options
.selectionEnd
} );
210 selText
= $( this ).textSelection( 'getSelection' );
211 allText
= $( this ).textSelection( 'getContents' );
212 currSelection
= $( this ).textSelection( 'getCaretPosition', { startAndEnd
: true } );
213 startPos
= currSelection
[ 0 ];
214 endPos
= currSelection
[ 1 ];
217 options
.selectionStart
!== undefined &&
218 endPos
- startPos
!== options
.selectionEnd
- options
.selectionStart
220 // This means there is a difference in the selection range returned by browser and what we passed.
221 // This happens for Safari 5.1, Chrome 12 in the case of composite characters. Ref T32130
222 // Set the startPos to the correct position.
223 startPos
= options
.selectionStart
;
224 combiningCharSelectionBug
= true;
225 // TODO: The comment above is from 2011. Is this still a problem for browsers we support today?
226 // Minimal test case: https://jsfiddle.net/z4q7a2ko/
229 insertText
= pre
+ selText
+ post
;
230 if ( options
.splitlines
) {
231 insertText
= doSplitLines( selText
, pre
, post
);
233 if ( options
.ownline
) {
234 if ( startPos
!== 0 && allText
.charAt( startPos
- 1 ) !== '\n' && allText
.charAt( startPos
- 1 ) !== '\r' ) {
235 insertText
= '\n' + insertText
;
238 if ( allText
.charAt( endPos
) !== '\n' && allText
.charAt( endPos
) !== '\r' ) {
243 if ( combiningCharSelectionBug
) {
244 $( this ).textSelection( 'setContents', allText
.slice( 0, startPos
) + insertText
+
245 allText
.slice( endPos
) );
247 $( this ).textSelection( 'replaceSelection', insertText
);
249 if ( isSample
&& options
.selectPeri
&& ( !options
.splitlines
|| ( options
.splitlines
&& selText
.indexOf( '\n' ) === -1 ) ) ) {
250 $( this ).textSelection( 'setSelection', {
251 start
: startPos
+ pre
.length
,
252 end
: startPos
+ pre
.length
+ selText
.length
255 $( this ).textSelection( 'setSelection', {
256 start
: startPos
+ insertText
.length
259 $( this ).trigger( 'encapsulateSelection', [ options
.pre
, options
.peri
, options
.post
, options
.ownline
,
260 options
.replace
, options
.splitlines
] );
265 * Get the current cursor position (in UTF-16 code units) in a textarea.
268 * @param {Object} [options]
269 * @param {Object} [options.startAndEnd=false] Return range of the selection rather than just start
271 * - When `startAndEnd` is `false`: number
272 * - When `startAndEnd` is `true`: array with two numbers, for start and end of selection
274 getCaretPosition: function ( options
) {
275 function getCaret( e
) {
279 caretPos
= e
.selectionStart
;
280 endPos
= e
.selectionEnd
;
282 return options
.startAndEnd
? [ caretPos
, endPos
] : caretPos
;
284 return getCaret( this.get( 0 ) );
288 * Set the current cursor position (in UTF-16 code units) in a textarea.
291 * @param {Object} [options]
292 * @param {number} options.start
293 * @param {number} [options.end=options.start]
297 setSelection: function ( options
) {
298 return this.each( function () {
299 // Opera 9.0 doesn't allow setting selectionStart past
300 // selectionEnd; any attempts to do that will be ignored
301 // Make sure to set them in the right order
302 if ( options
.start
> this.selectionEnd
) {
303 this.selectionEnd
= options
.end
;
304 this.selectionStart
= options
.start
;
306 this.selectionStart
= options
.start
;
307 this.selectionEnd
= options
.end
;
313 * Scroll a textarea to the current cursor position. You can set the cursor
314 * position with #setSelection.
317 * @param {Object} [options]
318 * @param {string} [options.force=false] Whether to force a scroll even if the caret position
319 * is already visible.
323 scrollToCaretPosition: function ( options
) {
324 return this.each( function () {
326 clientHeight
= this.clientHeight
,
327 origValue
= this.value
,
328 origSelectionStart
= this.selectionStart
,
329 origSelectionEnd
= this.selectionEnd
,
330 origScrollTop
= this.scrollTop
,
333 // Delete all text after the selection and scroll the textarea to the end.
334 // This ensures the selection is visible (aligned to the bottom of the textarea).
335 // Then restore the text we deleted without changing scroll position.
336 this.value
= this.value
.slice( 0, this.selectionEnd
);
337 this.scrollTop
= this.scrollHeight
;
338 // Chrome likes to adjust scroll position when changing value, so save and re-set later.
339 // Note that this is not equal to scrollHeight, it's scrollHeight minus clientHeight.
340 calcScrollTop
= this.scrollTop
;
341 this.value
= origValue
;
342 this.selectionStart
= origSelectionStart
;
343 this.selectionEnd
= origSelectionEnd
;
345 if ( !options
.force
) {
346 // Check if all the scrolling was unnecessary and if so, restore previous position.
347 // If the current position is no more than a screenful above the original,
348 // the selection was previously visible on the screen.
349 if ( calcScrollTop
< origScrollTop
&& origScrollTop
- calcScrollTop
< clientHeight
) {
350 calcScrollTop
= origScrollTop
;
354 this.scrollTop
= calcScrollTop
;
356 $( this ).trigger( 'scrollToPosition' );
364 * Register an alternative textSelection API for this element.
367 * @param {Object} functions Functions to replace. Keys are command names (as in #textSelection,
368 * except 'register' and 'unregister'). Values are functions to execute when a given command is
375 * Unregister the alternative textSelection API for this element (see #register).
380 alternateFn
= $( this ).data( 'jquery.textSelection' );
384 // case 'getContents': // no params
385 // case 'setContents': // no params with defaults
386 // case 'getSelection': // no params
387 // case 'replaceSelection': // no params with defaults
388 case 'encapsulateSelection':
389 options
= $.extend( {
397 selectionStart
: undefined,
398 selectionEnd
: undefined
401 case 'getCaretPosition':
402 options
= $.extend( {
407 options
= $.extend( {
411 if ( options
.end
=== undefined ) {
412 options
.end
= options
.start
;
415 case 'scrollToCaretPosition':
416 options
= $.extend( {
422 throw new Error( 'Another textSelection API was already registered' );
424 $( this ).data( 'jquery.textSelection', options
);
425 // No need to update alternateFn as this command only stores the options.
426 // A command that uses it will set it again.
429 $( this ).removeData( 'jquery.textSelection' );
433 retval
= ( alternateFn
&& alternateFn
[ command
] || fn
[ command
] ).call( this, options
);
442 * @method textSelection
443 * @inheritdoc jQuery.plugin.textSelection#textSelection