new commands focus-previous-link and focus-next-link
[conkeror.git] / modules / content-buffer-input.js
blobe40601b17490f4a59e8670a7e4748ced875df0e4
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2008 John J. Foerch
4  *
5  * Use, modification, and distribution are subject to the terms specified in the
6  * COPYING file.
7 **/
9 require("content-buffer.js");
11 define_current_buffer_hook("current_buffer_input_mode_change_hook", "input_mode_change_hook");
13 var content_buffer_input_mode_keymaps = {};
15 function define_input_mode(base_name, display_name, keymap_name, doc) {
16     var name = base_name + "_input_mode";
17     content_buffer_input_mode_keymaps[name] = keymap_name;
18     define_buffer_mode(name,
19                        display_name,
20                        $class = "input_mode",
21                        $enable = function (buffer) {
22                            check_buffer(buffer, content_buffer);
23                            content_buffer_update_keymap_for_input_mode(buffer);
24                        },
25                        $disable = false,
26                        $doc = doc);
28 ignore_function_for_get_caller_source_code_reference("define_input_mode");
30 function content_buffer_update_keymap_for_input_mode (buffer) {
31     if (buffer.input_mode)
32         buffer.keymap = conkeror[content_buffer_input_mode_keymaps[buffer.input_mode]];
35 add_hook("content_buffer_location_change_hook", function (buf) { normal_input_mode(buf, true); });
38 // Input mode for "normal" view mode
39 define_input_mode("normal", null, "content_buffer_normal_keymap");
41 // Input modes for form elements
42 define_input_mode("select", "input:SELECT", "content_buffer_select_keymap");
43 define_input_mode("text", "input:TEXT", "content_buffer_text_keymap");
44 define_input_mode("textarea", "input:TEXTAREA", "content_buffer_textarea_keymap");
45 define_input_mode("richedit", "input:RICHEDIT", "content_buffer_richedit_keymap");
46 define_input_mode("checkbox", "input:CHECKBOX/RADIOBUTTON", "content_buffer_checkbox_keymap");
48 // Input modes for sending key events to gecko
49 define_input_mode(
50     "quote_next", "input:PASS-THROUGH(next)", "content_buffer_quote_next_keymap",
51     "This input mode sends the next key combo to the buffer, "+
52         "bypassing Conkeror's normal key handling.  The mode disengages "+
53         "after one key combo.");
54 define_input_mode(
55     "quote", "input:PASS-THROUGH", "content_buffer_quote_keymap",
56     "This input mode sends all key combos to the buffer, "+
57         "bypassing Conkeror's normal key handling, until the "+
58         "Escape key is pressed.");
60 // Input mode for the visible caret
61 define_input_mode("caret", null, "content_buffer_caret_keymap");
64 function content_buffer_update_input_mode_for_focus(buffer, force) {
65     var mode = buffer.input_mode;
66     var form_input_mode_enabled = (mode == "text_input_mode" ||
67                                    mode == "textarea_input_mode" ||
68                                    mode == "select_input_mode" ||
69                                    mode == "checkbox_input_mode" ||
70                                    mode == "richedit_input_mode");
72     if (force || form_input_mode_enabled || mode == "normal_input_mode") {
73         let elem = buffer.focused_element;
75         if (elem) {
76             var input_mode_function = null;
77             if (elem instanceof Ci.nsIDOMHTMLInputElement) {
78                 var type = elem.getAttribute("type");
79                 if (type != null) type = type.toLowerCase();
80                 if (type == "checkbox" || type == "radio")
81                     input_mode_function = checkbox_input_mode;
82                 else if (type != "submit" &&
83                          type != "reset")
84                     input_mode_function = text_input_mode;
85             }
86             else if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
87                 input_mode_function = textarea_input_mode;
89             else if (elem instanceof Ci.nsIDOMHTMLSelectElement)
90                 input_mode_function = select_input_mode;
91         }
93         if (!input_mode_function) {
95             let frame = buffer.focused_frame;
96             let in_design_mode = false;
97             if (frame && frame.document.designMode == "on")
98                 in_design_mode = true;
99             else {
100                 outer:
101                 while (elem) {
102                     switch (elem.contentEditable) {
103                     case "true":
104                         in_design_mode = true;
105                         break outer;
106                     case "false":
107                         break outer;
108                     default: // == "inherit"
109                         elem = elem.parentNode;
110                     }
111                 }
112             }
113             if (in_design_mode)
114                 input_mode_function = richedit_input_mode;
115         }
117         if (input_mode_function) {
118             if (!force &&
119                 browser_prevent_automatic_form_focus_mode_enabled &&
120                 !form_input_mode_enabled &&
121                 (buffer.last_user_input_received == null ||
122                  (Date.now() - buffer.last_user_input_received)
123                  > browser_automatic_form_focus_window_duration)) {
124                 // Automatic focus attempt blocked
125                 elem.blur();
126             } else
127                 input_mode_function(buffer, true);
128             return;
129         }
131         normal_input_mode(buffer, true);
132     }
135 add_hook("content_buffer_focus_change_hook",
136          function (buf) {
137              content_buffer_update_input_mode_for_focus(buf, false);
138          });
140 define_buffer_mode('caret_mode', 'CARET',
141                    $enable = function(buffer) {
142                        buffer.browser.setAttribute('showcaret', 'true');
143                        let sc = getFocusedSelCtrl(buffer);
144                        sc.setCaretEnabled(true);
145                        buffer.top_frame.focus();
146                        caret_input_mode(buffer, true);
147                    },
148                    $disable = function(buffer) {
149                        buffer.browser.setAttribute('showcaret', '');
150                        let sc = getFocusedSelCtrl(buffer);
151                        sc.setCaretEnabled(false);
152                        buffer.browser.focus();
153                        content_buffer_update_input_mode_for_focus(buffer, true);
154                    });
156 //XXX: CARET_PREF is defined in find.js---why?
157 watch_pref(CARET_PREF, function() {
158                if (get_pref(CARET_PREF)) {
159                    session_pref(CARET_PREF, false);
160                    let window = window_watcher.activeWindow;
161                    let buffer = window.buffers.current;
162                    caret_mode(buffer);
163                }
164            });
166 interactive("content-buffer-update-input-mode-for-focus", null, function (I) {
167     content_buffer_update_input_mode_for_focus(I.buffer, true);
170 function minibuffer_input_mode_indicator(window) {
171     this.window = window;
172     this.hook_func = method_caller(this, this.update);
173     add_hook.call(window, "select_buffer_hook", this.hook_func);
174     add_hook.call(window, "current_buffer_input_mode_change_hook", this.hook_func);
175     this.update();
178 minibuffer_input_mode_indicator.prototype = {
179     update : function () {
180         var buf = this.window.buffers.current;
181         var mode = buf.input_mode;
182         if (mode)
183             this.window.minibuffer.element.className = "minibuffer-" + buf.input_mode.replace("_","-","g");
184     },
185     uninstall : function () {
186         remove_hook.call(window, "select_buffer_hook", this.hook_func);
187         remove_hook.call(window, "current_buffer_input_mode_change_hook", this.hook_func);
188     }
191 define_global_window_mode("minibuffer_input_mode_indicator", "window_initialize_hook");
192 minibuffer_input_mode_indicator_mode(true);
194 // Milliseconds
195 define_variable("browser_automatic_form_focus_window_duration", 20,
196                 "Time window (in milliseconds) during which a form element "+
197                 "is allowed to gain focus following a mouse click or key "+
198                 "press, if `browser_prevent_automatic_form_focus_mode' is "+
199                 "enabled.");;
201 define_global_mode("browser_prevent_automatic_form_focus_mode",
202                    function () {}, // enable
203                    function () {} // disable
204                   );
206 // note: The apparent misspellings here are not a bug.
207 // see https://developer.mozilla.org/en/XPath/Functions/translate
209 define_variable(
210     "browser_form_field_xpath_expression",
211     "//input[" + (
212         //        "translate(@type,'RADIO','radio')!='radio' and " +
213         //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
214         "translate(@type,'HIDEN','hiden')!='hidden'"
215         //        "translate(@type,'SUBMIT','submit')!='submit' and " +
216         //        "translate(@type,'REST','rest')!='reset'"
217     ) +  "] | " +
218         "//xhtml:input[" + (
219             //        "translate(@type,'RADIO','radio')!='radio' and " +
220             //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
221             "translate(@type,'HIDEN','hiden')!='hidden'"
222             //        "translate(@type,'SUBMIT','submit')!='submit' and " +
223             //        "translate(@type,'REST','rest')!='reset'"
224         ) +  "] |" +
225         "//select | //xhtml:select | " +
226         "//textarea | //xhtml:textarea | " +
227         "//textbox | //xul:textbox",
228     "XPath expression matching elements to be selected by `browser-focus-next-form-field' " +
229         "and `browser-focus-previous-form-field.'");
231 function focus_next (buffer, count, xpath_expr, name) {
232     var focused_elem = buffer.focused_element;
233     if (count == 0)
234         return; // invalid count
236     function helper (win, skip_win) {
237         if (win == skip_win)
238             return null;
239         var doc = win.document;
240         var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
241             Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
242             null /* existing results */);
243         var length = res.snapshotLength;
244         if (length > 0) {
245             let valid_nodes = [];
246             for (let i = 0; i < length; ++i) {
247                 let elem = res.snapshotItem(i);
248                 if (elem.offsetWidth == 0 ||
249                     elem.offsetHeight == 0)
250                     continue;
251                 let style = win.getComputedStyle(elem, "");
252                 if (style.display == "none" || style.visibility == "hidden")
253                     continue;
254                 valid_nodes.push(elem);
255             }
257             if (valid_nodes.length > 0) {
258                 var index = -1;
259                 if (focused_elem != null)
260                     index = valid_nodes.indexOf(focused_elem);
261                 if (index == -1) {
262                     if (count > 0)
263                         index = count - 1;
264                     else
265                         index = -count;
266                 }
267                 else
268                     index = index + count;
269                 index = index % valid_nodes.length;
270                 if (index < 0)
271                     index += valid_nodes.length;
273                 return valid_nodes[index];
274             }
275         }
276         // Recurse on sub-frames
277         for (var i = 0; i < win.frames.length; ++i) {
278             var elem = helper(win.frames[i], skip_win);
279             if (elem)
280                 return elem;
281         }
282         return null;
283     }
285     var focused_win = buffer.focused_frame;
286     var elem = helper(focused_win, null);
287     if (!elem)
288         // if focused_frame is top_frame, we're doing twice as much
289         // work as necessary
290         elem = helper(buffer.top_frame, focused_win);
291     if (elem) {
292         browser_element_focus(buffer, elem);
293     } else
294         throw interactive_error("No "+name+" found");
297 interactive("browser-focus-next-form-field",
298             "Focus the next element matching "+
299             "`browser_form_field_xpath_expression'.",
300             function (I) {
301                 focus_next(I.buffer, I.p,
302                            browser_form_field_xpath_expression,
303                            "form field");
304             });
306 interactive("browser-focus-previous-form-field",
307             "Focus the previous element matching "+
308             "`browser_form_field_xpath_expression'.",
309             function (I) {
310                 focus_next(I.buffer, -I.p,
311                            browser_form_field_xpath_expression,
312                            "form field");
313             });
316 define_variable("links_xpath_expression",
317     "//*[@onclick or @onmouseover or @onmousedown or "+
318         "@onmouseup or @oncommand or @role='link'] | " +
319     "//input[not(@type='hidden')] | //a | //area | "+
320     "//iframe | //textarea | //button | //select",
321     "XPath expression matching elements to be selected by "+
322     "`focus-next-link' and `focus-previous-link.'");
324 interactive("focus-next-link",
325             "Focus the next element matching `links_xpath_expression'.",
326             function (I) {
327                 focus_next(I.buffer, I.p,
328                            links_xpath_expression,
329                            "link");
330             });
332 interactive("focus-previous-link",
333             "Focus the previous element matching `links_xpath_expression'.",
334             function (I) {
335                 focus_next(I.buffer, -I.p,
336                            links_xpath_expression,
337                            "link");
338             });
341 define_variable('edit_field_in_external_editor_extension', "txt",
342     "File extension for the temp files created by "+
343     "edit-current-field-in-external-editor.");
345 function get_filename_for_current_textfield(doc, elem) {
346     var name = doc.URL
347         + "-"
348         + ( elem.getAttribute("name")
349             || elem.getAttribute("id")
350             || "textarea" );
352     // get rid filesystem unfriendly chars
353     name = name.replace(doc.location.protocol, "")
354         .replace(/[^a-zA-Z0-9]+/g, "-")
355         .replace(/(^-+|-+$)/g, "")
356         + '.' + edit_field_in_external_editor_extension;
358     return name;
361 function edit_field_in_external_editor(buffer, elem) {
362     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
363         var type = elem.getAttribute("type");
364         if (type != null)
365             type = type.toLowerCase();
366         if (type == "hidden" || type == "checkbox" || type == "radio")
367             throw interactive_error("Element is not a text field.");
368     } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
369         throw interactive_error("Element is not a text field.");
371     var name = get_filename_for_current_textfield(buffer.document, elem);
372     var file = get_temporary_file(name);
374     // Write to file
375     try {
376         write_text_file(file, elem.value);
377     } catch (e) {
378         file.remove(false);
379         throw e;
380     }
382     // FIXME: decide if we should do this
383     var old_class = elem.className;
384     elem.className = "__conkeror_textbox_edited_externally " + old_class;
386     try {
387         yield open_file_with_external_editor(file);
389         elem.value = read_text_file(file);
390     } finally {
391         elem.className = old_class;
393         file.remove(false);
394     }
397 interactive("edit-current-field-in-external-editor",
398             "Edit the contents of the currently-focused text field in an external editor.",
399             function (I) {
400                 var buf = I.buffer;
401                 var elem = buf.focused_element;
402                 yield edit_field_in_external_editor(buf, elem);
403                 elem.blur();
404             });
406 define_variable("kill_whole_line", false,
407                 "If true, `kill-line' with no arg at beg of line kills the whole line.");
409 function cut_to_end_of_line (buffer) {
410     var elem = buffer.focused_element;
411     try {
412         var st = elem.selectionStart;
413         var en = elem.selectionEnd;
414         if (st == en) {
415             // there is no selection.  set one up.
416             var eol = elem.value.indexOf ("\n", en);
417             if (eol == -1)
418             {
419                 elem.selectionEnd = elem.textLength;
420             } else if (eol == st) {
421                 elem.selectionEnd = eol + 1;
422             } else if (kill_whole_line &&
423                        (st == 0 || elem.value[st - 1] == "\n"))
424             {
425                 elem.selectionEnd = eol + 1;
426             } else {
427                 elem.selectionEnd = eol;
428             }
429         }
430         buffer.do_command ('cmd_cut');
431     } catch (e) {
432         /* FIXME: Make this work for richedit mode as well */
433     }
435 interactive("cut-to-end-of-line",
436     null,
437     function (I) {
438         cut_to_end_of_line (I.buffer);
439     });
442 function downcase_word(I) {
443     modify_word_at_point(I, function (word) { return word.toLocaleLowerCase(); });
445 interactive("downcase-word",
446             "Convert following word to lower case, moving over.",
447             downcase_word);
450 function upcase_word(I) {
451     modify_word_at_point(I, function (word) { return word.toLocaleUpperCase(); });
453 interactive("upcase-word",
454             "Convert following word to upper case, moving over.",
455             upcase_word);
458 function capitalize_word(I) {
459     modify_word_at_point(I, function (word) {
460         if (word.length > 0) {
461             return word[0].toLocaleUpperCase() + word.substring(1);
462         }
463         return word;
464     });
466 interactive("capitalize-word",
467             "Capitalize the following word (or arg words), moving over.",
468             capitalize_word);