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#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
46 $.fn
.textSelection = function ( command
, options
) {
53 * Get the contents of the textarea.
58 getContents: function () {
63 * Set the contents of the textarea, replacing anything that was there before.
66 * @param {string} content
68 setContents: function ( content
) {
73 * Get the currently selected text in this textarea.
78 getSelection: function () {
85 retval
= el
.value
.substring( el
.selectionStart
, el
.selectionEnd
);
92 * Insert text at the beginning and end of a text selection, optionally
93 * inserting text at the caret when selection is empty.
95 * Also focusses the textarea.
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
114 encapsulateSelection: function ( options
) {
115 return this.each( function () {
116 var selText
, allText
, currSelection
, scrollTop
, insertText
,
117 isSample
, startPos
, endPos
,
123 * Check if the selected text is the same as the insert text
125 function checkSelectedText() {
127 selText
= options
.peri
;
129 } else if ( options
.replace
) {
130 selText
= options
.peri
;
132 while ( selText
.charAt( selText
.length
- 1 ) === ' ' ) {
133 // Exclude ending space char
134 selText
= selText
.slice( 0, -1 );
137 while ( selText
.charAt( 0 ) === ' ' ) {
138 // Exclude prepending space char
139 selText
= selText
.slice( 1 );
147 * Do the splitlines stuff.
149 * Wrap each line of the selected text with pre and post
151 * @param {string} selText Selected text
152 * @param {string} pre Text before
153 * @param {string} post Text after
154 * @return {string} Wrapped text
156 function doSplitLines( selText
, pre
, post
) {
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 ) {
171 if ( options
.selectionStart
!== undefined ) {
172 $( this ).textSelection( 'setSelection', { start
: options
.selectionStart
, end
: options
.selectionEnd
} );
175 selText
= $( this ).textSelection( 'getSelection' );
176 allText
= $( this ).textSelection( 'getContents' );
177 currSelection
= $( this ).textSelection( 'getCaretPosition', { startAndEnd
: true } );
178 startPos
= currSelection
[ 0 ];
179 endPos
= currSelection
[ 1 ];
180 scrollTop
= this.scrollTop
;
183 options
.selectionStart
!== undefined &&
184 endPos
- startPos
!== options
.selectionEnd
- options
.selectionStart
186 // This means there is a difference in the selection range returned by browser and what we passed.
187 // This happens for Chrome in the case of composite characters. Ref T32130
188 // Set the startPos to the correct position.
189 startPos
= options
.selectionStart
;
192 insertText
= pre
+ selText
+ post
;
193 if ( options
.splitlines
) {
194 insertText
= doSplitLines( selText
, pre
, post
);
196 if ( options
.ownline
) {
197 if ( startPos
!== 0 && allText
.charAt( startPos
- 1 ) !== '\n' && allText
.charAt( startPos
- 1 ) !== '\r' ) {
198 insertText
= '\n' + insertText
;
201 if ( allText
.charAt( endPos
) !== '\n' && allText
.charAt( endPos
) !== '\r' ) {
206 $( this ).textSelection( 'setContents', allText
.slice( 0, startPos
) + insertText
+
207 allText
.slice( endPos
) );
208 // Setting this.value (via setContents) may scroll the textarea, restore the scroll position
209 this.scrollTop
= scrollTop
;
210 if ( isSample
&& options
.selectPeri
&& ( !options
.splitlines
|| ( options
.splitlines
&& selText
.indexOf( '\n' ) === -1 ) ) ) {
211 $( this ).textSelection( 'setSelection', {
212 start
: startPos
+ pre
.length
,
213 end
: startPos
+ pre
.length
+ selText
.length
216 $( this ).textSelection( 'setSelection', {
217 start
: startPos
+ insertText
.length
220 $( this ).trigger( 'encapsulateSelection', [ options
.pre
, options
.peri
, options
.post
, options
.ownline
,
221 options
.replace
, options
.splitlines
] );
226 * Get the current cursor position (in UTF-16 code units) in a textarea.
229 * @param {Object} [options]
230 * @param {Object} [options.startAndEnd=false] Return range of the selection rather than just start
232 * - When `startAndEnd` is `false`: number
233 * - When `startAndEnd` is `true`: array with two numbers, for start and end of selection
235 getCaretPosition: function ( options
) {
236 function getCaret( e
) {
240 caretPos
= e
.selectionStart
;
241 endPos
= e
.selectionEnd
;
243 return options
.startAndEnd
? [ caretPos
, endPos
] : caretPos
;
245 return getCaret( this.get( 0 ) );
249 * Set the current cursor position (in UTF-16 code units) in a textarea.
252 * @param {Object} [options]
253 * @param {number} options.start
254 * @param {number} [options.end=options.start]
258 setSelection: function ( options
) {
259 return this.each( function () {
260 // Opera 9.0 doesn't allow setting selectionStart past
261 // selectionEnd; any attempts to do that will be ignored
262 // Make sure to set them in the right order
263 if ( options
.start
> this.selectionEnd
) {
264 this.selectionEnd
= options
.end
;
265 this.selectionStart
= options
.start
;
267 this.selectionStart
= options
.start
;
268 this.selectionEnd
= options
.end
;
274 * Scroll a textarea to the current cursor position. You can set the cursor
275 * position with #setSelection.
278 * @param {Object} [options]
279 * @param {string} [options.force=false] Whether to force a scroll even if the caret position
280 * is already visible.
284 scrollToCaretPosition: function ( options
) {
285 return this.each( function () {
287 clientHeight
= this.clientHeight
,
288 origValue
= this.value
,
289 origSelectionStart
= this.selectionStart
,
290 origSelectionEnd
= this.selectionEnd
,
291 origScrollTop
= this.scrollTop
,
294 // Delete all text after the selection and scroll the textarea to the end.
295 // This ensures the selection is visible (aligned to the bottom of the textarea).
296 // Then restore the text we deleted without changing scroll position.
297 this.value
= this.value
.slice( 0, this.selectionEnd
);
298 this.scrollTop
= this.scrollHeight
;
299 // Chrome likes to adjust scroll position when changing value, so save and re-set later.
300 // Note that this is not equal to scrollHeight, it's scrollHeight minus clientHeight.
301 calcScrollTop
= this.scrollTop
;
302 this.value
= origValue
;
303 this.selectionStart
= origSelectionStart
;
304 this.selectionEnd
= origSelectionEnd
;
306 if ( !options
.force
) {
307 // Check if all the scrolling was unnecessary and if so, restore previous position.
308 // If the current position is no more than a screenful above the original,
309 // the selection was previously visible on the screen.
310 if ( calcScrollTop
< origScrollTop
&& origScrollTop
- calcScrollTop
< clientHeight
) {
311 calcScrollTop
= origScrollTop
;
315 this.scrollTop
= calcScrollTop
;
317 $( this ).trigger( 'scrollToPosition' );
325 * Register an alternative textSelection API for this element.
328 * @param {Object} functions Functions to replace. Keys are command names (as in #textSelection,
329 * except 'register' and 'unregister'). Values are functions to execute when a given command is
336 * Unregister the alternative textSelection API for this element (see #register).
341 alternateFn
= $( this ).data( 'jquery.textSelection' );
345 // case 'getContents': // no params
346 // case 'setContents': // no params with defaults
347 // case 'getSelection': // no params
348 case 'encapsulateSelection':
349 options
= $.extend( {
357 selectionStart
: undefined,
358 selectionEnd
: undefined
361 case 'getCaretPosition':
362 options
= $.extend( {
367 options
= $.extend( {
371 if ( options
.end
=== undefined ) {
372 options
.end
= options
.start
;
375 case 'scrollToCaretPosition':
376 options
= $.extend( {
382 throw new Error( 'Another textSelection API was already registered' );
384 $( this ).data( 'jquery.textSelection', options
);
385 // No need to update alternateFn as this command only stores the options.
386 // A command that uses it will set it again.
389 $( this ).removeData( 'jquery.textSelection' );
393 retval
= ( alternateFn
&& alternateFn
[ command
] || fn
[ command
] ).call( this, options
);
402 * @method textSelection
403 * @inheritdoc jQuery.plugin.textSelection#textSelection