xref: /dokuwiki/lib/scripts/textselection.js (revision 7800517deb6ab16f8211f73adc961a5bcb1ea328)
1/**
2 * Text selection related functions.
3 */
4
5/**
6 * selection prototype
7 *
8 * Object that capsulates the selection in a textarea. Returned by getSelection.
9 *
10 * @author Andreas Gohr <andi@splitbrain.org>
11 */
12function selection_class(){
13    this.start     = 0;
14    this.end       = 0;
15    this.obj       = null;
16    this.rangeCopy = null;
17    this.scroll    = 0;
18    this.fix       = 0;
19
20    this.getLength = function(){
21        return this.end - this.start;
22    };
23
24    this.getText = function(){
25        if(!this.obj) return '';
26        return this.obj.value.substring(this.start,this.end);
27    };
28}
29
30/**
31 * Get current selection/cursor position in a given textArea
32 *
33 * @link   http://groups.drupal.org/node/1210
34 * @author Andreas Gohr <andi@splitbrain.org>
35 * @link   http://linebyline.blogspot.com/2006/11/textarea-cursor-position-in-internet.html
36 * @returns object - a selection object
37 */
38function getSelection(textArea) {
39    var sel = new selection_class();
40
41    sel.obj   = textArea;
42    sel.start = textArea.value.length;
43    sel.end   = textArea.value.length;
44    textArea.focus();
45    if(document.getSelection) {          // Mozilla et al.
46        sel.start  = textArea.selectionStart;
47        sel.end    = textArea.selectionEnd;
48        sel.scroll = textArea.scrollTop;
49    } else if(document.selection) {      // MSIE
50        /*
51         * This huge lump of code is neccessary to work around two MSIE bugs:
52         *
53         * 1. Selections trim newlines at the end of the code
54         * 2. Selections count newlines as two characters
55         */
56
57        // The current selection
58        sel.rangeCopy = document.selection.createRange().duplicate();
59        if (textArea.tagName === 'INPUT')  {
60            var before_range = textArea.createTextRange();
61            before_range.expand('textedit');                       // Selects all the text
62        } else {
63            var before_range = document.body.createTextRange();
64            before_range.moveToElementText(textArea);              // Selects all the text
65        }
66        before_range.setEndPoint("EndToStart", sel.rangeCopy);     // Moves the end where we need it
67
68        var before_finished = false, selection_finished = false;
69        var before_text, selection_text;
70        // Load the text values we need to compare
71        before_text  = before_range.text;
72        selection_text = sel.rangeCopy.text;
73
74        sel.start = before_text.length;
75        sel.end   = sel.start + selection_text.length;
76
77        // Check each range for trimmed newlines by shrinking the range by 1 character and seeing
78        // if the text property has changed.  If it has not changed then we know that IE has trimmed
79        // a \r\n from the end.
80        do {
81            if (!before_finished) {
82                if (before_range.compareEndPoints("StartToEnd", before_range) == 0) {
83                    before_finished = true;
84                } else {
85                    before_range.moveEnd("character", -1);
86                    if (before_range.text == before_text) {
87                        sel.start += 2;
88                        sel.end += 2;
89                    } else {
90                        before_finished = true;
91                    }
92                }
93            }
94            if (!selection_finished) {
95                if (sel.rangeCopy.compareEndPoints("StartToEnd", sel.rangeCopy) == 0) {
96                    selection_finished = true;
97                } else {
98                    sel.rangeCopy.moveEnd("character", -1);
99                    if (sel.rangeCopy.text == selection_text) {
100                        sel.end += 2;
101                    } else {
102                        selection_finished = true;
103                    }
104                }
105            }
106        } while ((!before_finished || !selection_finished));
107
108
109        // count number of newlines in str to work around stupid IE selection bug
110        var countNL = function(str) {
111            var m = str.split("\r\n");
112            if (!m || !m.length) return 0;
113            return m.length-1;
114        };
115        sel.fix = countNL(sel.obj.value.substring(0,sel.start));
116
117    }
118    return sel;
119}
120
121/**
122 * Set the selection
123 *
124 * You need to get a selection object via getSelection() first, then modify the
125 * start and end properties and pass it back to this function.
126 *
127 * @link http://groups.drupal.org/node/1210
128 * @author Andreas Gohr <andi@splitbrain.org>
129 * @param object selection - a selection object as returned by getSelection()
130 */
131function setSelection(selection){
132    if(document.getSelection){ // FF
133        // what a pleasure in FF ;)
134        selection.obj.setSelectionRange(selection.start,selection.end);
135        if(selection.scroll) selection.obj.scrollTop = selection.scroll;
136    } else if(document.selection) { // IE
137        selection.rangeCopy.collapse(true);
138        selection.rangeCopy.moveStart('character',selection.start - selection.fix);
139        selection.rangeCopy.moveEnd('character',selection.end - selection.start);
140        selection.rangeCopy.select();
141    }
142}
143
144/**
145 * Inserts the given text at the current cursor position or replaces the current
146 * selection
147 *
148 * @author Andreas Gohr <andi@splitbrain.org>
149 * @param string text          - the new text to be pasted
150 * @param objct  selecttion    - selection object returned by getSelection
151 * @param int    opts.startofs - number of charcters at the start to skip from new selection
152 * @param int    opts.endofs   - number of characters at the end to skip from new selection
153 * @param bool   opts.nosel    - set true if new text should not be selected
154 */
155function pasteText(selection,text,opts){
156    if(!opts) opts = {};
157    // replace the content
158
159    selection.obj.value =
160        selection.obj.value.substring(0, selection.start) + text +
161        selection.obj.value.substring(selection.end, selection.obj.value.length);
162
163    // set new selection
164    if (is_opera) {
165        // Opera replaces \n by \r\n when inserting text.
166        selection.end = selection.start + text.replace(/\r?\n/g, '\r\n').length;
167    } else {
168        selection.end = selection.start + text.length;
169    }
170
171    // modify the new selection if wanted
172    if(opts.startofs) selection.start += opts.startofs;
173    if(opts.endofs)   selection.end   -= opts.endofs;
174
175    // no selection wanted? set cursor to end position
176    if(opts.nosel) selection.start = selection.end;
177
178    setSelection(selection);
179}
180
181
182/**
183 * Format selection
184 *
185 * Apply tagOpen/tagClose to selection in textarea, use sampleText instead
186 * of selection if there is none.
187 *
188 * @author Andreas Gohr <andi@splitbrain.org>
189 */
190function insertTags(textAreaID, tagOpen, tagClose, sampleText){
191    var txtarea = $(textAreaID);
192
193    var selection = getSelection(txtarea);
194    var text = selection.getText();
195    var opts;
196
197    // don't include trailing space in selection
198    if(text.charAt(text.length - 1) == ' '){
199        selection.end--;
200        text = selection.getText();
201    }
202
203    if(!text){
204        // nothing selected, use the sample text and select it
205        text = sampleText;
206        opts = {
207            startofs: tagOpen.length,
208            endofs: tagClose.length
209        };
210    }else{
211        // place cursor at the end
212        opts = {
213            nosel: true
214        };
215    }
216
217    // surround with tags
218    text = tagOpen + text + tagClose;
219
220    // do it
221    pasteText(selection,text,opts);
222}
223
224/**
225 * Wraps around pasteText() for backward compatibility
226 *
227 * @author Andreas Gohr <andi@splitbrain.org>
228 */
229function insertAtCarret(textAreaID, text){
230    var txtarea = $(textAreaID);
231    var selection = getSelection(txtarea);
232    pasteText(selection,text,{nosel: true});
233}
234
235