Revert "current_buffer_input_mode_change_hook: removed"
[conkeror.git] / modules / content-buffer-input.js
blobbdccdf11b28fad9f3125819f0c9ab0100ef42ce6
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); });
39 // Input mode for "normal" view mode
40 define_input_mode("normal", null, "content_buffer_normal_keymap");
42 // Input modes for form elements
43 define_input_mode("select", "input:SELECT", "content_buffer_select_keymap");
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");
47 define_input_mode("checkbox", "input:CHECKBOX/RADIOBUTTON", "content_buffer_checkbox_keymap");
49 // Input modes for sending key events to gecko
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 // Input mode for the visible caret
62 define_input_mode("caret", null, "content_buffer_caret_keymap");
65 function content_buffer_update_input_mode_for_focus(buffer, force) {
66     var mode = buffer.input_mode;
67     var form_input_mode_enabled = (mode == "text_input_mode" ||
68                                    mode == "textarea_input_mode" ||
69                                    mode == "select_input_mode" ||
70                                    mode == "checkbox_input_mode" ||
71                                    mode == "richedit_input_mode");
73     if (force || form_input_mode_enabled || mode == "normal_input_mode") {
74         let elem = buffer.focused_element;
76         if (elem) {
77             var input_mode_function = null;
78             if (elem instanceof Ci.nsIDOMHTMLInputElement) {
79                 var type = elem.getAttribute("type");
80                 if (type != null) type = type.toLowerCase();
81                 if (type == "checkbox" || type == "radio")
82                     input_mode_function = checkbox_input_mode;
83                 else if (type != "submit" &&
84                          type != "reset")
85                     input_mode_function = text_input_mode;
86             }
87             else if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
88                 input_mode_function = textarea_input_mode;
90             else if (elem instanceof Ci.nsIDOMHTMLSelectElement)
91                 input_mode_function = select_input_mode;
92         }
94         if (!input_mode_function) {
96             let frame = buffer.focused_frame;
97             let in_design_mode = false;
98             if (frame && frame.document.designMode == "on")
99                 in_design_mode = true;
100             else {
101                 outer:
102                 while (elem) {
103                     switch (elem.contentEditable) {
104                     case "true":
105                         in_design_mode = true;
106                         break outer;
107                     case "false":
108                         break outer;
109                     default: // == "inherit"
110                         elem = elem.parentNode;
111                     }
112                 }
113             }
114             if (in_design_mode)
115                 input_mode_function = richedit_input_mode;
116         }
118         if (input_mode_function) {
119             if (!force &&
120                 browser_prevent_automatic_form_focus_mode_enabled &&
121                 !form_input_mode_enabled &&
122                 (buffer.last_user_input_received == null ||
123                  (Date.now() - buffer.last_user_input_received)
124                  > browser_automatic_form_focus_window_duration)) {
125                 // Automatic focus attempt blocked
126                 elem.blur();
127             } else
128                 input_mode_function(buffer, true);
129             return;
130         }
132         normal_input_mode(buffer, true);
133     }
136 add_hook("content_buffer_focus_change_hook",
137          function (buf) {
138              content_buffer_update_input_mode_for_focus(buf, false);
139          });
141 define_buffer_mode('caret_mode', 'CARET',
142                    $enable = function(buffer) {
143                        buffer.browser.setAttribute('showcaret', 'true');
144                        let sc = getFocusedSelCtrl(buffer);
145                        sc.setCaretEnabled(true);
146                        buffer.top_frame.focus();
147                        caret_input_mode(buffer, true);
148                    },
149                    $disable = function(buffer) {
150                        buffer.browser.setAttribute('showcaret', '');
151                        let sc = getFocusedSelCtrl(buffer);
152                        sc.setCaretEnabled(false);
153                        buffer.browser.focus();
154                        content_buffer_update_input_mode_for_focus(buffer, true);
155                    });
157 //XXX: CARET_PREF is defined in find.js---why?
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,
197                 "Time window (in milliseconds) during which a form element "+
198                 "is allowed to gain focus following a mouse click or key "+
199                 "press, if `browser_prevent_automatic_form_focus_mode' is "+
200                 "enabled.");;
202 define_global_mode("browser_prevent_automatic_form_focus_mode",
203                    function () {}, // enable
204                    function () {} // disable
205                   );
207 // note: The apparent misspellings here are not a bug.
208 // see https://developer.mozilla.org/en/XPath/Functions/translate
210 define_variable(
211     "browser_form_field_xpath_expression",
212     "//input[" + (
213         //        "translate(@type,'RADIO','radio')!='radio' and " +
214         //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
215         "translate(@type,'HIDEN','hiden')!='hidden'"
216         //        "translate(@type,'SUBMIT','submit')!='submit' and " +
217         //        "translate(@type,'REST','rest')!='reset'"
218     ) +  "] | " +
219         "//xhtml:input[" + (
220             //        "translate(@type,'RADIO','radio')!='radio' and " +
221             //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
222             "translate(@type,'HIDEN','hiden')!='hidden'"
223             //        "translate(@type,'SUBMIT','submit')!='submit' and " +
224             //        "translate(@type,'REST','rest')!='reset'"
225         ) +  "] |" +
226         "//select | //xhtml:select | " +
227         "//textarea | //xhtml:textarea | " +
228         "//textbox | //xul:textbox",
229     "XPath expression matching elements to be selected by `browser-focus-next-form-field' " +
230         "and `browser-focus-previous-form-field.'");
232 function browser_focus_next_form_field(buffer, count, xpath_expr) {
233     var focused_elem = buffer.focused_element;
234     if (count == 0)
235         return; // invalid count
237     function helper(win, skip_win) {
238         if (win == skip_win)
239             return null;
240         var doc = win.document;
241         var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
242             Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
243             null /* existing results */);
244         var length = res.snapshotLength;
245         if (length > 0) {
246             let valid_nodes = [];
247             for (let i = 0; i < length; ++i) {
248                 let elem = res.snapshotItem(i);
249                 if (elem.clientWidth == 0 &&
250                     elem.clientHeight == 0)
251                     continue;
252                 let style = win.getComputedStyle(elem, "");
253                 if (style.display == "none" || style.visibility == "hidden")
254                     continue;
255                 valid_nodes.push(elem);
256             }
258             if (valid_nodes.length > 0) {
259                 var index = -1;
260                 if (focused_elem != null)
261                     index = valid_nodes.indexOf(focused_elem);
262                 if (index == -1) {
263                     if (count > 0)
264                         index = count - 1;
265                     else
266                         index = -count;
267                 }
268                 else
269                     index = index + count;
270                 index = index % valid_nodes.length;
271                 if (index < 0)
272                     index += valid_nodes.length;
274                 return valid_nodes[index];
275             }
276         }
277         // Recurse on sub-frames
278         for (var i = 0; i < win.frames.length; ++i) {
279             var elem = helper(win.frames[i], skip_win);
280             if (elem)
281                 return elem;
282         }
283         return null;
284     }
286     var focused_win = buffer.focused_frame;
287     var elem = helper(focused_win, null);
288     if (!elem)
289         elem = helper(buffer.top_frame, focused_win);
290     if (elem) {
291         browser_element_focus(buffer, elem);
292     } else
293         throw interactive_error("No form field found");
296 interactive("browser-focus-next-form-field",
297             "Focus the next element matching "+
298             "`browser_form_field_xpath_expression'.",
299             function (I) {
300                 browser_focus_next_form_field(
301                     I.buffer, I.p, browser_form_field_xpath_expression);
302             });
304 interactive("browser-focus-previous-form-field",
305             "Focus the previous element matching "+
306             "`browser_form_field_xpath_expression'.",
307             function (I) {
308                 browser_focus_next_form_field(
309                     I.buffer, -I.p, browser_form_field_xpath_expression);
310             });
313 define_variable('edit_field_in_external_editor_extension', "txt",
314     "File extension for the temp files created by "+
315     "edit-current-field-in-external-editor.");
317 function get_filename_for_current_textfield(doc, elem) {
318     var name = doc.URL
319         + "-"
320         + ( elem.getAttribute("name")
321             || elem.getAttribute("id")
322             || "textarea" );
324     // get rid filesystem unfriendly chars
325     name = name.replace(doc.location.protocol, "")
326         .replace(/[^a-zA-Z0-9]+/g, "-")
327         .replace(/(^-+|-+$)/g, "")
328         + '.' + edit_field_in_external_editor_extension;
330     return name;
333 function edit_field_in_external_editor(buffer, elem) {
334     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
335         var type = elem.getAttribute("type");
336         if (type != null)
337             type = type.toLowerCase();
338         if (type == "hidden" || type == "checkbox" || type == "radio")
339             throw interactive_error("Element is not a text field.");
340     } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
341         throw interactive_error("Element is not a text field.");
343     var name = get_filename_for_current_textfield(buffer.document, elem);
344     var file = get_temporary_file(name);
346     // Write to file
347     try {
348         write_text_file(file, elem.value);
349     } catch (e) {
350         file.remove(false);
351         throw e;
352     }
354     // FIXME: decide if we should do this
355     var old_class = elem.className;
356     elem.className = "__conkeror_textbox_edited_externally " + old_class;
358     try {
359         yield open_file_with_external_editor(file);
361         elem.value = read_text_file(file);
362     } finally {
363         elem.className = old_class;
365         file.remove(false);
366     }
369 interactive("edit-current-field-in-external-editor",
370             "Edit the contents of the currently-focused text field in an external editor.",
371             function (I) {
372                 var buf = I.buffer;
373                 yield edit_field_in_external_editor(buf, buf.focused_element);
374                 unfocus(I.window, buf);
375             });
377 define_variable("kill_whole_line", false,
378                 "If true, `kill-line' with no arg at beg of line kills the whole line.");
380 function cut_to_end_of_line (buffer) {
381     var elem = buffer.focused_element;
382     try {
383         var st = elem.selectionStart;
384         var en = elem.selectionEnd;
385         if (st == en) {
386             // there is no selection.  set one up.
387             var eol = elem.value.indexOf ("\n", en);
388             if (eol == -1)
389             {
390                 elem.selectionEnd = elem.textLength;
391             } else if (eol == st) {
392                 elem.selectionEnd = eol + 1;
393             } else if (kill_whole_line &&
394                        (st == 0 || elem.value[st - 1] == "\n"))
395             {
396                 elem.selectionEnd = eol + 1;
397             } else {
398                 elem.selectionEnd = eol;
399             }
400         }
401         buffer.do_command ('cmd_cut');
402     } catch (e) {
403         /* FIXME: Make this work for richedit mode as well */
404     }
407 interactive (
408     "cut-to-end-of-line",
409     null,
410     function (I) {
411         cut_to_end_of_line (I.buffer);
412     });