1 /*
  2  * RapidContext <http://www.rapidcontext.com/>
  3  * Copyright (c) 2007-2013 Per Cederberg. All rights reserved.
  4  *
  5  * This program is free software: you can redistribute it and/or
  6  * modify it under the terms of the BSD license.
  7  *
  8  * This program is distributed in the hope that it will be useful,
  9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 11  *
 12  * See the RapidContext LICENSE for more details.
 13  */
 14
 15 // Namespace initialization
 16 if (typeof(RapidContext) == "undefined") {
 17     RapidContext = {};
 18 }
 19 RapidContext.Widget = RapidContext.Widget || { Classes: {}};
 20
 21 /**
 22  * Creates a new text field widget.
 23  *
 24  * @constructor
 25  * @param {Object} attrs the widget and node attributes
 26  * @param {String} [attrs.name] the form field name
 27  * @param {String} [attrs.value] the field value, defaults to ""
 28  * @param {String} [attrs.helpText] the help text shown on empty input,
 29  *            defaults to ""
 30  * @param {Boolean} [attrs.disabled] the disabled widget flag, defaults to
 31  *            false
 32  * @param {Boolean} [attrs.hidden] the hidden widget flag, defaults to false
 33  * @param {Object} [...] the initial text content
 34  *
 35  * @return {Widget} the widget DOM node
 36  *
 37  * @class The text field widget class. Used to provide a text input field for a
 38  *     single line, using the `<input>` HTML element. The text field may also
 39  *     be connected to a popup (for auto-complete or similar).
 40  * @property {Boolean} disabled The read-only widget disabled flag.
 41  * @property {Boolean} focused The read-only widget focused flag.
 42  * @property {String} defaultValue The value to use on form reset.
 43  * @extends RapidContext.Widget
 44  *
 45  * @example {JavaScript}
 46  * var attrs = { name: "name", helpText: "Your Name Here" };
 47  * var field = RapidContext.Widget.TextField(attrs);
 48  *
 49  * @example {User Interface XML}
 50  * <TextField name="name" helpText="Your Name Here" />
 51  */
 52 RapidContext.Widget.TextField = function (attrs/*, ...*/) {
 53     var type = (attrs && attrs.type) || "text";
 54     var text = (attrs && attrs.value) || "";
 55     for (var i = 1; i < arguments.length; i++) {
 56         var o = arguments[i];
 57         if (RapidContext.Util.isDOM(o)) {
 58             text += MochiKit.DOM.scrapeText(o);
 59         } else if (o != null) {
 60             text += o.toString();
 61         }
 62     }
 63     var o = MochiKit.DOM.INPUT({ type: type, value: text });
 64     RapidContext.Widget._widgetMixin(o, arguments.callee);
 65     o.addClass("widgetTextField");
 66     o.focused = false;
 67     o._popupCreated = false;
 68     o.setAttrs(MochiKit.Base.update({ helpText: "", value: text }, attrs));
 69     var changeHandler = RapidContext.Widget._eventHandler(null, "_handleChange");
 70     o.onkeyup = changeHandler;
 71     o.oncut = changeHandler;
 72     o.onpaste = changeHandler;
 73     var focusHandler = RapidContext.Widget._eventHandler(null, "_handleFocus");
 74     o.onfocus = focusHandler;
 75     o.onblur = focusHandler;
 76     return o;
 77 };
 78
 79 // Register widget class
 80 RapidContext.Widget.Classes.TextField = RapidContext.Widget.TextField;
 81
 82 /**
 83  * Emitted when the text is modified. This event is triggered by either
 84  * user events (keypress, paste, cut, blur) or by setting the value via
 85  * setAttrs(). The DOM standard onchange event has no 'event.detail'
 86  * data and is triggered on blur. The synthetic onchange events all
 87  * contain an 'event.detail' object with 'before', 'after' and 'cause'
 88  * properties.
 89  *
 90  * @name RapidContext.Widget.TextField#onchange
 91  * @event
 92  */
 93
 94 /**
 95  * Emitted when an item has been selected in the connected popup.
 96  * This event signal carries no 'event.detail' information.
 97  *
 98  * @name RapidContext.Widget.TextField#ondataavailable
 99  * @event
100  */
101
102 /**
103  * Updates the widget or HTML DOM node attributes.
104  *
105  * @param {Object} attrs the widget and node attributes to set
106  * @param {String} [attrs.name] the form field name
107  * @param {String} [attrs.value] the field value
108  * @param {String} [attrs.helpText] the help text shown on empty input
109  * @param {Boolean} [attrs.disabled] the disabled widget flag
110  * @param {Boolean} [attrs.hidden] the hidden widget flag
111  */
112 RapidContext.Widget.TextField.prototype.setAttrs = function (attrs) {
113     attrs = MochiKit.Base.update({}, attrs);
114     var locals = RapidContext.Util.mask(attrs, ["helpText", "value"]);
115     if (typeof(locals.helpText) != "undefined") {
116         var str = MochiKit.Format.strip(locals.helpText);
117         if ("placeholder" in this) {
118             attrs.placeholder = str;
119         } else {
120             this.helpText = str;
121         }
122     }
123     if (typeof(locals.value) != "undefined") {
124         this.value = locals.value;
125         this._handleChange(false, "set");
126     }
127     this.__setAttrs(attrs);
128     this._render();
129 };
130
131 /**
132  * Resets the text area form value to the initial value.
133  */
134 RapidContext.Widget.TextField.prototype.reset = function () {
135     this.setAttrs({ value: this.defaultValue });
136 };
137
138 /**
139  * Returns the text field value. This function is slightly different
140  * from using the `value` property directly, since it will always
141  * return the actual value string instead of the temporary help text
142  * displayed when the text field is empty and unfocused.
143  *
144  * @return {String} the field value
145  *
146  * @example
147  * var value = field.getValue();
148  * value = MochiKit.Format.strip(value);
149  * field.setAttrs({ "value": value });
150  */
151 RapidContext.Widget.TextField.prototype.getValue = function () {
152     return (this.focused) ? this.value : this.storedValue;
153 };
154
155 /**
156  * Returns (or creates) a popup for this text field. The popup will
157  * not be shown by this method, only returned as-is. If the create
158  * flag is specified, a new popup will be created if none has been
159  * created previously.
160  *
161  * @param {Boolean} create the create popup flag
162  *
163  * @return {Widget} the popup widget, or
164  *         null if none existed or was created
165  */
166 RapidContext.Widget.TextField.prototype.popup = function (create) {
167     if (!this._popupCreated && create) {
168         this.autocomplete = "off";
169         this._popupCreated = true;
170         var dim = MochiKit.Style.getElementDimensions(this);
171         var style = { "max-height": "300px", "width": Math.max(dim.w - 5, 300) + "px" };
172         var popup = RapidContext.Widget.Popup({ style: style });
173         MochiKit.DOM.insertSiblingNodesAfter(this, popup);
174         MochiKit.Style.makePositioned(this.parentNode);
175         MochiKit.Signal.connect(this, "onkeydown", this, "_handleKeyDown");
176         MochiKit.Signal.connect(popup, "onclick", this, "_handleClick");
177     }
178     return (this._popupCreated) ? this.nextSibling : null;
179 };
180
181 /**
182  * Shows a popup for the text field containing the specified items.
183  * The items specified may be either a list of HTML DOM nodes or
184  * text strings.
185  *
186  * @param {Object} [attrs] the popup attributes to set
187  * @param {Number} [attrs.delay] the popup auto-hide delay, defaults
188  *            to 30 seconds
189  * @param {Array} [items] the items to show, or null to keep the
190  *            previous popup content
191  *
192  * @example
193  * var items = ["Cat", "Dog", "Elephant", "Zebra"];
194  * field.showPopup({}, items);
195  */
196 RapidContext.Widget.TextField.prototype.showPopup = function (attrs, items) {
197     var popup = this.popup(true);
198     if (items) {
199         popup.hide();
200         MochiKit.DOM.replaceChildNodes(popup);
201         for (var i = 0; i < items.length; i++) {
202             if (typeof(items[i]) == "string") {
203                 var node = MochiKit.DOM.DIV({ "class": "widgetPopupItem" }, items[i]);
204                 node.value = items[i];
205                 popup.appendChild(node);
206             } else {
207                 MochiKit.DOM.appendChildNodes(popup, items[i]);
208             }
209         }
210     }
211     if (popup.childNodes.length > 0) {
212         var pos = { x: this.offsetLeft + 1,
213                     y: this.offsetTop + this.offsetHeight + 1 };
214         MochiKit.Style.setElementPosition(popup, pos);
215         popup.setAttrs(MochiKit.Base.update({ delay: 30000 }, attrs));
216         popup.show();
217         if (items && items.length == 1) {
218             popup.selectChild(0);
219         }
220     }
221 };
222
223 /**
224  * Handles keypress and paste events for this this widget.
225  *
226  * @param evt the MochiKit.Signal.Event object
227  */
228 RapidContext.Widget.TextField.prototype._handleChange = function (evt, cause) {
229     if (evt) {
230         cause = "on" + evt.type();
231         setTimeout(MochiKit.Base.bind("_handleChange", this, false, cause));
232     } else if (this.storedValue != this.value) {
233         var detail = { before: this.storedValue, after: this.value, cause: cause };
234         this.storedValue = this.value;
235         RapidContext.Widget._fireEvent(this, "change", detail);
236     }
237 }
238
239 /**
240  * Handles focus and blur events for this widget.
241  *
242  * @param evt the `MochiKit.Signal.Event` object
243  */
244 RapidContext.Widget.TextField.prototype._handleFocus = function (evt) {
245     var value = this.getValue();
246     if (evt.type() == "focus") {
247         this.focused = true;
248         if (this.value != value) {
249             this.value = value
250         }
251     } else if (evt.type() == "blur") {
252         this.focused = false;
253         this.storedValue = value;
254         var popup = this.popup();
255         if (popup && !popup.isHidden()) {
256             popup.setAttrs({ delay: 250 });
257         }
258     }
259     this._render();
260 };
261
262 /**
263  * Handles the key down event for the text field.
264  *
265  * @param {Event} evt the `MochiKit.Signal.Event` object
266  */
267 RapidContext.Widget.TextField.prototype._handleKeyDown = function (evt) {
268     var popup = this.popup(false);
269     if (popup != null) {
270         popup.resetDelay();
271         if (popup.isHidden()) {
272             switch (evt.key().string) {
273             case "KEY_ESCAPE":
274                 evt.stop();
275                 break;
276             case "KEY_ARROW_UP":
277             case "KEY_ARROW_DOWN":
278                 this.showPopup();
279                 popup.selectChild(0);
280                 evt.stop();
281                 break;
282             }
283         } else {
284             switch (evt.key().string) {
285             case "KEY_TAB":
286             case "KEY_ENTER":
287                 popup.hide();
288                 evt.stop();
289                 if (popup.selectedChild() != null) {
290                     RapidContext.Widget._fireEvent(this, "dataavailable");
291                 }
292                 break;
293             case "KEY_ESCAPE":
294                 popup.hide();
295                 evt.stop();
296                 break;
297             case "KEY_ARROW_UP":
298             case "KEY_ARROW_DOWN":
299                 popup.selectMove(evt.key().string == "KEY_ARROW_UP" ? -1 : 1);
300                 evt.stop();
301                 break;
302             }
303         }
304     }
305 };
306
307 /**
308  * Handles the mouse click event on the popup.
309  *
310  * @param evt the `MochiKit.Signal.Event` object
311  */
312 RapidContext.Widget.TextField.prototype._handleClick = function (evt) {
313     this.blur();
314     this.focus();
315     RapidContext.Widget._fireEvent(this, "dataavailable")
316 };
317
318 /**
319  * Updates the display of the widget content.
320  */
321 RapidContext.Widget.TextField.prototype._render = function () {
322     var value = this.getValue();
323     var str = MochiKit.Format.strip(value);
324     if (!this.focused && str == "" && this.helpText) {
325         this.value = this.helpText;
326         this.addClass("widgetTextFieldHelp");
327     } else {
328         if (this.value != value) {
329             this.value = value;
330         }
331         this.removeClass("widgetTextFieldHelp");
332     }
333 };
334