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
;
75 if ( !document
.execCommand( 'insertText', false, content
) ) {
76 $( this ).val( content
);
78 // Setting this.value may scroll the textarea, restore the scroll position
79 this.scrollTop
= scrollTop
;
84 * Get the currently selected text in this textarea.
89 getSelection: function () {
96 retval
= el
.value
.substring( el
.selectionStart
, el
.selectionEnd
);
103 * Replace the selected text in the textarea with the given text, or insert it at the cursor.
106 * @param {string} value
110 replaceSelection: function ( value
) {
111 return this.each( function () {
112 var allText
, currSelection
, startPos
, endPos
;
114 if ( !document
.execCommand( 'insertText', false, value
) ) {
115 allText
= $( this ).textSelection( 'getContents' );
116 currSelection
= $( this ).textSelection( 'getCaretPosition', { startAndEnd
: true } );
117 startPos
= currSelection
[ 0 ];
118 endPos
= currSelection
[ 1 ];
120 $( this ).textSelection( 'setContents', allText
.slice( 0, startPos
) + value
+
121 allText
.slice( endPos
) );
122 $( this ).textSelection( 'setSelection', {
124 end
: startPos
+ value
.length
131 * Insert text at the beginning and end of a text selection, optionally
132 * inserting text at the caret when selection is empty.
134 * Also focusses the textarea.
137 * @param {Object} [options]
138 * @param {string} [options.pre] Text to insert before the cursor/selection
139 * @param {string} [options.peri] Text to insert between pre and post and select afterwards
140 * @param {string} [options.post] Text to insert after the cursor/selection
141 * @param {boolean} [options.ownline=false] Put the inserted text on a line of its own
142 * @param {boolean} [options.replace=false] If there is a selection, replace it with peri
143 * instead of leaving it alone
144 * @param {boolean} [options.selectPeri=true] Select the peri text if it was inserted (but not
145 * if there was a selection and replace==false, or if splitlines==true)
146 * @param {boolean} [options.splitlines=false] If multiple lines are selected, encapsulate
147 * each line individually
148 * @param {number} [options.selectionStart] Position to start selection at
149 * @param {number} [options.selectionEnd=options.selectionStart] Position to end selection at
153 encapsulateSelection: function ( options
) {
154 return this.each( function () {
155 var selText
, allText
, currSelection
, insertText
,
156 combiningCharSelectionBug
= false,
157 isSample
, startPos
, endPos
,
163 * Check if the selected text is the same as the insert text
165 function checkSelectedText() {
167 selText
= options
.peri
;
169 } else if ( options
.replace
) {
170 selText
= options
.peri
;
172 while ( selText
.charAt( selText
.length
- 1 ) === ' ' ) {
173 // Exclude ending space char
174 selText
= selText
.slice( 0, -1 );
177 while ( selText
.charAt( 0 ) === ' ' ) {
178 // Exclude prepending space char
179 selText
= selText
.slice( 1 );
187 * Do the splitlines stuff.
189 * Wrap each line of the selected text with pre and post
191 * @param {string} selText Selected text
192 * @param {string} pre Text before
193 * @param {string} post Text after
194 * @return {string} Wrapped text
196 function doSplitLines( selText
, pre
, post
) {
199 selTextArr
= selText
.split( '\n' );
200 for ( i
= 0; i
< selTextArr
.length
; i
++ ) {
201 insertText
+= pre
+ selTextArr
[ i
] + post
;
202 if ( i
!== selTextArr
.length
- 1 ) {
211 if ( options
.selectionStart
!== undefined ) {
212 $( this ).textSelection( 'setSelection', { start
: options
.selectionStart
, end
: options
.selectionEnd
} );
215 selText
= $( this ).textSelection( 'getSelection' );
216 allText
= $( this ).textSelection( 'getContents' );
217 currSelection
= $( this ).textSelection( 'getCaretPosition', { startAndEnd
: true } );
218 startPos
= currSelection
[ 0 ];
219 endPos
= currSelection
[ 1 ];
222 options
.selectionStart
!== undefined &&
223 endPos
- startPos
!== options
.selectionEnd
- options
.selectionStart
225 // This means there is a difference in the selection range returned by browser and what we passed.
226 // This happens for Safari 5.1, Chrome 12 in the case of composite characters. Ref T32130
227 // Set the startPos to the correct position.
228 startPos
= options
.selectionStart
;
229 combiningCharSelectionBug
= true;
230 // TODO: The comment above is from 2011. Is this still a problem for browsers we support today?
231 // Minimal test case: https://jsfiddle.net/z4q7a2ko/
234 insertText
= pre
+ selText
+ post
;
235 if ( options
.splitlines
) {
236 insertText
= doSplitLines( selText
, pre
, post
);
238 if ( options
.ownline
) {
239 if ( startPos
!== 0 && allText
.charAt( startPos
- 1 ) !== '\n' && allText
.charAt( startPos
- 1 ) !== '\r' ) {
240 insertText
= '\n' + insertText
;
243 if ( allText
.charAt( endPos
) !== '\n' && allText
.charAt( endPos
) !== '\r' ) {
248 if ( combiningCharSelectionBug
) {
249 $( this ).textSelection( 'setContents', allText
.slice( 0, startPos
) + insertText
+
250 allText
.slice( endPos
) );
252 $( this ).textSelection( 'replaceSelection', insertText
);
254 if ( isSample
&& options
.selectPeri
&& ( !options
.splitlines
|| ( options
.splitlines
&& selText
.indexOf( '\n' ) === -1 ) ) ) {
255 $( this ).textSelection( 'setSelection', {
256 start
: startPos
+ pre
.length
,
257 end
: startPos
+ pre
.length
+ selText
.length
260 $( this ).textSelection( 'setSelection', {
261 start
: startPos
+ insertText
.length
264 $( this ).trigger( 'encapsulateSelection', [ options
.pre
, options
.peri
, options
.post
, options
.ownline
,
265 options
.replace
, options
.splitlines
] );
270 * Get the current cursor position (in UTF-16 code units) in a textarea.
273 * @param {Object} [options]
274 * @param {Object} [options.startAndEnd=false] Return range of the selection rather than just start
276 * - When `startAndEnd` is `false`: number
277 * - When `startAndEnd` is `true`: array with two numbers, for start and end of selection
279 getCaretPosition: function ( options
) {
280 function getCaret( e
) {
284 caretPos
= e
.selectionStart
;
285 endPos
= e
.selectionEnd
;
287 return options
.startAndEnd
? [ caretPos
, endPos
] : caretPos
;
289 return getCaret( this.get( 0 ) );
293 * Set the current cursor position (in UTF-16 code units) in a textarea.
296 * @param {Object} [options]
297 * @param {number} options.start
298 * @param {number} [options.end=options.start]
302 setSelection: function ( options
) {
303 return this.each( function () {
304 // Opera 9.0 doesn't allow setting selectionStart past
305 // selectionEnd; any attempts to do that will be ignored
306 // Make sure to set them in the right order
307 if ( options
.start
> this.selectionEnd
) {
308 this.selectionEnd
= options
.end
;
309 this.selectionStart
= options
.start
;
311 this.selectionStart
= options
.start
;
312 this.selectionEnd
= options
.end
;
318 * Scroll a textarea to the current cursor position. You can set the cursor
319 * position with #setSelection.
322 * @param {Object} [options]
323 * @param {string} [options.force=false] Whether to force a scroll even if the caret position
324 * is already visible.
328 scrollToCaretPosition: function ( options
) {
329 return this.each( function () {
331 clientHeight
= this.clientHeight
,
332 origValue
= this.value
,
333 origSelectionStart
= this.selectionStart
,
334 origSelectionEnd
= this.selectionEnd
,
335 origScrollTop
= this.scrollTop
,
338 // Delete all text after the selection and scroll the textarea to the end.
339 // This ensures the selection is visible (aligned to the bottom of the textarea).
340 // Then restore the text we deleted without changing scroll position.
341 this.value
= this.value
.slice( 0, this.selectionEnd
);
342 this.scrollTop
= this.scrollHeight
;
343 // Chrome likes to adjust scroll position when changing value, so save and re-set later.
344 // Note that this is not equal to scrollHeight, it's scrollHeight minus clientHeight.
345 calcScrollTop
= this.scrollTop
;
346 this.value
= origValue
;
347 this.selectionStart
= origSelectionStart
;
348 this.selectionEnd
= origSelectionEnd
;
350 if ( !options
.force
) {
351 // Check if all the scrolling was unnecessary and if so, restore previous position.
352 // If the current position is no more than a screenful above the original,
353 // the selection was previously visible on the screen.
354 if ( calcScrollTop
< origScrollTop
&& origScrollTop
- calcScrollTop
< clientHeight
) {
355 calcScrollTop
= origScrollTop
;
359 this.scrollTop
= calcScrollTop
;
361 $( this ).trigger( 'scrollToPosition' );
369 * Register an alternative textSelection API for this element.
372 * @param {Object} functions Functions to replace. Keys are command names (as in #textSelection,
373 * except 'register' and 'unregister'). Values are functions to execute when a given command is
380 * Unregister the alternative textSelection API for this element (see #register).
385 alternateFn
= $( this ).data( 'jquery.textSelection' );
389 // case 'getContents': // no params
390 // case 'setContents': // no params with defaults
391 // case 'getSelection': // no params
392 // case 'replaceSelection': // no params with defaults
393 case 'encapsulateSelection':
394 options
= $.extend( {
402 selectionStart
: undefined,
403 selectionEnd
: undefined
406 case 'getCaretPosition':
407 options
= $.extend( {
412 options
= $.extend( {
416 if ( options
.end
=== undefined ) {
417 options
.end
= options
.start
;
420 case 'scrollToCaretPosition':
421 options
= $.extend( {
427 throw new Error( 'Another textSelection API was already registered' );
429 $( this ).data( 'jquery.textSelection', options
);
430 // No need to update alternateFn as this command only stores the options.
431 // A command that uses it will set it again.
434 $( this ).removeData( 'jquery.textSelection' );
438 retval
= ( alternateFn
&& alternateFn
[ command
] || fn
[ command
] ).call( this, options
);
447 * @method textSelection
448 * @inheritdoc jQuery.plugin.textSelection#textSelection