Improved "mode" infrastructure, including support for page modes
[conkeror.git] / modules / content-buffer-input.js
blob71e4fe6820ed7840a48e11749b2f7b721ed1d85b
1 require("content-buffer.js");
3 define_current_buffer_hook("current_buffer_input_mode_change_hook", "input_mode_change_hook");
5 function define_input_mode(base_name, display_name, keymap_name, doc) {
6     define_buffer_mode(base_name + "_input_mode",
7                        display_name,
8                        $class = "input_mode",
9                        $enable = function (buffer) { check_buffer(buffer, content_buffer);
10                                                      buffer.keymap = buffer.get(keymap_name); },
11                        $disable = false,
12                        $doc = doc);
14 ignore_function_for_get_caller_source_code_reference("define_input_mode");
16 define_input_mode("normal", null, "content_buffer_normal_keymap");
18 // For SELECT elements
19 define_input_mode("select", "input:SELECT", "content_buffer_select_keymap");
21 // For text INPUT and TEXTAREA elements
22 define_input_mode("text", "input:TEXT", "content_buffer_text_keymap");
23 define_input_mode("textarea", "input:TEXTAREA", "content_buffer_textarea_keymap");
25 define_input_mode(
26     "quote_next", "input:PASS-THROUGH(next)", "content_buffer_quote_next_keymap",
27     "This input mode sends the next key combo to the buffer, "+
28         "bypassing Conkeror's normal key handling.  The mode disengages "+
29         "after one key combo.");
30 define_input_mode(
31     "quote", "input:PASS-THROUGH", "content_buffer_quote_keymap",
32     "This input mode sends all key combos to the buffer, "+
33         "bypassing Conkeror's normal key handling, until the "+
34         "Escape key is pressed.");
36 add_hook("content_buffer_focus_change_hook", function (buffer) {
37     var mode = buffer.input_mode;
38     var form_input_mode_enabled = (mode == "text_input_mode" ||
39                                    mode == "textarea_input_mode" ||
40                                    mode == "select_input_mode");
42     if (form_input_mode_enabled || mode == "normal_input_mode") {
43         var elem = buffer.focused_element;
45         if (elem) {
46             var input_mode_function = null;
47             if (elem instanceof Ci.nsIDOMHTMLInputElement) {
48                 var type = elem.getAttribute("type");
49                 if (type != null) type = type.toLowerCase();
50                 if (type != "radio" &&
51                     type != "checkbox" &&
52                     type != "submit" &&
53                     type != "reset")
54                     input_mode_function = text_input_mode;
55             }
56             else if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
57                 input_mode_function = textarea_input_mode;
59             else if (elem instanceof Ci.nsIDOMHTMLSelectElement)
60                 input_mode_function = select_input_mode;
62             if (input_mode_function) {
63                 if (browser_prevent_automatic_form_focus_mode_enabled &&
64                     !form_input_mode_enabled &&
65                     (buffer.last_user_input_received == null ||
66                      (Date.now() - buffer.last_user_input_received)
67                      > browser_automatic_form_focus_window_duration)) {
68                     // Automatic focus attempt blocked
69                     elem.blur();
70                 } else
71                     input_mode_function(buffer);
72                 return;
73             }
74         }
75         normal_input_mode(buffer);
76     }
77 });
79 function minibuffer_input_mode_indicator(window) {
80     this.window = window;
81     this.hook_func = method_caller(this, this.update);
82     add_hook.call(window, "select_buffer_hook", this.hook_func);
83     add_hook.call(window, "current_buffer_input_mode_change_hook", this.hook_func);
84     this.update();
87 minibuffer_input_mode_indicator.prototype = {
88     update : function () {
89         var buf = this.window.buffers.current;
90         var mode = buf.input_mode;
91         if (mode)
92             this.window.minibuffer.element.className = "minibuffer-" + buf.input_mode.replace("_","-","g");
93     },
94     uninstall : function () {
95         remove_hook.call(window, "select_buffer_hook", this.hook_func);
96         remove_hook.call(window, "current_buffer_input_mode_change_hook", this.hook_func);
97     }
100 define_global_window_mode("minibuffer_input_mode_indicator", "window_initialize_hook");
101 minibuffer_input_mode_indicator_mode(true);
103 // Milliseconds
104 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.");;
106 define_global_mode("browser_prevent_automatic_form_focus_mode",
107                    function () {}, // enable
108                    function () {} // disable
109                    );
110 browser_prevent_automatic_form_focus_mode(true);
112 define_variable(
113     "browser_form_field_xpath_expression",
114     "//input[" + (
115 //        "translate(@type,'RADIO','radio')!='radio' and " +
116 //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
117         "translate(@type,'HIDEN','hiden')!='hidden'"
118 //        "translate(@type,'SUBMIT','submit')!='submit' and " +
119 //        "translate(@type,'REST','rest')!='reset'"
120     ) +  "] | " +
121         "//xhtml:input[" + (
122 //        "translate(@type,'RADIO','radio')!='radio' and " +
123 //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
124             "translate(@type,'HIDEN','hiden')!='hidden'"
125 //        "translate(@type,'SUBMIT','submit')!='submit' and " +
126 //        "translate(@type,'REST','rest')!='reset'"
127         ) +  "] |" +
128         "//select | //xhtml:select | " +
129         "//textarea | //xhtml:textarea | " +
130         "//textbox | //xul:textbox",
131     "XPath expression matching elements to be selected by `browser-focus-next-form-field' " +
132         "and `browser-focus-previous-form-field.'");
134 function browser_focus_next_form_field(buffer, count, xpath_expr) {
135     var focused_elem = buffer.focused_element;
136     if (count == 0)
137         return; // invalid count
139     function helper(win, skip_win) {
140         if (win == skip_win)
141             return null;
142         var doc = win.document;
143         var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
144                                Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
145                                null /* existing results */);
146         var length = res.snapshotLength;
147         if (length > 0) {
148             var index = null;
149             if (focused_elem != null) {
150                 for (var i = 0; i < length; ++i) {
151                     if (res.snapshotItem(i) == focused_elem) {
152                         index = i;
153                         break;
154                     }
155                 }
156             }
157             if (index == null) {
158                 if (count > 0)
159                     index = count - 1;
160                 else
161                     index = -count;
162             }
163             else
164                 index = index + count;
165             index = index % length;
166             if (index < 0)
167                 index += length;
169             return res.snapshotItem(index);
170         }
172         // Recurse on sub-frames
173         for (var i = 0; i < win.frames.length; ++i) {
174             var elem = helper(win.frames[i], skip_win);
175             if (elem)
176                 return elem;
177         }
178         return null;
179     }
181     var focused_win = buffer.focused_frame;
182     var elem = helper(focused_win, null);
183     if (!elem)
184         elem = helper(buffer.top_frame, focused_win);
185     if (elem) {
186         browser_element_focus(buffer, elem);
187     } else
188         throw interactive_error("No form field found");
191 interactive("browser-focus-next-form-field", "Focus the next element matching `browser_form_field_xpath_expression'.",
192             function (I) {
193                 browser_focus_next_form_field(I.buffer, I.p, browser_form_field_xpath_expression);
194             });
196 interactive("browser-focus-previous-form-field",  "Focus the previous element matching `browser_form_field_xpath_expression'.",
197             function (I) {
198                 browser_focus_next_form_field(I.buffer, -I.p, browser_form_field_xpath_expression);
201 function edit_field_in_external_editor(buffer, elem) {
202     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
203         var type = elem.getAttribute("type");
204         if (type != null)
205             type = type.toLowerCase();
206         if (type == "hidden" || type == "checkbox" || type == "radio")
207             throw interactive_error("Element is not a text field.");
208     } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
209         throw interactive_error("Element is not a text field.");
211     var name = elem.getAttribute("name");
212     if (!name || name.length == 0)
213         name = elem.getAttribute("id");
214     if (!name)
215         name = "";
216     name = name.replace(/[^a-zA-Z0-9\-_]/g, "");
217     if (name.length == 0)
218         name = "text";
219     name += ".txt";
220     var file = get_temporary_file(name);
222     // Write to file
223     try {
224         write_text_file(file, elem.value);
225     } catch (e) {
226         file.remove(false);
227         throw e;
228     }
230     // FIXME: decide if we should do this
231     var old_class = elem.className;
232     elem.className = "__conkeror_textbox_edited_externally " + old_class;
234     try {
235         yield open_file_with_external_editor(file);
237         elem.value = read_text_file(file);
238     } finally {
239         elem.className = old_class;
241         file.remove(false);
242     }
244 interactive("edit-current-field-in-external-editor", "Edit the contents of the currently-focused text field in an external editor.",
245             function (I) {
246     var buf = I.buffer;
247     yield edit_field_in_external_editor(buf, buf.focused_element);
248     unfocus(buf);
251 define_variable("kill_whole_line", false, "If true, `kill-line' with no arg at beg of line kills the whole line.");
253 function cut_to_end_of_line (buffer) {
254     var elem = buffer.focused_element;
255     var st = elem.selectionStart;
256     var en = elem.selectionEnd;
257     if (st == en) {
258         // there is no selection.  set one up.
259         var eol = elem.value.indexOf ("\n", en);
260         if (eol == -1)
261         {
262             elem.selectionEnd = elem.textLength;
263         } else if (eol == st) {
264             elem.selectionEnd = eol + 1;
265         } else if (kill_whole_line &&
266                    (st == 0 || elem.value[st - 1] == "\n"))
267         {
268             elem.selectionEnd = eol + 1;
269         } else {
270             elem.selectionEnd = eol;
271         }
272     }
273     buffer.do_command ('cmd_cut');
276 interactive (
277     "cut-to-end-of-line",
278     function (I) {
279         cut_to_end_of_line (I.buffer);
280     });