view-as-mime-type: fix bug in interactive declaration
[conkeror.git] / modules / content-buffer-input.js
blob5a5a70eee2e99e27de11136195b8ed5bf1090266
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 browser_focus_next_form_field(buffer, count, xpath_expr) {
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.clientWidth == 0 &&
249                     elem.clientHeight == 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         elem = helper(buffer.top_frame, focused_win);
289     if (elem) {
290         browser_element_focus(buffer, elem);
291     } else
292         throw interactive_error("No form field found");
295 interactive("browser-focus-next-form-field",
296             "Focus the next element matching "+
297             "`browser_form_field_xpath_expression'.",
298             function (I) {
299                 browser_focus_next_form_field(
300                     I.buffer, I.p, browser_form_field_xpath_expression);
301             });
303 interactive("browser-focus-previous-form-field",
304             "Focus the previous element matching "+
305             "`browser_form_field_xpath_expression'.",
306             function (I) {
307                 browser_focus_next_form_field(
308                     I.buffer, -I.p, browser_form_field_xpath_expression);
309             });
312 define_variable('edit_field_in_external_editor_extension', "txt",
313     "File extension for the temp files created by "+
314     "edit-current-field-in-external-editor.");
316 function get_filename_for_current_textfield(doc, elem) {
317     var name = doc.URL
318         + "-"
319         + ( elem.getAttribute("name")
320             || elem.getAttribute("id")
321             || "textarea" );
323     // get rid filesystem unfriendly chars
324     name = name.replace(doc.location.protocol, "")
325         .replace(/[^a-zA-Z0-9]+/g, "-")
326         .replace(/(^-+|-+$)/g, "")
327         + '.' + edit_field_in_external_editor_extension;
329     return name;
332 function edit_field_in_external_editor(buffer, elem) {
333     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
334         var type = elem.getAttribute("type");
335         if (type != null)
336             type = type.toLowerCase();
337         if (type == "hidden" || type == "checkbox" || type == "radio")
338             throw interactive_error("Element is not a text field.");
339     } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
340         throw interactive_error("Element is not a text field.");
342     var name = get_filename_for_current_textfield(buffer.document, elem);
343     var file = get_temporary_file(name);
345     // Write to file
346     try {
347         write_text_file(file, elem.value);
348     } catch (e) {
349         file.remove(false);
350         throw e;
351     }
353     // FIXME: decide if we should do this
354     var old_class = elem.className;
355     elem.className = "__conkeror_textbox_edited_externally " + old_class;
357     try {
358         yield open_file_with_external_editor(file);
360         elem.value = read_text_file(file);
361     } finally {
362         elem.className = old_class;
364         file.remove(false);
365     }
368 interactive("edit-current-field-in-external-editor",
369             "Edit the contents of the currently-focused text field in an external editor.",
370             function (I) {
371                 var buf = I.buffer;
372                 yield edit_field_in_external_editor(buf, buf.focused_element);
373                 unfocus(I.window, buf);
374             });
376 define_variable("kill_whole_line", false,
377                 "If true, `kill-line' with no arg at beg of line kills the whole line.");
379 function cut_to_end_of_line (buffer) {
380     var elem = buffer.focused_element;
381     try {
382         var st = elem.selectionStart;
383         var en = elem.selectionEnd;
384         if (st == en) {
385             // there is no selection.  set one up.
386             var eol = elem.value.indexOf ("\n", en);
387             if (eol == -1)
388             {
389                 elem.selectionEnd = elem.textLength;
390             } else if (eol == st) {
391                 elem.selectionEnd = eol + 1;
392             } else if (kill_whole_line &&
393                        (st == 0 || elem.value[st - 1] == "\n"))
394             {
395                 elem.selectionEnd = eol + 1;
396             } else {
397                 elem.selectionEnd = eol;
398             }
399         }
400         buffer.do_command ('cmd_cut');
401     } catch (e) {
402         /* FIXME: Make this work for richedit mode as well */
403     }
405 interactive("cut-to-end-of-line",
406     null,
407     function (I) {
408         cut_to_end_of_line (I.buffer);
409     });
412 function downcase_word(I) {
413     modify_word_at_point(I, function (word) { return word.toLocaleLowerCase(); });
415 interactive("downcase-word",
416             "Convert following word to lower case, moving over.",
417             downcase_word);
420 function upcase_word(I) {
421     modify_word_at_point(I, function (word) { return word.toLocaleUpperCase(); });
423 interactive("upcase-word",
424             "Convert following word to upper case, moving over.",
425             upcase_word);
428 function capitalize_word(I) {
429     modify_word_at_point(I, function (word) {
430         if (word.length > 0) {
431             return word[0].toLocaleUpperCase() + word.substring(1);
432         }
433         return word;
434     });
436 interactive("capitalize-word",
437             "Capitalize the following word (or arg words), moving over.",
438             capitalize_word);