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