2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3 * (C) Copyright 2008-2009 John J. Foerch
5 * Use, modification, and distribution are subject to the terms specified in the
10 * This file contains the default input-mode system. An input-mode
11 * system is a set of input-modes along with code that coordinates
12 * switching among them based on user actions and focus-context.
15 require("content-buffer.js");
18 * DEFAULT INPUT-MODE SYSTEM
21 // Input mode for "normal" view mode. You might also call it "scroll" mode.
22 define_input_mode("normal", null, "content_buffer_normal_keymap");
24 // Input modes for form elements
25 define_input_mode("select", "input:SELECT", "content_buffer_select_keymap");
26 define_input_mode("text", "input:TEXT", "content_buffer_text_keymap");
27 define_input_mode("textarea", "input:TEXTAREA", "content_buffer_textarea_keymap");
28 define_input_mode("richedit", "input:RICHEDIT", "content_buffer_richedit_keymap");
29 define_input_mode("checkbox", "input:CHECKBOX/RADIOBUTTON", "content_buffer_checkbox_keymap");
33 * content_buffer_update_input_mode_for_focus is the controller for
34 * this input-mode system. It sets the input-mode based on the focused
35 * element or lack thereof in the given buffer. By default, it will
36 * not override input-modes such as caret-mode, which have been turned
37 * on explicitly by the user. If force is true, then this function
38 * will even override those special input-modes.
40 function content_buffer_update_input_mode_for_focus (buffer, force) {
41 var mode = buffer.input_mode;
42 var form_input_mode_enabled = (mode == "text_input_mode" ||
43 mode == "textarea_input_mode" ||
44 mode == "select_input_mode" ||
45 mode == "checkbox_input_mode" ||
46 mode == "richedit_input_mode");
48 if (force || form_input_mode_enabled ||
49 mode == "normal_input_mode" || mode == null)
51 let elem = buffer.focused_element;
54 var input_mode_function = null;
55 if (elem instanceof Ci.nsIDOMHTMLInputElement) {
56 var type = elem.getAttribute("type");
57 if (type != null) type = type.toLowerCase();
58 if (type == "checkbox" || type == "radio")
59 input_mode_function = checkbox_input_mode;
60 else if (type != "submit" &&
62 input_mode_function = text_input_mode;
64 else if (elem instanceof Ci.nsIDOMHTMLTextAreaElement)
65 input_mode_function = textarea_input_mode;
67 else if (elem instanceof Ci.nsIDOMHTMLSelectElement)
68 input_mode_function = select_input_mode;
71 if (!input_mode_function) {
72 let frame = buffer.focused_frame;
73 let in_design_mode = false;
74 if (frame && frame.document.designMode == "on")
75 in_design_mode = true;
79 switch (elem.contentEditable) {
81 in_design_mode = true;
85 default: // == "inherit"
86 elem = elem.parentNode;
91 input_mode_function = richedit_input_mode;
94 if (input_mode_function) {
96 browser_prevent_automatic_form_focus_mode_enabled && //XXX: breaks abstraction
97 !form_input_mode_enabled &&
98 (buffer.last_user_input_received == null ||
99 (Date.now() - buffer.last_user_input_received)
100 > browser_automatic_form_focus_window_duration)) {
101 // Automatic focus attempt blocked
104 input_mode_function(buffer, true);
107 normal_input_mode(buffer, true);
111 //XXX: why does this command exist? if the input-mode system works correctly
112 // then the user should never have to call this.
113 interactive("content-buffer-update-input-mode-for-focus",
114 "This command provides a way for the user to force a reset of the"+
115 "input-mode system.",
117 content_buffer_update_input_mode_for_focus(I.buffer, true);
122 * Install the default input-mode system
124 add_hook("content_buffer_location_change_hook",
125 function (buf) { // ignore other args
126 content_buffer_update_input_mode_for_focus(buf);
129 add_hook("content_buffer_focus_change_hook",
130 function (buf) { // ignore other args
131 content_buffer_update_input_mode_for_focus(buf);
139 // Input modes for sending key events to gecko
141 // XXX: These quote modes are not rightly part of the system because
142 // they are always invoked by the user. Furthermore, they should be
143 // generalized to work with any type of buffer.
145 "quote_next", "input:PASS-THROUGH(next)", "content_buffer_quote_next_keymap",
146 "This input mode sends the next key combo to the buffer, "+
147 "bypassing Conkeror's normal key handling. The mode disengages "+
148 "after one key combo.");
150 "quote", "input:PASS-THROUGH", "content_buffer_quote_keymap",
151 "This input mode sends all key combos to the buffer, "+
152 "bypassing Conkeror's normal key handling, until the "+
153 "Escape key is pressed.");
155 define_key_match_predicate('match_not_escape_key', 'any key but escape',
157 return event.keyCode != 27 ||
169 // Input mode for the visible caret
171 // XXX: caret-mode should be defined elsewhere, as it is not part of
172 // the input-mode system.
173 define_input_mode("caret", null, "content_buffer_caret_keymap");
175 define_buffer_mode('caret_mode', 'CARET',
176 $enable = function(buffer) {
177 buffer.browser.setAttribute('showcaret', 'true');
178 let sc = getFocusedSelCtrl(buffer);
179 sc.setCaretEnabled(true);
180 buffer.top_frame.focus();
181 caret_input_mode(buffer, true);
183 $disable = function(buffer) {
184 buffer.browser.setAttribute('showcaret', '');
185 let sc = getFocusedSelCtrl(buffer);
186 sc.setCaretEnabled(false);
187 buffer.browser.focus();
188 //XXX: dependency on the default input-mode system
189 content_buffer_update_input_mode_for_focus(buffer, true);
193 const CARET_PREF = 'accessibility.browsewithcaret';
195 watch_pref(CARET_PREF, function() {
196 if (get_pref(CARET_PREF)) {
197 session_pref(CARET_PREF, false);
198 let window = window_watcher.activeWindow;
199 let buffer = window.buffers.current;
207 * Browser Prevent Automatic Form Focus Mode
211 define_variable("browser_automatic_form_focus_window_duration", 20,
212 "Time window (in milliseconds) during which a form element "+
213 "is allowed to gain focus following a mouse click or key "+
214 "press, if `browser_prevent_automatic_form_focus_mode' is "+
217 define_global_mode("browser_prevent_automatic_form_focus_mode",
218 function () {}, // enable
219 function () {} // disable
222 // note: The apparent misspellings here are not a bug.
223 // see https://developer.mozilla.org/en/XPath/Functions/translate
226 "browser_form_field_xpath_expression",
228 // "translate(@type,'RADIO','radio')!='radio' and " +
229 // "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
230 "translate(@type,'HIDEN','hiden')!='hidden'"
231 // "translate(@type,'SUBMIT','submit')!='submit' and " +
232 // "translate(@type,'REST','rest')!='reset'"
235 // "translate(@type,'RADIO','radio')!='radio' and " +
236 // "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
237 "translate(@type,'HIDEN','hiden')!='hidden'"
238 // "translate(@type,'SUBMIT','submit')!='submit' and " +
239 // "translate(@type,'REST','rest')!='reset'"
241 "//select | //xhtml:select | " +
242 "//textarea | //xhtml:textarea | " +
243 "//textbox | //xul:textbox",
244 "XPath expression matching elements to be selected by `browser-focus-next-form-field' " +
245 "and `browser-focus-previous-form-field.'");
247 function focus_next (buffer, count, xpath_expr, name) {
248 var focused_elem = buffer.focused_element;
250 return; // invalid count
252 function helper (win, skip_win) {
255 var doc = win.document;
256 var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
257 Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
258 null /* existing results */);
259 var length = res.snapshotLength;
261 let valid_nodes = [];
262 for (let i = 0; i < length; ++i) {
263 let elem = res.snapshotItem(i);
264 if (elem.offsetWidth == 0 ||
265 elem.offsetHeight == 0)
267 let style = win.getComputedStyle(elem, "");
268 if (style.display == "none" || style.visibility == "hidden")
270 valid_nodes.push(elem);
273 if (valid_nodes.length > 0) {
275 if (focused_elem != null)
276 index = valid_nodes.indexOf(focused_elem);
284 index = index + count;
285 index = index % valid_nodes.length;
287 index += valid_nodes.length;
289 return valid_nodes[index];
292 // Recurse on sub-frames
293 for (var i = 0; i < win.frames.length; ++i) {
294 var elem = helper(win.frames[i], skip_win);
301 var focused_win = buffer.focused_frame;
302 var elem = helper(focused_win, null);
304 // if focused_frame is top_frame, we're doing twice as much
306 elem = helper(buffer.top_frame, focused_win);
308 browser_element_focus(buffer, elem);
310 throw interactive_error("No "+name+" found");
313 interactive("browser-focus-next-form-field",
314 "Focus the next element matching "+
315 "`browser_form_field_xpath_expression'.",
317 focus_next(I.buffer, I.p,
318 browser_form_field_xpath_expression,
322 interactive("browser-focus-previous-form-field",
323 "Focus the previous element matching "+
324 "`browser_form_field_xpath_expression'.",
326 focus_next(I.buffer, -I.p,
327 browser_form_field_xpath_expression,
332 define_variable("links_xpath_expression",
333 "//*[@onclick or @onmouseover or @onmousedown or "+
334 "@onmouseup or @oncommand or @role='link'] | " +
335 "//input[not(@type='hidden')] | //a | //area | "+
336 "//iframe | //textarea | //button | //select",
337 "XPath expression matching elements to be selected by "+
338 "`focus-next-link' and `focus-previous-link.'");
340 interactive("focus-next-link",
341 "Focus the next element matching `links_xpath_expression'.",
343 focus_next(I.buffer, I.p,
344 links_xpath_expression,
348 interactive("focus-previous-link",
349 "Focus the previous element matching `links_xpath_expression'.",
351 focus_next(I.buffer, -I.p,
352 links_xpath_expression,
357 define_variable('edit_field_in_external_editor_extension', "txt",
358 "File extension for the temp files created by "+
359 "edit-current-field-in-external-editor.");
361 function get_filename_for_current_textfield (doc, elem) {
364 + ( elem.getAttribute("name")
365 || elem.getAttribute("id")
368 // get rid filesystem unfriendly chars
369 name = name.replace(doc.location.protocol, "")
370 .replace(/[^a-zA-Z0-9]+/g, "-")
371 .replace(/(^-+|-+$)/g, "")
372 + '.' + edit_field_in_external_editor_extension;
377 function edit_field_in_external_editor (buffer, elem) {
378 if (elem instanceof Ci.nsIDOMHTMLInputElement) {
379 var type = elem.getAttribute("type");
381 type = type.toLowerCase();
382 if (type == "hidden" || type == "checkbox" || type == "radio")
383 throw interactive_error("Element is not a text field.");
384 } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
385 throw interactive_error("Element is not a text field.");
387 var name = get_filename_for_current_textfield(buffer.document, elem);
388 var file = get_temporary_file(name);
392 write_text_file(file, elem.value);
398 // FIXME: decide if we should do this
399 var old_class = elem.className;
400 elem.className = "__conkeror_textbox_edited_externally " + old_class;
403 yield open_file_with_external_editor(file);
405 elem.value = read_text_file(file);
407 elem.className = old_class;
413 interactive("edit-current-field-in-external-editor",
414 "Edit the contents of the currently-focused text field in an external editor.",
417 var elem = buf.focused_element;
418 yield edit_field_in_external_editor(buf, elem);
422 define_variable("kill_whole_line", false,
423 "If true, `kill-line' with no arg at beg of line kills the whole line.");
425 function cut_to_end_of_line (buffer) {
426 var elem = buffer.focused_element;
428 var st = elem.selectionStart;
429 var en = elem.selectionEnd;
431 // there is no selection. set one up.
432 var eol = elem.value.indexOf("\n", en);
434 elem.selectionEnd = elem.textLength;
436 elem.selectionEnd = eol + 1;
437 else if (kill_whole_line &&
438 (st == 0 || elem.value[st - 1] == "\n"))
439 elem.selectionEnd = eol + 1;
441 elem.selectionEnd = eol;
443 buffer.do_command('cmd_cut');
445 /* FIXME: Make this work for richedit mode as well */
448 interactive("cut-to-end-of-line",
451 cut_to_end_of_line(I.buffer);
455 function downcase_word (I) {
456 modify_word_at_point(I, function (word) { return word.toLocaleLowerCase(); });
458 interactive("downcase-word",
459 "Convert following word to lower case, moving over.",
463 function upcase_word (I) {
464 modify_word_at_point(I, function (word) { return word.toLocaleUpperCase(); });
466 interactive("upcase-word",
467 "Convert following word to upper case, moving over.",
471 function capitalize_word (I) {
472 modify_word_at_point(I, function (word) {
473 if (word.length > 0) {
474 return word[0].toLocaleUpperCase() + word.substring(1);
479 interactive("capitalize-word",
480 "Capitalize the following word (or arg words), moving over.",