minibuffer.js: fix bug in remove_state
[conkeror.git] / modules / minibuffer.js
blob475adb66b278f014dbb7a2ea58b3485151ce034f
2 /* This should only be used for minibuffer states where it makes
3  * sense.  In particular, it should not be used if additional cleanup
4  * must be done. */
5 function minibuffer_abort (window)
7     var m = window.minibuffer;
8     var s = m.current_state;
9     if (s == null)
10         throw "Invalid minibuffer state";
11     m.pop_state();
13 interactive("minibuffer-abort", function (I) {minibuffer_abort(I.window);});
15 define_builtin_commands(
16     "minibuffer-",
17     function (I, command) {
18         try {
19             var m = I.minibuffer;
20             if (m._input_mode_enabled)
21             {
22                 m._restore_normal_state();
23                 var e = m.input_element;
24                 var c = e.controllers.getControllerForCommand(command);
25                 try {
26                     m.ignore_input_events = true;
27                     if (c && c.isCommandEnabled(command))
28                         c.doCommand(command);
29                 } finally {
30                     m.ignore_input_events = false;
31                 }
32                 var s = m.current_state;
33                 let x = s.ran_minibuffer_command;
34                 if (x)
35                     x(command);
36             }
37         } catch (e)
38         {
39             /* Ignore exceptions. */
40         }
41     },
42     function (I) {
43         I.minibuffer.current_state.mark_active = !I.minibuffer.current_state.mark_active;
44     },
46     function (I) I.minibuffer.current_state.mark_active
49 function minibuffer_state(keymap, use_input_mode)
51     this.keymap = keymap;
52     this.use_input_mode = use_input_mode;
54 minibuffer_state.prototype.load = function () {}
55 minibuffer_state.prototype.unload = function () {}
56 minibuffer_state.prototype.destroy = function () {}
58 function minibuffer_message_state(keymap, message, destroy_function)
60     minibuffer_state.call(this, keymap, false);
61     this._message = message;
62     if (destroy_function)
63         this.destroy = destroy_function;
65 minibuffer_message_state.prototype = {
66     __proto__: minibuffer_state.prototype,
67     load : function (window) {
68         this.window = window;
69     },
70     unload : function (window) {
71         this.window = null;
72     },
73     get message () { return this._message; },
74     set message (x) {
75         if (this.window) {
76             this.window.minibuffer._restore_normal_state();
77             this.window.minibuffer._show(this._message);
78         }
79     }
82 function minibuffer_input_state(keymap, prompt, input, selection_start, selection_end)
84     this.prompt = prompt;
85     if (input)
86         this.input = input;
87     else
88         this.input = "";
89     if (selection_start)
90         this.selection_start = selection_start;
91     else
92         this.selection_start = 0;
93     if (selection_end)
94         this.selection_end = selection_end;
95     else
96         this.selection_end = this.selection_start;
98     minibuffer_state.call(this, keymap, true);
100 minibuffer_input_state.prototype.__proto__ = minibuffer_state.prototype;
104  * The parameter `args' is an object specifying the arguments for
105  * basic_minibuffer_state.  The following properties of args must/may
106  * be set:
108  * prompt:            [required]
110  * initial_value:     [optional] specifies the initial text
112  * select:            [optional] specifies to select the initial text if set to non-null
113  */
114 define_keywords("$prompt", "$initial_value", "$select");
115 function basic_minibuffer_state()
117     keywords(arguments);
118     var initial_value = arguments.$initial_value || "";
119     var sel_start, sel_end;
120     if (arguments.$select)
121     {
122         sel_start = 0;
123         sel_end = initial_value.length;
124     } else {
125         sel_start = sel_end = initial_value.length;
126     }
127     minibuffer_input_state.call(this, minibuffer_base_keymap,
128                                 arguments.$prompt, initial_value,
129                                 sel_start, sel_end);
131 basic_minibuffer_state.prototype.__proto__ = minibuffer_input_state.prototype; // inherit from minibuffer_state
133 define_variable("minibuffer_input_mode_show_message_timeout", 1000, "Time duration (in milliseconds) to flash minibuffer messages while in minibuffer input mode.");
135 function minibuffer (window)
137     this.element = window.document.getElementById("minibuffer");
138     this.output_element = window.document.getElementById("minibuffer-message");
139     this.input_prompt_element = window.document.getElementById("minibuffer-prompt");
140     this.input_element = window.document.getElementById("minibuffer-input");
141     var m = this;
142     this.input_element.inputField.addEventListener("blur", function() {
143             if (m.active && m._input_mode_enabled && !m._showing_message)
144             {
145                 window.setTimeout(
146                     function(){
147                         m.input_element.inputField.focus();
148                     }, 0);
149             }
150         }, false);
151     this.input_element.addEventListener("input", function(e) {
152         if (m.ignore_input_events || !m._input_mode_enabled)
153             return;
154         var s = m.current_state;
155         if (s) {
156             if (s.handle_input)
157                 s.handle_input(m);
158         }
159     }, true);
161     // Ensure that the input area will have focus if a message is
162     // currently being flashed so that the default handler for key
163     // events will properly add text to the input area.
164     window.addEventListener("keydown", function (e) {
165         if (m._input_mode_enabled && m._showing_message)
166             m._restore_normal_state();
167     }, true);
168     this.window = window;
169     this.last_message = "";
170     this.states = [];
173 minibuffer.prototype = {
174     constructor : minibuffer.constructor,
175     get _selection_start () { return this.input_element.selectionStart; },
176     get _selection_end () { return this.input_element.selectionEnd; },
177     get _input_text () { return this.input_element.value; },
178     set _input_text (text) { this.input_element.value = text; },
179     get prompt () { return this.input_prompt_element.value; },
180     set prompt (s) { this.input_prompt_element.value = s; },
182     _set_selection : function (start, end) {
183         if (start == null)
184             start = this._input_text.length;
185         if (end == null)
186             end = this._input_text.length;
187         this.input_element.setSelectionRange(start,end);
188     },
190     /* Saved focus state */
191     saved_focused_frame : null,
192     saved_focused_element : null,
194     default_message : "",
196     current_message : null,
198     /* This method will display the specified string in the
199      * minibuffer, without recording it in any log/Messages buffer. */
200     show : function (str, force) {
201         if (!this.active || force) {
202             this.current_message = str;
203             this._show(str);
204         }
205     },
207     _show : function (str, force) {
208         if (this.last_message != str)
209         {
210             this.output_element.value = str;
211             this.last_message = str;
212         }
213     },
215     message : function (str) {
216         /* TODO: add the message to a *Messages* buffer, and/or
217          * possibly dump them to the console. */
218         this.show(str, true /* force */);
220         if (str.length > 0 && this.active)
221             this._flash_temporary_message();
222     },
223     clear : function () {
224         this.current_message = null;
225         if (!this.active)
226             this._show(this.default_message);
227     },
229     set_default_message : function (str) {
230         this.default_message = str;
231         if (this.current_message == null)
232             this._show(str);
233     },
235     get current_state () {
236         if (this.states.length == 0)
237             return null;
238         return this.states[this.states.length - 1];
239     },
241     push_state : function (state) {
242         this._save_state();
243         this.states.push(state);
244         this._restore_state();
245         state.load(this.window);
246     },
248     pop_state : function () {
249         this.current_state.destroy();
250         this.states.pop();
251         this._restore_state();
252     },
254     pop_all : function () {
255         while (this.states.length > 0) {
256             this.current_state.destroy();
257             this.states.pop();
258         }
259     },
261     remove_state : function (state) {
262         var i = this.states.indexOf(state);
263         if (i == -1)
264             return;
265         var was_current = (i == (this.states.length - 1));
266         state.destroy();
267         this.states.splice(i, 1);
268         if (was_current)
269             this._restore_state();
270     },
272     _input_mode_enabled : false,
274     active : false,
276     /* If _input_mode_enabled is true, this is set to indicate that
277      * the message area is being temporarily shown instead of the
278      * input box. */
279     _showing_message : false,
281     _message_timer_ID : null,
283     /* This must only be called if _input_mode_enabled is true */
284     _restore_normal_state : function () {
285         if (this._showing_message)
286         {
287             this.window.clearTimeout(this._message_timer_ID);
288             this._message_timer_ID = null;
289             this._showing_message = false;
291             if (this._input_mode_enabled)
292                 this._switch_to_input_mode();
293             else
294                 this._show(this.current_state._message);
295         }
296     },
298     /* This must only be called if _input_mode_enabled is true */
299     _flash_temporary_message : function () {
300         if (this._showing_message)
301             this.window.clearTimeout(this._message_timer_ID);
302         else {
303             this._showing_message = true;
304             if (this._input_mode_enabled)
305                 this._switch_to_message_mode();
306         }
307         var obj = this;
308         this._message_timer_ID = this.window.setTimeout(function(){
309             obj._restore_normal_state();
310         }, minibuffer_input_mode_show_message_timeout);
311     },
313     _switch_to_input_mode : function () {
314         this.element.setAttribute("minibuffermode", "input");
315         this.input_element.inputField.focus();
316     },
318     _switch_to_message_mode : function () {
319         this.element.setAttribute("minibuffermode", "message");
320     },
322     _restore_state : function () {
323         var s = this.current_state;
324         var want_input_mode = false;
325         if (s) {
326             if (!this.active) {
327                 this.saved_focused_frame = this.window.document.commandDispatcher.focusedWindow;
328                 this.saved_focused_element = this.window.document.commandDispatcher.focusedElement;
329             }
330             if (s.use_input_mode) {
331                 want_input_mode = true;
332                 this._input_text = s.input;
333                 this.prompt = s.prompt;
334                 this._set_selection(s.selection_start, s.selection_end);
335             } else {
336                 this._show(s._message);
337             }
338             this.window.keyboard.set_override_keymap(s.keymap);
339             this.active = true;
340         } else {
341             if (this.active) {
342                 this.active = false;
343                 this.window.keyboard.set_override_keymap(null);
344                 if (this.saved_focused_element)
345                     set_focus_no_scroll(this.window, this.saved_focused_element);
346                 else if (this.saved_focused_frame)
347                     set_focus_no_scroll(this.window, this.saved_focused_frame);
348                 this.saved_focused_element = null;
349                 this.saved_focused_frame = null;
350                 this._show(this.current_message || this.default_message);
351             }
352         }
353         var in_input_mode = this._input_mode_enabled && !this._showing_message;
354         if (this._showing_message) {
355             this.window.clearTimeout(this._message_timer_ID);
356             this._message_timer_ID = null;
357             this._showing_message = false;
358         }
359         if (want_input_mode && !in_input_mode)
360             this._switch_to_input_mode();
361         else if (!want_input_mode && in_input_mode)
362             this._switch_to_message_mode();
363         this._input_mode_enabled = want_input_mode;
364     },
366     _save_state : function () {
367         var s = this.current_state;
368         if (s)
369         {
370             if (s.use_input_mode) {
371                 s.input = this._input_text;
372                 s.prompt = this.prompt;
373                 s.selection_start = this._selection_start;
374                 s.selection_end = this._selection_end;
375             }
376             s.unload(this.window);
377         }
378     },
380     insert_before : function (element) {
381         this.element.parentNode.insertBefore(element, this.element);
382     }
385 function minibuffer_initialize_window(window)
387     window.minibuffer = new minibuffer(window);
390 add_hook("window_initialize_early_hook", minibuffer_initialize_window);
392 function minibuffer_window_close_handler(window) {
393     window.minibuffer.pop_all();
395 add_hook("window_close_hook", minibuffer_window_close_handler);
397 /* Note: This is concise, but doesn't seem to be useful in practice,
398  * because nothing can be done with the state alone. */
399 minibuffer.prototype.check_state = function(type) {
400     var s = this.current_state;
401     if (!(s instanceof type))
402         throw new Error("Invalid minibuffer state.");
403     return s;
406 minibuffer.prototype.show_wait_message = function (initial_message, destroy_function) {
407     var s = new minibuffer_message_state(minibuffer_message_keymap, initial_message, destroy_function);
408     this.push_state(s);
409     return s;