3203459105ff61cd8647817cffa2caf458275908
[framework/web/web-ui-fw.git] / src / widgets / 000_widgetex / js / widgetex.js
1 /*
2  *
3  * This software is licensed under the MIT licence (as defined by the OSI at
4  * http://www.opensource.org/licenses/mit-license.php)
5  * 
6  * ***************************************************************************
7  * Copyright (C) 2011 by Intel Corporation Ltd.
8  * 
9  * Permission is hereby granted, free of charge, to any person obtaining a
10  * copy of this software and associated documentation files (the "Software"),
11  * to deal in the Software without restriction, including without limitation
12  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13  * and/or sell copies of the Software, and to permit persons to whom the
14  * Software is furnished to do so, subject to the following conditions:
15  * 
16  * The above copyright notice and this permission notice shall be included in
17  * all copies or substantial portions of the Software.
18  * 
19  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25  * DEALINGS IN THE SOFTWARE.
26  * ***************************************************************************
27  */
28
29 // Base class for widgets that need the following features:
30 //
31 // I. HTML prototype loading
32 //
33 // This class provides HTML prototype loading for widgets. That is, the widget implementation specifies its HTML portions
34 // in one continuous HTML snippet, and it optionally provides an object containing selectors into the various parts of the
35 // HTML snippet. This widget loads the HTML snippet into a jQuery object, and optionally assigns jQuery objects to each of
36 // the selectors in the optionally provided object.
37 //
38 // To use this functionality you can either derive from this class, or you can call its prototype's gtype method.
39 //
40 // 1. Widgets deriving from this class should define _htmlProto as part of their prototype declaration. _htmlProto looks like
41 // this:
42 //
43 // _htmlProto: {
44 //     source: string|jQuery object (optional) default: string - The name of the widget
45 //     ui: {
46 //         uiElement1: "#ui-element-1-selector",
47 //         uiElement2: "#ui-element-2-selector",
48 //         ...
49 //         subElement: {
50 //             subElement1: "#sub-element-1-selector",
51 //             subElement2: "#sub-element-2-selector",
52 //             ...
53 //         }
54 //         ...
55 //     }
56 // }
57 //
58 // If neither 'source' nor 'ui' are defined, you must still include an empty _htmlProto key (_htmlProto: {}) to indicate
59 // that you wish to make use of this feature. This will cause a prototype HTML file named after your widget to be loaded.
60 // The loaded prototype will be placed into your widget's prototype's _protoHtml.source key.
61 //
62 // If 'source' is defined as a string, it is the name of the widget (including namespace). This is the default. If your
63 // widget's HTML prototype is loaded via AJAX and the name of the AJAX file is different from the name of your widget
64 // (that is, it is not "<widgetName>.prototype.html", then you should explicitly define 'source' as:
65 //
66 // If you wish to load HTML prototypes via AJAX, modify the getProtoPath() function defined below to reflect the directory
67 // structure holding your widget HTML prototypes.
68 //
69 // source: "alternateWidgetName"
70 //
71 // If AJAX loading fails, source is set to a jQuery object containing a div with an error message. You can check whether
72 // loading failed via the jQuery object's jqmData("tizen.widgetex.ajax.fail") data item. If false, then the jQuery object
73 // is the actual prototype loaded via AJAX or present inline. Otherwise, the jQuery object is the error message div.
74 //
75 // If 'source' is defined as a jQuery object, it is considered already loaded.
76 //
77 // if 'ui' is defined inside _htmlProto, It is assumed to be an object such that every one of its keys is either a string,
78 // or another object with the same properties as itself.
79 //
80 // When a widget is instantiated, the HTML prototype is loaded if not already present in the prototype. If 'ui' is present
81 // inside _htmlProto, the prototype is cloned. Then, a new structure is created based on 'ui' with each selector replaced
82 // by a jQuery object containing the results of performing .find() on the prototype's clone with the filter set to the
83 // value of the string. In the special case where the selector starts with a '#', the ID is removed from the element after
84 // it is assigned into the structure being created. This structure is then made accessible from the widget instance via
85 // the '_ui' key (i.e., this._ui).
86 //
87 // 2. Use the loadPrototype method when your widget does not derive from $.tizen.widgetex:
88 // Add _htmlProto to your widget's prototype as described above. Then, in your widget's _create() method, call
89 // loadPrototype in the following manner:
90 //
91 // $.tizen.widgetex.loadPrototype.call(this, "namespace.widgetName");
92 //
93 // Thereafter, you may use the HTML prototype from your widget's prototype or, if you have specified a 'ui' key in your
94 // _htmlProto key, you may use this._ui from your widget instance.
95 //
96 // II. realize method
97 //
98 // When a widget is created, some of its properties cannot be set immediately, because they depend on the widths/heights
99 // of its constituent elements. They can only be calculated when the page containing the widget is made visible via the
100 // "pageshow" event, because widths/heights always evaluate to 0 when retrieved from a widget that is not visible. When
101 // you inherit from widgetex, you can add a "_realize" function to your prototype. This function will be called once right
102 // after _create() if the element that anchors your widget is on a visible page. Otherwise, it will be called when the
103 // page to which the widget belongs emits the "pageshow" event.
104 //
105 // NB: If your widget is inside a container which is itself not visible, such as an expandable or a collapsible, your
106 // widget will remain hidden even though "pageshow" is fired and therefore _realize is called. In this case, widths and
107 // heights will be unreliable even during _realize.
108 //
109 // III. systematic option handling
110 //
111 // If a widget has lots of options, the _setOption function can become a long switch for setting each recognized option.
112 // It is also tempting to allow options to determine the way a widget is created, by basing decisions on various options
113 // during _create(). Often, the actions based on option values in _create() are the same as those in _setOption. To avoid
114 // such code duplication, this class calls _setOption once for each option after _create() has completed.
115 //
116 // Furthermore, to avoid writing long switches in a widget's _setOption method, this class implements _setOption in such
117 // a way that, for any given option (e.g. "myOption"), _setOption looks for a method _setMyOption in the widget's
118 // implementation, and if found, calls the method with the value of the option.
119 //
120 // If your widget does not inherit from widgetex, you can still use widgetex' systematic option handling:
121 // 1. define the _setOption method for your widget as follows:
122 //      _setOption: $.tizen.widgetex.prototype._setOption
123 // 2. Call this._setOptions(this.options) from your widget's _create() function.
124 // 3. As with widgetex-derived widgets, implement a corresponding _setMyOptionName function for each option myOptionName
125 // you wish to handle.
126 //
127 // IV. systematic value handling for input elements
128 //
129 // If your widget happens to be constructed from an <input> element, you have to handle the "value" attribute specially,
130 // and you have to emit the "change" signal whenever it changes, in addition to your widget's normal signals and option
131 // changes. With widgetex, you can assign one of your widget's "data-*" properties to be synchronized to the "value"
132 // property whenever your widget is constructed onto an <input> element. To do this, define, in your prototype:
133 //
134 // _value: {
135 //      attr: "data-my-attribute",
136 //      signal: "signal-to-emit"
137 // }
138 //
139 // Then, call this._setValue(newValue) whenever you wish to set the value for your widget. This will set the data-*
140 // attribute, emit the custom signal (if set) with the new value as its parameter, and, if the widget is based on an
141 // <input> element, it will also set the "value" attribute and emit the "change" signal.
142 //
143 // "attr" is required if you choose to define "_value", and identifies the data-attribute to set in addition to "value",
144 // if your widget's element is an input.
145 // "signal" is optional, and will be emitted when setting the data-attribute via this._setValue(newValue).
146 //
147 // If your widget does not derive from widgetex, you can still define "_value" as described above and call
148 // $.tizen.widgetex.setValue(widget, newValue).
149 //
150 // V. Systematic enabled/disabled handling for input elements
151 //
152 // widgetex implements _setDisabled which will disable the input associated with this widget, if any. Thus, if you derive
153 // from widgetex and you plan on implementing the disabled state, you should chain up to
154 // $.tizen.widgetex.prototype._setDisabled(value), rather than $.Widget.prototype._setOption("disabled", value).
155
156 (function($, undefined) {
157
158 // Framework-specific HTML prototype path for AJAX loads
159 function getProtoPath() {
160     var theScriptTag = $("script[data-framework-version][data-framework-root][data-framework-theme]");
161
162     return (theScriptTag.attr("data-framework-root")    + "/" +
163             theScriptTag.attr("data-framework-version") + "/themes/" + 
164             theScriptTag.attr("data-framework-theme")   + "/proto-html");
165 }
166
167 $.widget("tizen.widgetex", $.mobile.widget, {
168     _createWidget: function() {
169         $.tizen.widgetex.loadPrototype.call(this, this.namespace + "." + this.widgetName);
170         $.mobile.widget.prototype._createWidget.apply(this, arguments);
171     },
172
173     _init: function() {
174         var page = this.element.closest(".ui-page"),
175             self = this,
176             myOptions = {};
177
178         if (page.is(":visible"))
179             this._realize();
180         else
181             page.bind("pageshow", function() { self._realize(); });
182
183         $.extend(myOptions, this.options);
184
185         this.options = {};
186
187         this._setOptions(myOptions);
188     },
189
190     _getCreateOptions: function() {
191         // if we're dealing with an <input> element, value takes precedence over corresponding data-* attribute, if a
192         // mapping has been established via this._value. So, assign the value to the data-* attribute, so that it may
193         // then be assigned to this.options in the superclass' _getCreateOptions
194
195         if (this.element.is("input") && this._value !== undefined) {
196             var theValue =
197                 ((this.element.attr("type") === "checkbox" || this.element.attr("type") === "radio")
198                     ? this.element.is(":checked")
199                     : this.element.is("[value]")
200                         ? this.element.attr("value")
201                         : undefined);
202
203             if (theValue != undefined)
204                 this.element.attr(this._value.attr, theValue);
205         }
206
207         return $.mobile.widget.prototype._getCreateOptions.apply(this, arguments);
208     },
209
210     _setOption: function(key, value) {
211         var setter = "_set" + key.replace(/^[a-z]/, function(c) {return c.toUpperCase();});
212
213         if (this[setter] !== undefined)
214             this[setter](value);
215         else
216             $.mobile.widget.prototype._setOption.apply(this, arguments);
217     },
218
219     _setDisabled: function(value) {
220         $.Widget.prototype._setOption.call(this, "disabled", value);
221         if (this.element.is("input"))
222             this.element.attr("disabled", value);
223     },
224
225     _setValue: function(newValue) {
226         $.tizen.widgetex.setValue(this, newValue);
227     },
228
229     _realize: function() {}
230 });
231
232 $.tizen.widgetex.setValue = function(widget, newValue) {
233     if (widget._value !== undefined) {
234         var valueString = widget._value.makeString ? widget._value.makeString(newValue) : newValue;
235
236         widget.element.attr(widget._value.attr, valueString);
237         if (widget._value.signal !== undefined)
238             widget.element.triggerHandler(widget._value.signal, newValue);
239         if (widget.element.is("input")) {
240             var inputType = widget.element.attr("type");
241
242             // Special handling for checkboxes and radio buttons, where the presence of the "checked" attribute is really
243             // the value
244             if (inputType === "checkbox" || inputType === "radio") {
245                 if (newValue)
246                     widget.element.attr("checked", true);
247                 else
248                     widget.element.removeAttr("checked");
249             }
250             else
251                 widget.element.attr("value", valueString);
252             widget.element.trigger("change");
253         }
254     }
255 };
256
257 $.tizen.widgetex.assignElements = function(proto, obj) {
258     var ret = {};
259     for (var key in obj)
260         if ((typeof obj[key]) === "string") {
261             ret[key] = proto.find(obj[key]);
262             if (obj[key].match(/^#/))
263                 ret[key].removeAttr("id");
264         }
265         else
266         if ((typeof obj[key]) === "object")
267             ret[key] = $.tizen.widgetex.assignElements(proto, obj[key]);
268     return ret;
269 }
270
271 $.tizen.widgetex.loadPrototype = function(widget, ui) {
272     var ar = widget.split(".");
273
274     if (ar.length == 2) {
275         var namespace = ar[0],
276             widgetName = ar[1];
277
278         var htmlProto = $("<div></div>")
279                 .text("Failed to load proto for widget " + namespace + "." + widgetName + "!")
280                 .css({background: "red", color: "blue", border: "1px solid black"})
281                 .jqmData("tizen.widgetex.ajax.fail", true);
282
283         // If htmlProto is defined
284         if ($[namespace][widgetName].prototype._htmlProto !== undefined) {
285             // If no source is defined, use the widget name
286             if ($[namespace][widgetName].prototype._htmlProto.source === undefined)
287                 $[namespace][widgetName].prototype._htmlProto.source = widgetName;
288
289             // Load the HTML prototype via AJAX if not defined inline
290             if (typeof $[namespace][widgetName].prototype._htmlProto.source === "string") {
291                 // Establish the path for the proto file
292                     widget = $[namespace][widgetName].prototype._htmlProto.source,
293                     protoPath = getProtoPath();
294
295                 // Make the AJAX call
296                 $.ajax({
297                     url: protoPath + "/" + widget + ".prototype.html",
298                     async: false,
299                     dataType: "html"
300                 }).success(function(data, textStatus, jqXHR) {
301                     htmlProto = $("<div></div>").html(data).jqmData("tizen.widgetex.ajax.fail", false);
302                 });
303
304                 // Assign the HTML proto to the widget prototype
305                 $[namespace][widgetName].prototype._htmlProto.source = htmlProto;
306             }
307             // Otherwise, use the inline definition
308             else {
309                 // AJAX loading has trivially succeeded, since there was no AJAX loading at all
310                 $[namespace][widgetName].prototype._htmlProto.source.jqmData("tizen.widgetex.ajax.fail", false);
311                 htmlProto = $[namespace][widgetName].prototype._htmlProto.source;
312             }
313
314             // If there's a "ui" portion in the HTML proto, copy it over to this instance, and
315             // replace the selectors with the selected elements from a copy of the HTML prototype
316             if ($[namespace][widgetName].prototype._htmlProto.ui !== undefined) {
317                 // Assign the relevant parts of the proto
318                 $.extend(this, {
319                     _ui: $.tizen.widgetex.assignElements(htmlProto.clone(), $[namespace][widgetName].prototype._htmlProto.ui)
320                 });
321             }
322         }
323     }
324 };
325
326 })(jQuery);