Added API to manipulate external launchers for MIME types.
[conkeror.git] / modules / content-buffer-input.js
blob201eddbd3a3aa6535dc19b8368180e53d38f9654
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                          disable: false,
25                          doc: doc });
27 ignore_function_for_get_caller_source_code_reference("define_input_mode");
29 function content_buffer_update_keymap_for_input_mode(buffer) {
30     if (buffer.input_mode)
31         buffer.keymap = buffer.get(content_buffer_input_mode_keymaps[buffer.input_mode]);
34 add_hook("page_mode_change_hook", content_buffer_update_keymap_for_input_mode);
36 add_hook("content_buffer_location_change_hook", function (buf) { normal_input_mode(buf, true); });
38 define_input_mode("normal", null, "content_buffer_normal_keymap");
40 // For SELECT elements
41 define_input_mode("select", "input:SELECT", "content_buffer_select_keymap");
43 // For text INPUT and TEXTAREA elements
44 define_input_mode("text", "input:TEXT", "content_buffer_text_keymap");
45 define_input_mode("textarea", "input:TEXTAREA", "content_buffer_textarea_keymap");
46 define_input_mode("richedit", "input:RICHEDIT", "content_buffer_richedit_keymap");
48 define_input_mode("checkbox", "input:CHECKBOX/RADIOBUTTON", "content_buffer_checkbox_keymap");
50 define_input_mode(
51     "quote_next", "input:PASS-THROUGH(next)", "content_buffer_quote_next_keymap",
52     "This input mode sends the next key combo to the buffer, "+
53         "bypassing Conkeror's normal key handling.  The mode disengages "+
54         "after one key combo.");
55 define_input_mode(
56     "quote", "input:PASS-THROUGH", "content_buffer_quote_keymap",
57     "This input mode sends all key combos to the buffer, "+
58         "bypassing Conkeror's normal key handling, until the "+
59         "Escape key is pressed.");
61 define_input_mode("caret", null, "content_buffer_caret_keymap");
63 function content_buffer_update_input_mode_for_focus(buffer, force) {
64     var mode = buffer.input_mode;
65     var form_input_mode_enabled = (mode == "text_input_mode" ||
66                                    mode == "textarea_input_mode" ||
67                                    mode == "select_input_mode" ||
68                                    mode == "checkbox_input_mode" ||
69                                    mode == "richedit_input_mode");
71     if (force || form_input_mode_enabled || mode == "normal_input_mode") {
72         let elem = buffer.focused_element;
74         if (elem) {
75             var input_mode_function = null;
76             if (elem instanceof Ci.nsIDOMHTMLInputElement) {
77                 var type = elem.getAttribute("type");
78                 if (type != null) type = type.toLowerCase();
79                 if (type == "checkbox" || type == "radio")
80                     input_mode_function = checkbox_input_mode;
81                 else if (type != "submit" &&
82                          type != "reset")
83                     input_mode_function = text_input_mode;
84             }
85             else if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
86                 input_mode_function = textarea_input_mode;
88             else if (elem instanceof Ci.nsIDOMHTMLSelectElement)
89                 input_mode_function = select_input_mode;
90         }
92         if (!input_mode_function) {
94             let frame = buffer.focused_frame;
95             let in_design_mode = false;
96             if (frame && frame.document.designMode == "on")
97                 in_design_mode = true;
98             else {
99                 outer:
100                 while (elem) {
101                     switch (elem.contentEditable) {
102                     case "true":
103                         in_design_mode = true;
104                         break outer;
105                     case "false":
106                         break outer;
107                     default: // == "inherit"
108                         elem = elem.parentNode;
109                     }
110                 }
111             }
112             if (in_design_mode)
113                 input_mode_function = richedit_input_mode;
114         }
116         if (input_mode_function) {
117             if (!force &&
118                 browser_prevent_automatic_form_focus_mode_enabled &&
119                 !form_input_mode_enabled &&
120                 (buffer.last_user_input_received == null ||
121                  (Date.now() - buffer.last_user_input_received)
122                  > browser_automatic_form_focus_window_duration)) {
123                 // Automatic focus attempt blocked
124                 elem.blur();
125             } else
126                 input_mode_function(buffer, true);
127             return;
128         }
130         normal_input_mode(buffer, true);
131     }
134 add_hook("content_buffer_focus_change_hook", function (buf) { content_buffer_update_input_mode_for_focus(buf, false); } );
136 define_buffer_mode('caret_mode', 'CARET',
137                    { enable: function(buffer) {
138                        buffer.browser.setAttribute('showcaret', 'true');
139                        let sc = getFocusedSelCtrl(buffer);
140                        let s = sc.getSelection(sc.SELECTION_NORMAL);
141                        if(s.anchorNode) {
142                            s.collapseToStart();
143                        } else {
144                            s.collapse(buffer.document.body, 0);
145                        }
146                        sc.setCaretEnabled(true);
147                        buffer.top_frame.focus();
148                        caret_input_mode(buffer, true);
149                    },
150                      disable: function(buffer) {
151                        buffer.browser.setAttribute('showcaret', '');
152                        let sc = getFocusedSelCtrl(buffer);
153                        sc.setCaretEnabled(false);
154                        buffer.browser.focus();
155                        content_buffer_update_input_mode_for_focus(buffer, true);
156                      }});
158 watch_pref(CARET_PREF, function() {
159                if (get_pref(CARET_PREF)) {
160                    session_pref(CARET_PREF, false);
161                    let window = window_watcher.activeWindow;
162                    let buffer = window.buffers.current;
163                    caret_mode(buffer);
164                }
165            });
167 interactive("content-buffer-update-input-mode-for-focus", null, function (I) {
168     content_buffer_update_input_mode_for_focus(I.buffer, true);
171 function minibuffer_input_mode_indicator(window) {
172     this.window = window;
173     this.hook_func = method_caller(this, this.update);
174     add_hook.call(window, "select_buffer_hook", this.hook_func);
175     add_hook.call(window, "current_buffer_input_mode_change_hook", this.hook_func);
176     this.update();
179 minibuffer_input_mode_indicator.prototype = {
180     update : function () {
181         var buf = this.window.buffers.current;
182         var mode = buf.input_mode;
183         if (mode)
184             this.window.minibuffer.element.className = "minibuffer-" + buf.input_mode.replace("_","-","g");
185     },
186     uninstall : function () {
187         remove_hook.call(window, "select_buffer_hook", this.hook_func);
188         remove_hook.call(window, "current_buffer_input_mode_change_hook", this.hook_func);
189     }
192 define_global_window_mode("minibuffer_input_mode_indicator", "window_initialize_hook");
193 minibuffer_input_mode_indicator_mode(true);
195 // Milliseconds
196 define_variable("browser_automatic_form_focus_window_duration", 20, "Time window (in milliseconds) during which a form element is allowed to gain focus following a mouse click or key press, if `browser_prevent_automatic_form_focus_mode' is enabled.");;
198 define_global_mode("browser_prevent_automatic_form_focus_mode",
199                    function () {}, // enable
200                    function () {} // disable
201                   );
202 browser_prevent_automatic_form_focus_mode(true);
204 define_variable(
205     "browser_form_field_xpath_expression",
206     "//input[" + (
207         //        "translate(@type,'RADIO','radio')!='radio' and " +
208         //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
209         "translate(@type,'HIDEN','hiden')!='hidden'"
210         //        "translate(@type,'SUBMIT','submit')!='submit' and " +
211         //        "translate(@type,'REST','rest')!='reset'"
212     ) +  "] | " +
213         "//xhtml:input[" + (
214             //        "translate(@type,'RADIO','radio')!='radio' and " +
215             //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
216             "translate(@type,'HIDEN','hiden')!='hidden'"
217             //        "translate(@type,'SUBMIT','submit')!='submit' and " +
218             //        "translate(@type,'REST','rest')!='reset'"
219         ) +  "] |" +
220         "//select | //xhtml:select | " +
221         "//textarea | //xhtml:textarea | " +
222         "//textbox | //xul:textbox",
223     "XPath expression matching elements to be selected by `browser-focus-next-form-field' " +
224         "and `browser-focus-previous-form-field.'");
226 function browser_focus_next_form_field(buffer, count, xpath_expr) {
227     var focused_elem = buffer.focused_element;
228     if (count == 0)
229         return; // invalid count
231     function helper(win, skip_win) {
232         if (win == skip_win)
233             return null;
234         var doc = win.document;
235         var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
236             Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
237             null /* existing results */);
238         var length = res.snapshotLength;
239         if (length > 0) {
240             var index = null;
241             if (focused_elem != null) {
242                 for (var i = 0; i < length; ++i) {
243                     if (res.snapshotItem(i) == focused_elem) {
244                         index = i;
245                         break;
246                     }
247                 }
248             }
249             if (index == null) {
250                 if (count > 0)
251                     index = count - 1;
252                 else
253                     index = -count;
254             }
255             else
256                 index = index + count;
257             index = index % length;
258             if (index < 0)
259                 index += length;
261             return res.snapshotItem(index);
262         }
264         // Recurse on sub-frames
265         for (var i = 0; i < win.frames.length; ++i) {
266             var elem = helper(win.frames[i], skip_win);
267             if (elem)
268                 return elem;
269         }
270         return null;
271     }
273     var focused_win = buffer.focused_frame;
274     var elem = helper(focused_win, null);
275     if (!elem)
276         elem = helper(buffer.top_frame, focused_win);
277     if (elem) {
278         browser_element_focus(buffer, elem);
279     } else
280         throw interactive_error("No form field found");
283 interactive("browser-focus-next-form-field", "Focus the next element matching `browser_form_field_xpath_expression'.",
284             function (I) {
285                 browser_focus_next_form_field(I.buffer, I.p, browser_form_field_xpath_expression);
286             });
288 interactive("browser-focus-previous-form-field",  "Focus the previous element matching `browser_form_field_xpath_expression'.",
289             function (I) {
290                 browser_focus_next_form_field(I.buffer, -I.p, browser_form_field_xpath_expression);
291             });
293 function edit_field_in_external_editor(buffer, elem) {
294     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
295         var type = elem.getAttribute("type");
296         if (type != null)
297             type = type.toLowerCase();
298         if (type == "hidden" || type == "checkbox" || type == "radio")
299             throw interactive_error("Element is not a text field.");
300     } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
301         throw interactive_error("Element is not a text field.");
303     var name = elem.getAttribute("name");
304     if (!name || name.length == 0)
305         name = elem.getAttribute("id");
306     if (!name)
307         name = "";
308     name = name.replace(/[^a-zA-Z0-9\-_]/g, "");
309     if (name.length == 0)
310         name = "text";
311     name += ".txt";
312     var file = get_temporary_file(name);
314     // Write to file
315     try {
316         write_text_file(file, elem.value);
317     } catch (e) {
318         file.remove(false);
319         throw e;
320     }
322     // FIXME: decide if we should do this
323     var old_class = elem.className;
324     elem.className = "__conkeror_textbox_edited_externally " + old_class;
326     try {
327         yield open_file_with_external_editor(file);
329         elem.value = read_text_file(file);
330     } finally {
331         elem.className = old_class;
333         file.remove(false);
334     }
336 interactive("edit-current-field-in-external-editor", "Edit the contents of the currently-focused text field in an external editor.",
337             function (I) {
338                 var buf = I.buffer;
339                 yield edit_field_in_external_editor(buf, buf.focused_element);
340                 unfocus(I.window, buf);
341             });
343 define_variable("kill_whole_line", false, "If true, `kill-line' with no arg at beg of line kills the whole line.");
345 function cut_to_end_of_line (buffer) {
346     var elem = buffer.focused_element;
347     try {
348         var st = elem.selectionStart;
349         var en = elem.selectionEnd;
350         if (st == en) {
351             // there is no selection.  set one up.
352             var eol = elem.value.indexOf ("\n", en);
353             if (eol == -1)
354             {
355                 elem.selectionEnd = elem.textLength;
356             } else if (eol == st) {
357                 elem.selectionEnd = eol + 1;
358             } else if (kill_whole_line &&
359                        (st == 0 || elem.value[st - 1] == "\n"))
360             {
361                 elem.selectionEnd = eol + 1;
362             } else {
363                 elem.selectionEnd = eol;
364             }
365         }
366         buffer.do_command ('cmd_cut');
367     } catch (e) {
368         /* FIXME: Make this work for richedit mode as well */
369     }
372 interactive (
373     "cut-to-end-of-line",
374     null,
375     function (I) {
376         cut_to_end_of_line (I.buffer);
377     });