youtube.js: allow _ in video id
[conkeror.git] / modules / content-buffer-input.js
blob5bfdd112c3dd20a8d89bf8847417f7ef98f49d8e
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) { check_buffer(buffer, content_buffer);
22                                                      content_buffer_update_keymap_for_input_mode(buffer); },
23                        $disable = false,
24                        $doc = doc);
26 ignore_function_for_get_caller_source_code_reference("define_input_mode");
28 function content_buffer_update_keymap_for_input_mode(buffer) {
29     if (buffer.input_mode)
30         buffer.keymap = buffer.get(content_buffer_input_mode_keymaps[buffer.input_mode]);
33 add_hook("page_mode_change_hook", content_buffer_update_keymap_for_input_mode);
35 add_hook("content_buffer_location_change_hook", function (buf) { normal_input_mode(buf, true); });
37 define_input_mode("normal", null, "content_buffer_normal_keymap");
39 // For SELECT elements
40 define_input_mode("select", "input:SELECT", "content_buffer_select_keymap");
42 // For text INPUT and TEXTAREA elements
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");
47 define_input_mode("checkbox", "input:CHECKBOX/RADIOBUTTON", "content_buffer_checkbox_keymap");
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 function content_buffer_update_input_mode_for_focus(buffer, force) {
61     var mode = buffer.input_mode;
62     var form_input_mode_enabled = (mode == "text_input_mode" ||
63                                    mode == "textarea_input_mode" ||
64                                    mode == "select_input_mode" ||
65                                    mode == "checkbox_input_mode" ||
66                                    mode == "richedit_input_mode");
68     if (force || form_input_mode_enabled || mode == "normal_input_mode") {
69         let elem = buffer.focused_element;
71         if (elem) {
72             var input_mode_function = null;
73             if (elem instanceof Ci.nsIDOMHTMLInputElement) {
74                 var type = elem.getAttribute("type");
75                 if (type != null) type = type.toLowerCase();
76                 if (type == "checkbox" || type == "radio")
77                     input_mode_function = checkbox_input_mode;
78                 else if (type != "submit" &&
79                          type != "reset")
80                     input_mode_function = text_input_mode;
81             }
82             else if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
83                 input_mode_function = textarea_input_mode;
85             else if (elem instanceof Ci.nsIDOMHTMLSelectElement)
86                 input_mode_function = select_input_mode;
87         }
89         if (!input_mode_function) {
91             let frame = buffer.focused_frame;
92             let in_design_mode = false;
93             if (frame && frame.document.designMode == "on")
94                 in_design_mode = true;
95             else {
96                 outer:
97                 while (elem) {
98                     switch (elem.contentEditable) {
99                     case "true":
100                         in_design_mode = true;
101                         break outer;
102                     case "false":
103                         break outer;
104                     default: // == "inherit"
105                         elem = elem.parentNode;
106                     }
107                 }
108             }
109             if (in_design_mode)
110                 input_mode_function = richedit_input_mode;
111         }
113         if (input_mode_function) {
114             if (!force &&
115                 browser_prevent_automatic_form_focus_mode_enabled &&
116                 !form_input_mode_enabled &&
117                 (buffer.last_user_input_received == null ||
118                  (Date.now() - buffer.last_user_input_received)
119                  > browser_automatic_form_focus_window_duration)) {
120                 // Automatic focus attempt blocked
121                 elem.blur();
122             } else
123                 input_mode_function(buffer, true);
124             return;
125         }
127         normal_input_mode(buffer, true);
128     }
131 add_hook("content_buffer_focus_change_hook", function (buf) { content_buffer_update_input_mode_for_focus(buf, false); } );
133 interactive("content-buffer-update-input-mode-for-focus", function (I) {
134     content_buffer_update_input_mode_for_focus(I.buffer, true);
137 function minibuffer_input_mode_indicator(window) {
138     this.window = window;
139     this.hook_func = method_caller(this, this.update);
140     add_hook.call(window, "select_buffer_hook", this.hook_func);
141     add_hook.call(window, "current_buffer_input_mode_change_hook", this.hook_func);
142     this.update();
145 minibuffer_input_mode_indicator.prototype = {
146     update : function () {
147         var buf = this.window.buffers.current;
148         var mode = buf.input_mode;
149         if (mode)
150             this.window.minibuffer.element.className = "minibuffer-" + buf.input_mode.replace("_","-","g");
151     },
152     uninstall : function () {
153         remove_hook.call(window, "select_buffer_hook", this.hook_func);
154         remove_hook.call(window, "current_buffer_input_mode_change_hook", this.hook_func);
155     }
158 define_global_window_mode("minibuffer_input_mode_indicator", "window_initialize_hook");
159 minibuffer_input_mode_indicator_mode(true);
161 // Milliseconds
162 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.");;
164 define_global_mode("browser_prevent_automatic_form_focus_mode",
165                    function () {}, // enable
166                    function () {} // disable
167                   );
168 browser_prevent_automatic_form_focus_mode(true);
170 define_variable(
171     "browser_form_field_xpath_expression",
172     "//input[" + (
173         //        "translate(@type,'RADIO','radio')!='radio' and " +
174         //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
175         "translate(@type,'HIDEN','hiden')!='hidden'"
176         //        "translate(@type,'SUBMIT','submit')!='submit' and " +
177         //        "translate(@type,'REST','rest')!='reset'"
178     ) +  "] | " +
179         "//xhtml: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         "//select | //xhtml:select | " +
187         "//textarea | //xhtml:textarea | " +
188         "//textbox | //xul:textbox",
189     "XPath expression matching elements to be selected by `browser-focus-next-form-field' " +
190         "and `browser-focus-previous-form-field.'");
192 function browser_focus_next_form_field(buffer, count, xpath_expr) {
193     var focused_elem = buffer.focused_element;
194     if (count == 0)
195         return; // invalid count
197     function helper(win, skip_win) {
198         if (win == skip_win)
199             return null;
200         var doc = win.document;
201         var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
202             Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
203             null /* existing results */);
204         var length = res.snapshotLength;
205         if (length > 0) {
206             var index = null;
207             if (focused_elem != null) {
208                 for (var i = 0; i < length; ++i) {
209                     if (res.snapshotItem(i) == focused_elem) {
210                         index = i;
211                         break;
212                     }
213                 }
214             }
215             if (index == null) {
216                 if (count > 0)
217                     index = count - 1;
218                 else
219                     index = -count;
220             }
221             else
222                 index = index + count;
223             index = index % length;
224             if (index < 0)
225                 index += length;
227             return res.snapshotItem(index);
228         }
230         // Recurse on sub-frames
231         for (var i = 0; i < win.frames.length; ++i) {
232             var elem = helper(win.frames[i], skip_win);
233             if (elem)
234                 return elem;
235         }
236         return null;
237     }
239     var focused_win = buffer.focused_frame;
240     var elem = helper(focused_win, null);
241     if (!elem)
242         elem = helper(buffer.top_frame, focused_win);
243     if (elem) {
244         browser_element_focus(buffer, elem);
245     } else
246         throw interactive_error("No form field found");
249 interactive("browser-focus-next-form-field", "Focus the next element matching `browser_form_field_xpath_expression'.",
250             function (I) {
251                 browser_focus_next_form_field(I.buffer, I.p, browser_form_field_xpath_expression);
252             });
254 interactive("browser-focus-previous-form-field",  "Focus the previous element matching `browser_form_field_xpath_expression'.",
255             function (I) {
256                 browser_focus_next_form_field(I.buffer, -I.p, browser_form_field_xpath_expression);
257             });
259 function edit_field_in_external_editor(buffer, elem) {
260     if (elem instanceof Ci.nsIDOMHTMLInputElement) {
261         var type = elem.getAttribute("type");
262         if (type != null)
263             type = type.toLowerCase();
264         if (type == "hidden" || type == "checkbox" || type == "radio")
265             throw interactive_error("Element is not a text field.");
266     } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
267         throw interactive_error("Element is not a text field.");
269     var name = elem.getAttribute("name");
270     if (!name || name.length == 0)
271         name = elem.getAttribute("id");
272     if (!name)
273         name = "";
274     name = name.replace(/[^a-zA-Z0-9\-_]/g, "");
275     if (name.length == 0)
276         name = "text";
277     name += ".txt";
278     var file = get_temporary_file(name);
280     // Write to file
281     try {
282         write_text_file(file, elem.value);
283     } catch (e) {
284         file.remove(false);
285         throw e;
286     }
288     // FIXME: decide if we should do this
289     var old_class = elem.className;
290     elem.className = "__conkeror_textbox_edited_externally " + old_class;
292     try {
293         yield open_file_with_external_editor(file);
295         elem.value = read_text_file(file);
296     } finally {
297         elem.className = old_class;
299         file.remove(false);
300     }
302 interactive("edit-current-field-in-external-editor", "Edit the contents of the currently-focused text field in an external editor.",
303             function (I) {
304                 var buf = I.buffer;
305                 yield edit_field_in_external_editor(buf, buf.focused_element);
306                 unfocus(buf);
307             });
309 define_variable("kill_whole_line", false, "If true, `kill-line' with no arg at beg of line kills the whole line.");
311 function cut_to_end_of_line (buffer) {
312     var elem = buffer.focused_element;
313     try {
314         var st = elem.selectionStart;
315         var en = elem.selectionEnd;
316         if (st == en) {
317             // there is no selection.  set one up.
318             var eol = elem.value.indexOf ("\n", en);
319             if (eol == -1)
320             {
321                 elem.selectionEnd = elem.textLength;
322             } else if (eol == st) {
323                 elem.selectionEnd = eol + 1;
324             } else if (kill_whole_line &&
325                        (st == 0 || elem.value[st - 1] == "\n"))
326             {
327                 elem.selectionEnd = eol + 1;
328             } else {
329                 elem.selectionEnd = eol;
330             }
331         }
332         buffer.do_command ('cmd_cut');
333     } catch (e) {
334         /* FIXME: Make this work for richedit mode as well */
335     }
338 interactive (
339     "cut-to-end-of-line",
340     function (I) {
341         cut_to_end_of_line (I.buffer);
342     });