youtube.js: allow _ in video id
[conkeror.git] / modules / minibuffer.js
blob5ba7f1d81cab00c6b975d728f6cd0fd5738ad402
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  *
4  * Use, modification, and distribution are subject to the terms specified in the
5  * COPYING file.
6 **/
8 /* This should only be used for minibuffer states where it makes
9  * sense.  In particular, it should not be used if additional cleanup
10  * must be done. */
11 function minibuffer_abort (window)
13     var m = window.minibuffer;
14     var s = m.current_state;
15     if (s == null)
16         throw "Invalid minibuffer state";
17     m.pop_state();
19 interactive("minibuffer-abort", function (I) {minibuffer_abort(I.window);});
21 define_builtin_commands(
22     "minibuffer-",
23     function (I, command) {
24         try {
25             var m = I.minibuffer;
26             if (m._input_mode_enabled)
27             {
28                 m._restore_normal_state();
29                 var e = m.input_element;
30                 var c = e.controllers.getControllerForCommand(command);
31                 try {
32                     m.ignore_input_events = true;
33                     if (c && c.isCommandEnabled(command))
34                         c.doCommand(command);
35                 } finally {
36                     m.ignore_input_events = false;
37                 }
38                 var s = m.current_state;
39                 if (s.ran_minibuffer_command)
40                     s.ran_minibuffer_command(command);
41             }
42         } catch (e)
43         {
44             /* Ignore exceptions. */
45         }
46     },
47     function (I) {
48         I.minibuffer.current_state.mark_active = !I.minibuffer.current_state.mark_active;
49     },
51     function (I) I.minibuffer.current_state.mark_active
54 function minibuffer_state(keymap, use_input_mode)
56     this.keymap = keymap;
57     this.use_input_mode = use_input_mode;
59 minibuffer_state.prototype.load = function () {}
60 minibuffer_state.prototype.unload = function () {}
61 minibuffer_state.prototype.destroy = function () {}
63 function minibuffer_message_state(keymap, message, destroy_function)
65     minibuffer_state.call(this, keymap, false);
66     this._message = message;
67     if (destroy_function)
68         this.destroy = destroy_function;
70 minibuffer_message_state.prototype = {
71     __proto__: minibuffer_state.prototype,
72     load : function (window) {
73         this.window = window;
74     },
75     unload : function (window) {
76         this.window = null;
77     },
78     get message () { return this._message; },
79     set message (x) {
80         if (this.window) {
81             this.window.minibuffer._restore_normal_state();
82             this.window.minibuffer._show(this._message);
83         }
84     }
87 function minibuffer_input_state(keymap, prompt, input, selection_start, selection_end)
89     this.prompt = prompt;
90     if (input)
91         this.input = input;
92     else
93         this.input = "";
94     if (selection_start)
95         this.selection_start = selection_start;
96     else
97         this.selection_start = 0;
98     if (selection_end)
99         this.selection_end = selection_end;
100     else
101         this.selection_end = this.selection_start;
103     minibuffer_state.call(this, keymap, true);
105 minibuffer_input_state.prototype.__proto__ = minibuffer_state.prototype;
109  * The parameter `args' is an object specifying the arguments for
110  * basic_minibuffer_state.  The following properties of args must/may
111  * be set:
113  * prompt:            [required]
115  * initial_value:     [optional] specifies the initial text
117  * select:            [optional] specifies to select the initial text if set to non-null
118  */
119 define_keywords("$prompt", "$initial_value", "$select");
120 function basic_minibuffer_state()
122     keywords(arguments);
123     var initial_value = arguments.$initial_value || "";
124     var sel_start, sel_end;
125     if (arguments.$select)
126     {
127         sel_start = 0;
128         sel_end = initial_value.length;
129     } else {
130         sel_start = sel_end = initial_value.length;
131     }
132     minibuffer_input_state.call(this, minibuffer_base_keymap,
133                                 arguments.$prompt, initial_value,
134                                 sel_start, sel_end);
136 basic_minibuffer_state.prototype.__proto__ = minibuffer_input_state.prototype; // inherit from minibuffer_state
138 define_variable("minibuffer_input_mode_show_message_timeout", 1000, "Time duration (in milliseconds) to flash minibuffer messages while in minibuffer input mode.");
140 function minibuffer (window)
142     this.element = window.document.getElementById("minibuffer");
143     this.output_element = window.document.getElementById("minibuffer-message");
144     this.input_prompt_element = window.document.getElementById("minibuffer-prompt");
145     this.input_element = window.document.getElementById("minibuffer-input");
146     var m = this;
147     this.input_element.inputField.addEventListener("blur", function() {
148             if (m.active && m._input_mode_enabled && !m._showing_message)
149             {
150                 window.setTimeout(
151                     function(){
152                         m.input_element.inputField.focus();
153                     }, 0);
154             }
155         }, false);
156     this.input_element.addEventListener("input", function(e) {
157         if (m.ignore_input_events || !m._input_mode_enabled)
158             return;
159         var s = m.current_state;
160         if (s) {
161             if (s.handle_input)
162                 s.handle_input(m);
163         }
164     }, true);
166     // Ensure that the input area will have focus if a message is
167     // currently being flashed so that the default handler for key
168     // events will properly add text to the input area.
169     window.addEventListener("keydown", function (e) {
170         if (m._input_mode_enabled && m._showing_message)
171             m._restore_normal_state();
172     }, true);
173     this.window = window;
174     this.last_message = "";
175     this.states = [];
178 minibuffer.prototype = {
179     constructor : minibuffer.constructor,
180     get _selection_start () { return this.input_element.selectionStart; },
181     get _selection_end () { return this.input_element.selectionEnd; },
182     get _input_text () { return this.input_element.value; },
183     set _input_text (text) { this.input_element.value = text; },
184     get prompt () { return this.input_prompt_element.value; },
185     set prompt (s) { this.input_prompt_element.value = s; },
187     set_input_state : function(x) {
188         this._input_text = x[0];
189         this._set_selection(x[1], x[2]);
190     },
192     _set_selection : function (start, end) {
193         if (start == null)
194             start = this._input_text.length;
195         if (end == null)
196             end = this._input_text.length;
197         this.input_element.setSelectionRange(start,end);
198     },
200     /* Saved focus state */
201     saved_focused_frame : null,
202     saved_focused_element : null,
204     default_message : "",
206     current_message : null,
208     /* This method will display the specified string in the
209      * minibuffer, without recording it in any log/Messages buffer. */
210     show : function (str, force) {
211         if (!this.active || force) {
212             this.current_message = str;
213             this._show(str);
214         }
215     },
217     _show : function (str, force) {
218         if (this.last_message != str)
219         {
220             this.output_element.value = str;
221             this.last_message = str;
222         }
223     },
225     message : function (str) {
226         /* TODO: add the message to a *Messages* buffer, and/or
227          * possibly dump them to the console. */
228         this.show(str, true /* force */);
230         if (str.length > 0 && this.active)
231             this._flash_temporary_message();
232     },
233     clear : function () {
234         this.current_message = null;
235         if (!this.active)
236             this._show(this.default_message);
237     },
239     set_default_message : function (str) {
240         this.default_message = str;
241         if (this.current_message == null)
242             this._show(str);
243     },
245     get current_state () {
246         if (this.states.length == 0)
247             return null;
248         return this.states[this.states.length - 1];
249     },
251     push_state : function (state) {
252         this._save_state();
253         this.states.push(state);
254         this._restore_state();
255     },
257     pop_state : function () {
258         this.current_state.destroy();
259         this.states.pop();
260         this._restore_state();
261     },
263     pop_all : function () {
264         while (this.states.length > 0) {
265             this.current_state.destroy();
266             this.states.pop();
267         }
268     },
270     remove_state : function (state) {
271         var i = this.states.indexOf(state);
272         if (i == -1)
273             return;
274         var was_current = (i == (this.states.length - 1));
275         state.destroy();
276         this.states.splice(i, 1);
277         if (was_current)
278             this._restore_state();
279     },
281     _input_mode_enabled : false,
283     active : false,
285     /* If _input_mode_enabled is true, this is set to indicate that
286      * the message area is being temporarily shown instead of the
287      * input box. */
288     _showing_message : false,
290     _message_timer_ID : null,
292     /* This must only be called if _input_mode_enabled is true */
293     _restore_normal_state : function () {
294         if (this._showing_message)
295         {
296             this.window.clearTimeout(this._message_timer_ID);
297             this._message_timer_ID = null;
298             this._showing_message = false;
300             if (this._input_mode_enabled)
301                 this._switch_to_input_mode();
302             else
303                 this._show(this.current_state._message);
304         }
305     },
307     /* This must only be called if _input_mode_enabled is true */
308     _flash_temporary_message : function () {
309         if (this._showing_message)
310             this.window.clearTimeout(this._message_timer_ID);
311         else {
312             this._showing_message = true;
313             if (this._input_mode_enabled)
314                 this._switch_to_message_mode();
315         }
316         var obj = this;
317         this._message_timer_ID = this.window.setTimeout(function(){
318             obj._restore_normal_state();
319         }, minibuffer_input_mode_show_message_timeout);
320     },
322     _switch_to_input_mode : function () {
323         this.element.setAttribute("minibuffermode", "input");
324         this.input_element.inputField.focus();
325     },
327     _switch_to_message_mode : function () {
328         this.element.setAttribute("minibuffermode", "message");
329     },
331     _restore_state : function () {
332         var s = this.current_state;
333         var want_input_mode = false;
334         if (s) {
335             if (!this.active) {
336                 this.saved_focused_frame = this.window.document.commandDispatcher.focusedWindow;
337                 this.saved_focused_element = this.window.document.commandDispatcher.focusedElement;
338             }
339             if (s.use_input_mode) {
340                 want_input_mode = true;
341                 this._input_text = s.input;
342                 this.prompt = s.prompt;
343                 this._set_selection(s.selection_start, s.selection_end);
344             } else {
345                 this._show(s._message);
346             }
347             s.load(this.window);
348             this.window.keyboard.set_override_keymap(s.keymap);
349             this.active = true;
350         } else {
351             if (this.active) {
352                 this.active = false;
353                 this.window.keyboard.set_override_keymap(null);
354                 if (this.saved_focused_element)
355                     set_focus_no_scroll(this.window, this.saved_focused_element);
356                 else if (this.saved_focused_frame)
357                     set_focus_no_scroll(this.window, this.saved_focused_frame);
358                 this.saved_focused_element = null;
359                 this.saved_focused_frame = null;
360                 this._show(this.current_message || this.default_message);
361             }
362         }
363         var in_input_mode = this._input_mode_enabled && !this._showing_message;
364         if (this._showing_message) {
365             this.window.clearTimeout(this._message_timer_ID);
366             this._message_timer_ID = null;
367             this._showing_message = false;
368         }
369         if (want_input_mode && !in_input_mode)
370             this._switch_to_input_mode();
371         else if (!want_input_mode && in_input_mode)
372             this._switch_to_message_mode();
373         this._input_mode_enabled = want_input_mode;
374     },
376     _save_state : function () {
377         var s = this.current_state;
378         if (s)
379         {
380             if (s.use_input_mode) {
381                 s.input = this._input_text;
382                 s.prompt = this.prompt;
383                 s.selection_start = this._selection_start;
384                 s.selection_end = this._selection_end;
385             }
386             s.unload(this.window);
387         }
388     },
390     insert_before : function (element) {
391         this.element.parentNode.insertBefore(element, this.element);
392     }
395 function minibuffer_initialize_window(window)
397     window.minibuffer = new minibuffer(window);
400 add_hook("window_initialize_early_hook", minibuffer_initialize_window);
402 function minibuffer_window_close_handler(window) {
403     window.minibuffer.pop_all();
405 add_hook("window_close_hook", minibuffer_window_close_handler);
407 /* Note: This is concise, but doesn't seem to be useful in practice,
408  * because nothing can be done with the state alone. */
409 minibuffer.prototype.check_state = function(type) {
410     var s = this.current_state;
411     if (!(s instanceof type))
412         throw new Error("Invalid minibuffer state.");
413     return s;
416 minibuffer.prototype.show_wait_message = function (initial_message, destroy_function) {
417     var s = new minibuffer_message_state(minibuffer_message_keymap, initial_message, destroy_function);
418     this.push_state(s);
419     return s;
422 minibuffer.prototype.wait_for = function minibuffer__wait_for(message, coroutine) {
423     var cc = yield CONTINUATION;
424     var done = false;
425     var s = this.show_wait_message(message, function () { if (!done) cc.throw(abort()); });
426     var result;
427     try {
428         result = yield coroutine;
429     }
430     finally {
431         done = true;
432         this.remove_state(s);
433     }
434     yield co_return(result);