whitespace
[conkeror.git] / modules / content-buffer-input.js
blobe010a0743138cae438c621868546a7f8e5e62f70
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2008-2010 John J. Foerch
4  *
5  * Use, modification, and distribution are subject to the terms specified in the
6  * COPYING file.
7 **/
9 in_module(null);
11 require("content-buffer.js");
14 // note: The apparent misspellings here are not a bug.
15 // see https://developer.mozilla.org/en/XPath/Functions/translate
17 define_variable(
18     "browser_form_field_xpath_expression",
19     "//input[" + (
20         //        "translate(@type,'RADIO','radio')!='radio' and " +
21         //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
22         "translate(@type,'HIDEN','hiden')!='hidden'"
23         //        "translate(@type,'SUBMIT','submit')!='submit' and " +
24         //        "translate(@type,'REST','rest')!='reset'"
25     ) +  "] | " +
26         "//xhtml:input[" + (
27             //        "translate(@type,'RADIO','radio')!='radio' and " +
28             //        "translate(@type,'CHECKBOX','checkbox')!='checkbox' and " +
29             "translate(@type,'HIDEN','hiden')!='hidden'"
30             //        "translate(@type,'SUBMIT','submit')!='submit' and " +
31             //        "translate(@type,'REST','rest')!='reset'"
32         ) +  "] |" +
33         "//select | //xhtml:select | " +
34         "//textarea | //xhtml:textarea | " +
35         "//textbox | //xul:textbox",
36     "XPath expression matching elements to be selected by `browser-focus-next-form-field' " +
37         "and `browser-focus-previous-form-field.'");
39 function focus_next (buffer, count, xpath_expr, name) {
40     var focused_elem = buffer.focused_element;
41     if (count == 0)
42         return; // invalid count
44     function helper (win, skip_win) {
45         if (win == skip_win)
46             return null;
47         var doc = win.document;
48         var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
49             Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
50             null /* existing results */);
51         var length = res.snapshotLength;
52         if (length > 0) {
53             let valid_nodes = [];
54             for (let i = 0; i < length; ++i) {
55                 let elem = res.snapshotItem(i);
56                 if (elem.offsetWidth == 0 ||
57                     elem.offsetHeight == 0)
58                     continue;
59                 let style = win.getComputedStyle(elem, "");
60                 if (style.display == "none" || style.visibility == "hidden")
61                     continue;
62                 valid_nodes.push(elem);
63             }
65             if (valid_nodes.length > 0) {
66                 var index = -1;
67                 if (focused_elem != null)
68                     index = valid_nodes.indexOf(focused_elem);
69                 if (index == -1) {
70                     if (count > 0)
71                         index = count - 1;
72                     else
73                         index = -count;
74                 }
75                 else
76                     index = index + count;
77                 index = index % valid_nodes.length;
78                 if (index < 0)
79                     index += valid_nodes.length;
81                 return valid_nodes[index];
82             }
83         }
84         // Recurse on sub-frames
85         for (var i = 0, nframes = win.frames.length; i < nframes; ++i) {
86             var elem = helper(win.frames[i], skip_win);
87             if (elem)
88                 return elem;
89         }
90         return null;
91     }
93     var focused_win = buffer.focused_frame;
94     var elem = helper(focused_win, null);
95     if (!elem)
96         // if focused_frame is top_frame, we're doing twice as much
97         // work as necessary
98         elem = helper(buffer.top_frame, focused_win);
99     if (elem)
100         browser_element_focus(buffer, elem);
101     else
102         throw interactive_error("No "+name+" found");
105 interactive("browser-focus-next-form-field",
106             "Focus the next element matching "+
107             "`browser_form_field_xpath_expression'.",
108             function (I) {
109                 focus_next(I.buffer, I.p,
110                            browser_form_field_xpath_expression,
111                            "form field");
112             });
114 interactive("browser-focus-previous-form-field",
115             "Focus the previous element matching "+
116             "`browser_form_field_xpath_expression'.",
117             function (I) {
118                 focus_next(I.buffer, -I.p,
119                            browser_form_field_xpath_expression,
120                            "form field");
121             });
124 define_variable("links_xpath_expression",
125     "//*[@onclick or @onmouseover or @onmousedown or "+
126         "@onmouseup or @oncommand or @role='link'] | " +
127     "//input[not(@type='hidden')] | //a | //area | "+
128     "//iframe | //textarea | //button | //select",
129     "XPath expression matching elements to be selected by "+
130     "`focus-next-link' and `focus-previous-link.'");
132 interactive("focus-next-link",
133             "Focus the next element matching `links_xpath_expression'.",
134             function (I) {
135                 focus_next(I.buffer, I.p,
136                            links_xpath_expression,
137                            "link");
138             });
140 interactive("focus-previous-link",
141             "Focus the previous element matching `links_xpath_expression'.",
142             function (I) {
143                 focus_next(I.buffer, -I.p,
144                            links_xpath_expression,
145                            "link");
146             });
149 define_mime_type_table("external_editor_extension_overrides",
150     { text: { plain: "txt" } },
151     "Mime-type table for overriding file name extensions for the "+
152     "temporary file used by edit-current-field-in-external-editor.");
156  * external_editor_make_base_filename is called by
157  * edit_field_in_external_editor to generate a filename _without
158  * extension_ for the temporary file involved in external editing.
159  */
160 function external_editor_make_base_filename (elem, top_doc) {
161     var name = top_doc.URL
162         + "-"
163         + ( elem.getAttribute("name")
164             || elem.getAttribute("id")
165             || elem.tagName.toLowerCase() );
167     // get rid filesystem unfriendly chars
168     name = name.replace(top_doc.location.protocol, "")
169         .replace(/[^a-zA-Z0-9]+/g, "-")
170         .replace(/(^-+|-+$)/g, "");
172     return name;
176 function edit_field_in_external_editor (buffer, elem, doc) {
177     if (! doc) {
178         if (elem instanceof Ci.nsIDOMHTMLInputElement) {
179             var type = (elem.getAttribute("type") || "").toLowerCase();
180             if (type == "hidden" || type == "checkbox" || type == "radio")
181                 throw interactive_error("Element is not a text field.");
182         } else if (!(elem instanceof Ci.nsIDOMHTMLTextAreaElement))
183             throw interactive_error("Element is not a text field.");
184     }
186     var mime_type = doc ? doc.contentType : "text/plain";
187     var ext = external_editor_extension_overrides.get(mime_type);
188     if (! ext)
189         ext = mime_service.getPrimaryExtension(mime_type, null);
191     var name = external_editor_make_base_filename(elem, buffer.document);
192     if (ext)
193         name += "." + ext;
194     var file = get_temporary_file(name);
196     if (elem instanceof Ci.nsIDOMHTMLInputElement ||
197         elem instanceof Ci.nsIDOMHTMLTextAreaElement)
198     {
199         var content = elem.value;
200     } else {
201         content = elem.innerHTML;
202     }
204     // Write to file
205     try {
206         write_text_file(file, content);
207     } catch (e) {
208         file.remove(false);
209         throw e;
210     }
212     // FIXME: decide if we should do this
213     var old_class = elem.className;
214     elem.className = "__conkeror_textbox_edited_externally " + old_class;
216     try {
217         yield open_file_with_external_editor(file);
218         content = read_text_file(file);
219         if (elem instanceof Ci.nsIDOMHTMLInputElement ||
220             elem instanceof Ci.nsIDOMHTMLTextAreaElement)
221         {
222             elem.value = content;
223         } else {
224             elem.innerHTML = content;
225         }
226     } finally {
227         elem.className = old_class;
229         file.remove(false);
230     }
233 interactive("edit-current-field-in-external-editor",
234     "Edit the contents of the currently-focused text field in an external editor.",
235     function (I) {
236         var b = I.buffer;
237         var e = b.focused_element;
238         var frame = b.focused_frame;
239         var doc = null;
240         if (e) {
241             if (e.contentEditable == 'true')
242                 doc = e.ownerDocument;
243         } else if (frame && frame.document.designMode &&
244                    frame.document.designMode == "on") {
245             doc = frame.document;
246             e = frame.document.body;
247         }
248         yield edit_field_in_external_editor(b, e, doc);
249         e.blur();
250     });
253 define_variable("kill_whole_line", false,
254                 "If true, `kill-line' with no arg at beg of line kills the whole line.");
256 function cut_to_end_of_line (field, window) {
257     try {
258         var st = field.selectionStart;
259         var en = field.selectionEnd;
260         if (st == en) {
261             // there is no selection.  set one up.
262             var eol = field.value.indexOf("\n", en);
263             if (eol == -1)
264                 field.selectionEnd = field.textLength;
265             else if (eol == st)
266                 field.selectionEnd = eol + 1;
267             else if (kill_whole_line &&
268                      (st == 0 || field.value[st - 1] == "\n"))
269                 field.selectionEnd = eol + 1;
270             else
271                 field.selectionEnd = eol;
272         }
273         call_builtin_command(window, 'cmd_cut');
274     } catch (e) {
275         /* FIXME: Make this work for richedit mode as well */
276     }
278 interactive("cut-to-end-of-line",
279     null,
280     function (I) {
281         call_on_focused_field(I, function (field) {
282             cut_to_end_of_line(field, I.window);
283         });
284     });
287 interactive("downcase-word",
288     "Convert following word to lower case, moving over.",
289     function (I) {
290         call_on_focused_field(I, function (field) {
291             modify_word_at_point(field, function (word) {
292                 return word.toLocaleLowerCase();
293             });
294         });
295     });
298 interactive("upcase-word",
299     "Convert following word to upper case, moving over.",
300     function (I) {
301         call_on_focused_field(I, function (field) {
302             modify_word_at_point(field, function (word) {
303                 return word.toLocaleUpperCase();
304             });
305         });
306     });
309 interactive("capitalize-word",
310     "Capitalize the following word (or arg words), moving over.",
311     function (I) {
312         call_on_focused_field(I, function (field) {
313             modify_word_at_point(field, function (word) {
314                 if (word.length > 0)
315                     return word[0].toLocaleUpperCase() + word.substring(1);
316                 return word;
317             });
318         });
319     });
321 provide("content-buffer-input");