2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3 * (C) Copyright 2008 John J. Foerch
5 * Use, modification, and distribution are subject to the terms specified in the
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,
20 $class = "input_mode",
21 $enable = function (buffer) {
22 check_buffer(buffer, content_buffer);
23 content_buffer_update_keymap_for_input_mode(buffer);
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
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.");
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;
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" &&
84 input_mode_function = text_input_mode;
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;
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;
102 switch (elem.contentEditable) {
104 in_design_mode = true;
108 default: // == "inherit"
109 elem = elem.parentNode;
114 input_mode_function = richedit_input_mode;
117 if (input_mode_function) {
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
127 input_mode_function(buffer, true);
131 normal_input_mode(buffer, true);
135 add_hook("content_buffer_focus_change_hook",
137 content_buffer_update_input_mode_for_focus(buf, false);
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);
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);
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;
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);
178 minibuffer_input_mode_indicator.prototype = {
179 update : function () {
180 var buf = this.window.buffers.current;
181 var mode = buf.input_mode;
183 this.window.minibuffer.element.className = "minibuffer-" + buf.input_mode.replace("_","-","g");
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);
191 define_global_window_mode("minibuffer_input_mode_indicator", "window_initialize_hook");
192 minibuffer_input_mode_indicator_mode(true);
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 "+
201 define_global_mode("browser_prevent_automatic_form_focus_mode",
202 function () {}, // enable
203 function () {} // disable
206 // note: The apparent misspellings here are not a bug.
207 // see https://developer.mozilla.org/en/XPath/Functions/translate
210 "browser_form_field_xpath_expression",
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'"
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'"
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 focus_next (buffer, count, xpath_expr, name) {
232 var focused_elem = buffer.focused_element;
234 return; // invalid count
236 function helper (win, skip_win) {
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;
245 let valid_nodes = [];
246 for (let i = 0; i < length; ++i) {
247 let elem = res.snapshotItem(i);
248 if (elem.offsetWidth == 0 ||
249 elem.offsetHeight == 0)
251 let style = win.getComputedStyle(elem, "");
252 if (style.display == "none" || style.visibility == "hidden")
254 valid_nodes.push(elem);
257 if (valid_nodes.length > 0) {
259 if (focused_elem != null)
260 index = valid_nodes.indexOf(focused_elem);
268 index = index + count;
269 index = index % valid_nodes.length;
271 index += valid_nodes.length;
273 return valid_nodes[index];
276 // Recurse on sub-frames
277 for (var i = 0; i < win.frames.length; ++i) {
278 var elem = helper(win.frames[i], skip_win);
285 var focused_win = buffer.focused_frame;
286 var elem = helper(focused_win, null);
288 // if focused_frame is top_frame, we're doing twice as much
290 elem = helper(buffer.top_frame, focused_win);
292 browser_element_focus(buffer, elem);
294 throw interactive_error("No "+name+" found");
297 interactive("browser-focus-next-form-field",
298 "Focus the next element matching "+
299 "`browser_form_field_xpath_expression'.",
301 focus_next(I.buffer, I.p,
302 browser_form_field_xpath_expression,
306 interactive("browser-focus-previous-form-field",
307 "Focus the previous element matching "+
308 "`browser_form_field_xpath_expression'.",
310 focus_next(I.buffer, -I.p,
311 browser_form_field_xpath_expression,
316 define_variable("links_xpath_expression",
317 "//*[@onclick or @onmouseover or @onmousedown or "+
318 "@onmouseup or @oncommand or @role='link'] | " +
319 "//input[not(@type='hidden')] | //a | //area | "+
320 "//iframe | //textarea | //button | //select",
321 "XPath expression matching elements to be selected by "+
322 "`focus-next-link' and `focus-previous-link.'");
324 interactive("focus-next-link",
325 "Focus the next element matching `links_xpath_expression'.",
327 focus_next(I.buffer, I.p,
328 links_xpath_expression,
332 interactive("focus-previous-link",
333 "Focus the previous element matching `links_xpath_expression'.",
335 focus_next(I.buffer, -I.p,
336 links_xpath_expression,
341 define_variable('edit_field_in_external_editor_extension', "txt",
342 "File extension for the temp files created by "+
343 "edit-current-field-in-external-editor.");
345 function get_filename_for_current_textfield(doc, elem) {
348 + ( elem.getAttribute("name")
349 || elem.getAttribute("id")
352 // get rid filesystem unfriendly chars
353 name = name.replace(doc.location.protocol, "")
354 .replace(/[^a-zA-Z0-9]+/g, "-")
355 .replace(/(^-+|-+$)/g, "")
356 + '.' + edit_field_in_external_editor_extension;
361 function edit_field_in_external_editor(buffer, elem) {
362 if (elem instanceof Ci.nsIDOMHTMLInputElement) {
363 var type = elem.getAttribute("type");
365 type = type.toLowerCase();
366 if (type == "hidden" || type == "checkbox" || type == "radio")
367 throw interactive_error("Element is not a text field.");
368 } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
369 throw interactive_error("Element is not a text field.");
371 var name = get_filename_for_current_textfield(buffer.document, elem);
372 var file = get_temporary_file(name);
376 write_text_file(file, elem.value);
382 // FIXME: decide if we should do this
383 var old_class = elem.className;
384 elem.className = "__conkeror_textbox_edited_externally " + old_class;
387 yield open_file_with_external_editor(file);
389 elem.value = read_text_file(file);
391 elem.className = old_class;
397 interactive("edit-current-field-in-external-editor",
398 "Edit the contents of the currently-focused text field in an external editor.",
401 var elem = buf.focused_element;
402 yield edit_field_in_external_editor(buf, elem);
406 define_variable("kill_whole_line", false,
407 "If true, `kill-line' with no arg at beg of line kills the whole line.");
409 function cut_to_end_of_line (buffer) {
410 var elem = buffer.focused_element;
412 var st = elem.selectionStart;
413 var en = elem.selectionEnd;
415 // there is no selection. set one up.
416 var eol = elem.value.indexOf ("\n", en);
419 elem.selectionEnd = elem.textLength;
420 } else if (eol == st) {
421 elem.selectionEnd = eol + 1;
422 } else if (kill_whole_line &&
423 (st == 0 || elem.value[st - 1] == "\n"))
425 elem.selectionEnd = eol + 1;
427 elem.selectionEnd = eol;
430 buffer.do_command ('cmd_cut');
432 /* FIXME: Make this work for richedit mode as well */
435 interactive("cut-to-end-of-line",
438 cut_to_end_of_line (I.buffer);
442 function downcase_word(I) {
443 modify_word_at_point(I, function (word) { return word.toLocaleLowerCase(); });
445 interactive("downcase-word",
446 "Convert following word to lower case, moving over.",
450 function upcase_word(I) {
451 modify_word_at_point(I, function (word) { return word.toLocaleUpperCase(); });
453 interactive("upcase-word",
454 "Convert following word to upper case, moving over.",
458 function capitalize_word(I) {
459 modify_word_at_point(I, function (word) {
460 if (word.length > 0) {
461 return word[0].toLocaleUpperCase() + word.substring(1);
466 interactive("capitalize-word",
467 "Capitalize the following word (or arg words), moving over.",