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