Add support for documenting user variables.
[conkeror.git] / modules / content-buffer-input.js
blob955b54432b6f164554c51c5da6961976aaa41b15
1 require("content-buffer.js");
3 define_buffer_local_hook("content_buffer_input_mode_change_hook");
4 define_current_buffer_hook("current_content_buffer_input_mode_change_hook", "content_buffer_input_mode_change_hook");
6 function define_content_buffer_input_mode(base_name, keymap_name, doc) {
7     var name = "content_buffer_" + base_name + "_input_mode";
8     buffer[name + "_enabled"] = false;
9     define_buffer_local_hook(name + "_enable_hook");
10     define_buffer_local_hook(name + "_disable_hook");
11     conkeror[name] = function (buffer) {
12         if (buffer[name + "_enabled"])
13             return;
14         if (buffer.current_input_mode) {
15             conkeror[buffer.current_input_mode + "_disable_hook"].run(buffer);
16             buffer[buffer.current_input_mode + "_enabled"] = false;
17         }
18         buffer.current_input_mode = name;
19         buffer[name + "_enabled"] = true;
20         buffer.keymap = conkeror[keymap_name];
21         conkeror[name + "_enable_hook"].run(buffer);
22         content_buffer_input_mode_change_hook.run(buffer);
23     }
24     var hyphen_name = name.replace("_","-","g");
25     interactive(hyphen_name, doc, function(I) {conkeror[name](check_buffer(I.buffer,content_buffer));});
28 define_content_buffer_input_mode("normal", "content_buffer_normal_keymap");
30 // For SELECT elements
31 define_content_buffer_input_mode("select", "content_buffer_select_keymap");
33 // For text INPUT and TEXTAREA elements
34 define_content_buffer_input_mode("text", "content_buffer_text_keymap");
35 define_content_buffer_input_mode("textarea", "content_buffer_textarea_keymap");
37 define_content_buffer_input_mode(
38     "quote_next", "content_buffer_quote_next_keymap",
39     "This input mode sends the next key combo to the buffer, "+
40         "bypassing Conkeror's normal key handling.  The mode disengages "+
41         "after one key combo.");
42 define_content_buffer_input_mode(
43     "quote", "content_buffer_quote_keymap",
44     "This input mode sends all key combos to the buffer, "+
45         "bypassing Conkeror's normal key handling, until the "+
46         "Escape key is pressed.");
48 add_hook("content_buffer_focus_change_hook", function (buffer) {
49         var form_input_mode_enabled =
50             buffer.content_buffer_text_input_mode_enabled ||
51             buffer.content_buffer_textarea_input_mode_enabled ||
52             buffer.content_buffer_select_input_mode_enabled;
54         if (form_input_mode_enabled || buffer.content_buffer_normal_input_mode_enabled) {
55             var elem = buffer.focused_element;
57             if (elem) {
58                 var input_mode_function = null;
59                 if (elem instanceof Ci.nsIDOMHTMLInputElement) {
60                     var type = elem.getAttribute("type");
61                     if (type != null) type = type.toLowerCase();
62                     if (type != "radio" &&
63                         type != "checkbox" &&
64                         type != "submit" &&
65                         type != "reset")
66                         input_mode_function = content_buffer_text_input_mode;
67                 }
68                 else if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
69                     input_mode_function = content_buffer_textarea_input_mode;
71                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement)
72                     input_mode_function = content_buffer_select_input_mode;
74                 if (input_mode_function) {
75                     if (browser_prevent_automatic_form_focus_mode_enabled &&
76                         !form_input_mode_enabled &&
77                         (buffer.last_user_input_received == null ||
78                          (Date.now() - buffer.last_user_input_received)
79                          > browser_automatic_form_focus_window_duration)) {
80                         // Automatic focus attempt blocked
81                         elem.blur();
82                     } else
83                         input_mode_function(buffer);
84                     return;
85                 }
86             }
87             content_buffer_normal_input_mode(buffer);
88         }
89     });
91 function browser_input_minibuffer_status(window) {
92     this.window = window;
93     var element = create_XUL(window, "label");
94     element.setAttribute("id", "minibuffer-input-status");
95     element.collapsed = true;
96     element.setAttribute("class", "minibuffer");
97     var insert_before = window.document.getElementById("minibuffer-prompt");
98     insert_before.parentNode.insertBefore(element, insert_before);
99     this.element = element;
100     this.select_buffer_hook_func = add_hook.call(window, "select_buffer_hook", method_caller(this, this.update));
101     this.mode_change_hook_func = add_hook.call(window, "current_content_buffer_input_mode_change_hook",
102                                                method_caller(this, this.update));
103     this.update();
105 browser_input_minibuffer_status.prototype = {
106     update : function () {
107         var buf = this.window.buffers.current;
108         if (buf.content_buffer_normal_input_mode_enabled) {
109             this.element.collapsed = true;
110             this.window.minibuffer.element.className = "";
111         } else {
112             var name = "";
113             var className = null;
114             if (buf.content_buffer_text_input_mode_enabled) {
115                 name = "[TEXT INPUT]";
116                 className = "text";
117             } else if (buf.content_buffer_textarea_input_mode_enabled) {
118                 name = "[TEXTAREA INPUT]";
119                 className = "textarea";
120             } else if (buf.content_buffer_select_input_mode_enabled) {
121                 name = "[SELECT INPUT]";
122                 className = "select";
123             } else if (buf.content_buffer_quote_next_input_mode_enabled) {
124                 name = "[PASS THROUGH (next)]";
125                 className = "quote-next";
126             } else if (buf.content_buffer_quote_input_mode_enabled) {
127                 name = "[PASS THROUGH]";
128                 className = "quote";
129             } else /* error */;
131             this.element.value = name;
132             this.element.collapsed = false;
133             this.window.minibuffer.element.className = "minibuffer-" + className + "-input-mode";
134         }
135     },
136     uninstall : function () {
137         remove_hook.call(window, "select_buffer_hook", this.select_buffer_hook_func);
138         remove_hook.call(window, "current_content_buffer_input_mode_change_hook",
139                          method_caller(this, this.update), this.mode_change_hook_func);
140         this.element.parentNode.removeChild(this.element);
141     }
144 function browser_input_minibuffer_status_install(window) {
145     if (window.browser_input_minibuffer_status)
146         throw new Error("browser input minibuffer status already initialized for window");
147     window.browser_input_minibuffer_status = new browser_input_minibuffer_status(window);
150 function browser_input_minibuffer_status_uninstall(window) {
151     if (window.browser_input_minibuffer_status)
152         return;
153     window.browser_input_minibuffer_status.uninstall();
154     delete window.browser_input_minibuffer_status;
157 define_global_mode("browser_input_minibuffer_status_mode",
158                    function () { // enable
159                        add_hook("window_initialize_hook", browser_input_minibuffer_status_install);
160                        for_each_window(browser_input_minibuffer_status_install);
161                    },
162                    function () { // disable
163                        remove_hook("window_initialize_hook", browser_input_minibuffer_status_install);
164                        for_each_window(browser_input_minibuffer_status_uninstall);
165                    });
166 browser_input_minibuffer_status_mode(true);
168 // Milliseconds
169 define_user_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.");;
171 define_global_mode("browser_prevent_automatic_form_focus_mode",
172                    function () {}, // enable
173                    function () {} // disable
174                    );
175 browser_prevent_automatic_form_focus_mode(true);
177 define_user_variable(
178     "browser_form_field_xpath_expression",
179     "//input[" + (
180 //        "translate(@type,'RADIO','radio')!='radio' and " +
181 //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
182         "translate(@type,'HIDEN','hiden')!='hidden'"
183 //        "translate(@type,'SUBMIT','submit')!='submit' and " +
184 //        "translate(@type,'REST','rest')!='reset'"
185     ) +  "] | " +
186         "//xhtml:input[" + (
187 //        "translate(@type,'RADIO','radio')!='radio' and " +
188 //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
189             "translate(@type,'HIDEN','hiden')!='hidden'"
190 //        "translate(@type,'SUBMIT','submit')!='submit' and " +
191 //        "translate(@type,'REST','rest')!='reset'"
192         ) +  "] |" +
193         "//select | //xhtml:select | " +
194         "//textarea | //xhtml:textarea | " +
195         "//textbox | //xul:textbox",
196     "XPath expression matching elements to be selected by `browser-focus-next-form-field' " +
197         "and `browser-focus-previous-form-field.'");
199 function browser_focus_next_form_field(buffer, count, xpath_expr) {
200     var focused_elem = buffer.focused_element;
201     if (count == 0)
202         return; // invalid count
204     function helper(win, skip_win) {
205         if (win == skip_win)
206             return null;
207         var doc = win.document;
208         var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
209                                Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
210                                null /* existing results */);
211         var length = res.snapshotLength;
212         if (length > 0) {
213             var index = null;
214             if (focused_elem != null) {
215                 for (var i = 0; i < length; ++i) {
216                     if (res.snapshotItem(i) == focused_elem) {
217                         index = i;
218                         break;
219                     }
220                 }
221             }
222             if (index == null) {
223                 if (count > 0)
224                     index = count - 1;
225                 else
226                     index = -count;
227             }
228             else
229                 index = index + count;
230             index = index % length;
231             if (index < 0)
232                 index += length;
234             return res.snapshotItem(index);
235         }
237         // Recurse on sub-frames
238         for (var i = 0; i < win.frames.length; ++i) {
239             var elem = helper(win.frames[i], skip_win);
240             if (elem)
241                 return elem;
242         }
243         return null;
244     }
246     var focused_win = buffer.focused_frame;
247     var elem = helper(focused_win, null);
248     if (!elem)
249         elem = helper(buffer.top_frame, focused_win);
250     if (elem) {
251         browser_element_focus(buffer, elem);
252 v    } else
253         throw interactive_error("No form field found");
256 interactive("browser-focus-next-form-field", "Focus the next element matching `browser_form_field_xpath_expression'.",
257             function (I) {
258                 browser_focus_next_form_field(I.buffer, I.p, browser_form_field_xpath_expression);
259             });
261 interactive("browser-focus-previous-form-field",  "Focus the previous element matching `browser_form_field_xpath_expression'.",
262             function (I) {
263                 browser_focus_next_form_field(I.buffer, -I.p, browser_form_field_xpath_expression);
266 function edit_field_in_external_editor(buffer, elem) {
267     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
268         var type = elem.getAttribute("type");
269         if (type != null)
270             type = type.toLowerCase();
271         if (type == "hidden" || type == "checkbox" || type == "radio")
272             throw interactive_error("Element is not a text field.");
273     } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
274         throw interactive_error("Element is not a text field.");
276     var name = elem.getAttribute("name");
277     if (!name || name.length == 0)
278         name = elem.getAttribute("id");
279     if (!name)
280         name = "";
281     name = name.replace(/[^a-zA-Z0-9\-_]/g, "");
282     if (name.length == 0)
283         name = "text";
284     name += ".txt";
285     var file = get_temporary_file(name);
287     // Write to file
288     try {
289         write_text_file(file, elem.value);
290     } catch (e) {
291         file.remove(false);
292         throw e;
293     }
295     var old_readonly = elem.getAttribute("readonly");
296     elem.setAttribute("readonly", "true");
298     // FIXME: decide if we should do this
299     var old_class = elem.className;
300     elem.className = "__conkeror_textbox_edited_externally " + old_class;
302     try {
303         yield open_file_with_external_editor(file);
305         elem.value = read_text_file(file);
306     } finally {
307         elem.className = old_class;
309         if (old_readonly)
310             elem.setAttribute("readonly", old_readonly);
311         else
312             elem.removeAttribute("readonly");
313         file.remove(false);
314     }
316 interactive("edit-current-field-in-external-editor", "Edit the contents of the currently-focused text field in an external editor.",
317             function (I) {
318     var buf = I.buffer;
319     yield edit_field_in_external_editor(buf, buf.focused_element);
320     unfocus(buf);
323 define_user_variable("kill_whole_line", false, "If true, `kill-line' with no arg at beg of line kills the whole line.");
325 function cut_to_end_of_line (buffer) {
326     var elem = buffer.focused_element;
327     var st = elem.selectionStart;
328     var en = elem.selectionEnd;
329     if (st == en) {
330         // there is no selection.  set one up.
331         var eol = elem.value.indexOf ("\n", en);
332         if (eol == -1)
333         {
334             elem.selectionEnd = elem.textLength;
335         } else if (eol == st) {
336             elem.selectionEnd = eol + 1;
337         } else if (kill_whole_line &&
338                    (st == 0 || elem.value[st - 1] == "\n"))
339         {
340             elem.selectionEnd = eol + 1;
341         } else {
342             elem.selectionEnd = eol;
343         }
344     }
345     buffer.do_command ('cmd_cut');
348 interactive (
349     "cut-to-end-of-line",
350     function (I) {
351         cut_to_end_of_line (I.buffer);
352     });