define CARET_PREF where it is used
[conkeror.git] / modules / content-buffer-input.js
blobc285b8a14645ce3c14ecb21914a7979397402046
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2008-2009 John J. Foerch
4  *
5  * Use, modification, and distribution are subject to the terms specified in the
6  * COPYING file.
7 **/
9 /**
10  * This file contains the default input-mode system.  An input-mode
11  * system is a set of input-modes along with code that coordinates
12  * switching among them based on user actions and focus-context.
13  */
15 require("content-buffer.js");
18  * DEFAULT INPUT-MODE SYSTEM
19  */
21 // Input mode for "normal" view mode.  You might also call it "scroll" mode.
22 define_input_mode("normal", null, "content_buffer_normal_keymap");
24 // Input modes for form elements
25 define_input_mode("select", "input:SELECT", "content_buffer_select_keymap");
26 define_input_mode("text", "input:TEXT", "content_buffer_text_keymap");
27 define_input_mode("textarea", "input:TEXTAREA", "content_buffer_textarea_keymap");
28 define_input_mode("richedit", "input:RICHEDIT", "content_buffer_richedit_keymap");
29 define_input_mode("checkbox", "input:CHECKBOX/RADIOBUTTON", "content_buffer_checkbox_keymap");
32 /**
33  * content_buffer_update_input_mode_for_focus is the controller for
34  * this input-mode system.  It sets the input-mode based on the focused
35  * element or lack thereof in the given buffer.  By default, it will
36  * not override input-modes such as caret-mode, which have been turned
37  * on explicitly by the user.  If force is true, then this function
38  * will even override those special input-modes.
39  */
40 function content_buffer_update_input_mode_for_focus (buffer, force) {
41     var mode = buffer.input_mode;
42     var form_input_mode_enabled = (mode == "text_input_mode" ||
43                                    mode == "textarea_input_mode" ||
44                                    mode == "select_input_mode" ||
45                                    mode == "checkbox_input_mode" ||
46                                    mode == "richedit_input_mode");
48     if (force || form_input_mode_enabled ||
49         mode == "normal_input_mode" || mode == null)
50     {
51         let elem = buffer.focused_element;
53         if (elem) {
54             var input_mode_function = null;
55             if (elem instanceof Ci.nsIDOMHTMLInputElement) {
56                 var type = elem.getAttribute("type");
57                 if (type != null) type = type.toLowerCase();
58                 if (type == "checkbox" || type == "radio")
59                     input_mode_function = checkbox_input_mode;
60                 else if (type != "submit" &&
61                          type != "reset")
62                     input_mode_function = text_input_mode;
63             }
64             else if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
65                 input_mode_function = textarea_input_mode;
67             else if (elem instanceof Ci.nsIDOMHTMLSelectElement)
68                 input_mode_function = select_input_mode;
69         }
71         if (!input_mode_function) {
72             let frame = buffer.focused_frame;
73             let in_design_mode = false;
74             if (frame && frame.document.designMode == "on")
75                 in_design_mode = true;
76             else {
77                 outer:
78                 while (elem) {
79                     switch (elem.contentEditable) {
80                     case "true":
81                         in_design_mode = true;
82                         break outer;
83                     case "false":
84                         break outer;
85                     default: // == "inherit"
86                         elem = elem.parentNode;
87                     }
88                 }
89             }
90             if (in_design_mode)
91                 input_mode_function = richedit_input_mode;
92         }
94         if (input_mode_function) {
95             if (!force &&
96                 browser_prevent_automatic_form_focus_mode_enabled && //XXX: breaks abstraction
97                 !form_input_mode_enabled &&
98                 (buffer.last_user_input_received == null ||
99                  (Date.now() - buffer.last_user_input_received)
100                  > browser_automatic_form_focus_window_duration)) {
101                 // Automatic focus attempt blocked
102                 elem.blur();
103             } else
104                 input_mode_function(buffer, true);
105             return;
106         }
107         normal_input_mode(buffer, true);
108     }
111 //XXX: why does this command exist?  if the input-mode system works correctly
112 //     then the user should never have to call this.
113 interactive("content-buffer-update-input-mode-for-focus",
114     "This command provides a way for the user to force a reset of the"+
115     "input-mode system.",
116     function (I) {
117         content_buffer_update_input_mode_for_focus(I.buffer, true);
118     });
122  * Install the default input-mode system
123  */
124 add_hook("content_buffer_location_change_hook",
125          function (buf) { // ignore other args
126              content_buffer_update_input_mode_for_focus(buf);
127          });
129 add_hook("content_buffer_focus_change_hook",
130          function (buf) { // ignore other args
131              content_buffer_update_input_mode_for_focus(buf);
132          });
136  * QUOTE INPUT MODES
137  */
139 // Input modes for sending key events to gecko
141 // XXX: These quote modes are not rightly part of the system because
142 // they are always invoked by the user.  Furthermore, they should be
143 // generalized to work with any type of buffer.
144 define_input_mode(
145     "quote_next", "input:PASS-THROUGH(next)", "content_buffer_quote_next_keymap",
146     "This input mode sends the next key combo to the buffer, "+
147         "bypassing Conkeror's normal key handling.  The mode disengages "+
148         "after one key combo.");
149 define_input_mode(
150     "quote", "input:PASS-THROUGH", "content_buffer_quote_keymap",
151     "This input mode sends all key combos to the buffer, "+
152         "bypassing Conkeror's normal key handling, until the "+
153         "Escape key is pressed.");
155 define_key_match_predicate('match_not_escape_key', 'any key but escape',
156     function (event) {
157         return event.keyCode != 27 ||
158             event.shiftKey ||
159             event.altKey ||
160             event.metaKey ||
161             event.ctrlKey;
162     });
166  * CARET INPUT MODE
167  */
169 // Input mode for the visible caret
171 // XXX: caret-mode should be defined elsewhere, as it is not part of
172 // the input-mode system.
173 define_input_mode("caret", null, "content_buffer_caret_keymap");
175 define_buffer_mode('caret_mode', 'CARET',
176                    $enable = function(buffer) {
177                        buffer.browser.setAttribute('showcaret', 'true');
178                        let sc = getFocusedSelCtrl(buffer);
179                        sc.setCaretEnabled(true);
180                        buffer.top_frame.focus();
181                        caret_input_mode(buffer, true);
182                    },
183                    $disable = function(buffer) {
184                        buffer.browser.setAttribute('showcaret', '');
185                        let sc = getFocusedSelCtrl(buffer);
186                        sc.setCaretEnabled(false);
187                        buffer.browser.focus();
188                        //XXX: dependency on the default input-mode system
189                        content_buffer_update_input_mode_for_focus(buffer, true);
190                    });
193 const CARET_PREF = 'accessibility.browsewithcaret';
195 watch_pref(CARET_PREF, function() {
196                if (get_pref(CARET_PREF)) {
197                    session_pref(CARET_PREF, false);
198                    let window = window_watcher.activeWindow;
199                    let buffer = window.buffers.current;
200                    caret_mode(buffer);
201                }
202            });
207  * Browser Prevent Automatic Form Focus Mode
208  */
210 // Milliseconds
211 define_variable("browser_automatic_form_focus_window_duration", 20,
212                 "Time window (in milliseconds) during which a form element "+
213                 "is allowed to gain focus following a mouse click or key "+
214                 "press, if `browser_prevent_automatic_form_focus_mode' is "+
215                 "enabled.");;
217 define_global_mode("browser_prevent_automatic_form_focus_mode",
218                    function () {}, // enable
219                    function () {} // disable
220                   );
222 // note: The apparent misspellings here are not a bug.
223 // see https://developer.mozilla.org/en/XPath/Functions/translate
225 define_variable(
226     "browser_form_field_xpath_expression",
227     "//input[" + (
228         //        "translate(@type,'RADIO','radio')!='radio' and " +
229         //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
230         "translate(@type,'HIDEN','hiden')!='hidden'"
231         //        "translate(@type,'SUBMIT','submit')!='submit' and " +
232         //        "translate(@type,'REST','rest')!='reset'"
233     ) +  "] | " +
234         "//xhtml:input[" + (
235             //        "translate(@type,'RADIO','radio')!='radio' and " +
236             //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
237             "translate(@type,'HIDEN','hiden')!='hidden'"
238             //        "translate(@type,'SUBMIT','submit')!='submit' and " +
239             //        "translate(@type,'REST','rest')!='reset'"
240         ) +  "] |" +
241         "//select | //xhtml:select | " +
242         "//textarea | //xhtml:textarea | " +
243         "//textbox | //xul:textbox",
244     "XPath expression matching elements to be selected by `browser-focus-next-form-field' " +
245         "and `browser-focus-previous-form-field.'");
247 function focus_next (buffer, count, xpath_expr, name) {
248     var focused_elem = buffer.focused_element;
249     if (count == 0)
250         return; // invalid count
252     function helper (win, skip_win) {
253         if (win == skip_win)
254             return null;
255         var doc = win.document;
256         var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
257             Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
258             null /* existing results */);
259         var length = res.snapshotLength;
260         if (length > 0) {
261             let valid_nodes = [];
262             for (let i = 0; i < length; ++i) {
263                 let elem = res.snapshotItem(i);
264                 if (elem.offsetWidth == 0 ||
265                     elem.offsetHeight == 0)
266                     continue;
267                 let style = win.getComputedStyle(elem, "");
268                 if (style.display == "none" || style.visibility == "hidden")
269                     continue;
270                 valid_nodes.push(elem);
271             }
273             if (valid_nodes.length > 0) {
274                 var index = -1;
275                 if (focused_elem != null)
276                     index = valid_nodes.indexOf(focused_elem);
277                 if (index == -1) {
278                     if (count > 0)
279                         index = count - 1;
280                     else
281                         index = -count;
282                 }
283                 else
284                     index = index + count;
285                 index = index % valid_nodes.length;
286                 if (index < 0)
287                     index += valid_nodes.length;
289                 return valid_nodes[index];
290             }
291         }
292         // Recurse on sub-frames
293         for (var i = 0; i < win.frames.length; ++i) {
294             var elem = helper(win.frames[i], skip_win);
295             if (elem)
296                 return elem;
297         }
298         return null;
299     }
301     var focused_win = buffer.focused_frame;
302     var elem = helper(focused_win, null);
303     if (!elem)
304         // if focused_frame is top_frame, we're doing twice as much
305         // work as necessary
306         elem = helper(buffer.top_frame, focused_win);
307     if (elem)
308         browser_element_focus(buffer, elem);
309     else
310         throw interactive_error("No "+name+" found");
313 interactive("browser-focus-next-form-field",
314             "Focus the next element matching "+
315             "`browser_form_field_xpath_expression'.",
316             function (I) {
317                 focus_next(I.buffer, I.p,
318                            browser_form_field_xpath_expression,
319                            "form field");
320             });
322 interactive("browser-focus-previous-form-field",
323             "Focus the previous element matching "+
324             "`browser_form_field_xpath_expression'.",
325             function (I) {
326                 focus_next(I.buffer, -I.p,
327                            browser_form_field_xpath_expression,
328                            "form field");
329             });
332 define_variable("links_xpath_expression",
333     "//*[@onclick or @onmouseover or @onmousedown or "+
334         "@onmouseup or @oncommand or @role='link'] | " +
335     "//input[not(@type='hidden')] | //a | //area | "+
336     "//iframe | //textarea | //button | //select",
337     "XPath expression matching elements to be selected by "+
338     "`focus-next-link' and `focus-previous-link.'");
340 interactive("focus-next-link",
341             "Focus the next element matching `links_xpath_expression'.",
342             function (I) {
343                 focus_next(I.buffer, I.p,
344                            links_xpath_expression,
345                            "link");
346             });
348 interactive("focus-previous-link",
349             "Focus the previous element matching `links_xpath_expression'.",
350             function (I) {
351                 focus_next(I.buffer, -I.p,
352                            links_xpath_expression,
353                            "link");
354             });
357 define_variable('edit_field_in_external_editor_extension', "txt",
358     "File extension for the temp files created by "+
359     "edit-current-field-in-external-editor.");
361 function get_filename_for_current_textfield (doc, elem) {
362     var name = doc.URL
363         + "-"
364         + ( elem.getAttribute("name")
365             || elem.getAttribute("id")
366             || "textarea" );
368     // get rid filesystem unfriendly chars
369     name = name.replace(doc.location.protocol, "")
370         .replace(/[^a-zA-Z0-9]+/g, "-")
371         .replace(/(^-+|-+$)/g, "")
372         + '.' + edit_field_in_external_editor_extension;
374     return name;
377 function edit_field_in_external_editor (buffer, elem) {
378     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
379         var type = elem.getAttribute("type");
380         if (type != null)
381             type = type.toLowerCase();
382         if (type == "hidden" || type == "checkbox" || type == "radio")
383             throw interactive_error("Element is not a text field.");
384     } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
385         throw interactive_error("Element is not a text field.");
387     var name = get_filename_for_current_textfield(buffer.document, elem);
388     var file = get_temporary_file(name);
390     // Write to file
391     try {
392         write_text_file(file, elem.value);
393     } catch (e) {
394         file.remove(false);
395         throw e;
396     }
398     // FIXME: decide if we should do this
399     var old_class = elem.className;
400     elem.className = "__conkeror_textbox_edited_externally " + old_class;
402     try {
403         yield open_file_with_external_editor(file);
405         elem.value = read_text_file(file);
406     } finally {
407         elem.className = old_class;
409         file.remove(false);
410     }
413 interactive("edit-current-field-in-external-editor",
414             "Edit the contents of the currently-focused text field in an external editor.",
415             function (I) {
416                 var buf = I.buffer;
417                 var elem = buf.focused_element;
418                 yield edit_field_in_external_editor(buf, elem);
419                 elem.blur();
420             });
422 define_variable("kill_whole_line", false,
423                 "If true, `kill-line' with no arg at beg of line kills the whole line.");
425 function cut_to_end_of_line (buffer) {
426     var elem = buffer.focused_element;
427     try {
428         var st = elem.selectionStart;
429         var en = elem.selectionEnd;
430         if (st == en) {
431             // there is no selection.  set one up.
432             var eol = elem.value.indexOf("\n", en);
433             if (eol == -1)
434                 elem.selectionEnd = elem.textLength;
435             else if (eol == st)
436                 elem.selectionEnd = eol + 1;
437             else if (kill_whole_line &&
438                      (st == 0 || elem.value[st - 1] == "\n"))
439                 elem.selectionEnd = eol + 1;
440             else
441                 elem.selectionEnd = eol;
442         }
443         buffer.do_command('cmd_cut');
444     } catch (e) {
445         /* FIXME: Make this work for richedit mode as well */
446     }
448 interactive("cut-to-end-of-line",
449     null,
450     function (I) {
451         cut_to_end_of_line(I.buffer);
452     });
455 function downcase_word (I) {
456     modify_word_at_point(I, function (word) { return word.toLocaleLowerCase(); });
458 interactive("downcase-word",
459             "Convert following word to lower case, moving over.",
460             downcase_word);
463 function upcase_word (I) {
464     modify_word_at_point(I, function (word) { return word.toLocaleUpperCase(); });
466 interactive("upcase-word",
467             "Convert following word to upper case, moving over.",
468             upcase_word);
471 function capitalize_word (I) {
472     modify_word_at_point(I, function (word) {
473         if (word.length > 0) {
474             return word[0].toLocaleUpperCase() + word.substring(1);
475         }
476         return word;
477     });
479 interactive("capitalize-word",
480             "Capitalize the following word (or arg words), moving over.",
481             capitalize_word);