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