|
1 <!DOCTYPE html> |
|
2 <html lang="en"> |
|
3 <head> |
|
4 <meta charset="utf-8"> |
|
5 <title>Example: Extending the Base Widget Class</title> |
|
6 <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=PT+Sans:400,700,400italic,700italic"> |
|
7 <link rel="stylesheet" href="../../build/cssgrids/cssgrids-min.css"> |
|
8 <link rel="stylesheet" href="../assets/css/main.css"> |
|
9 <link rel="stylesheet" href="../assets/vendor/prettify/prettify-min.css"> |
|
10 <link rel="shortcut icon" type="image/png" href="../assets/favicon.png"> |
|
11 <script src="../../build/yui/yui-min.js"></script> |
|
12 |
|
13 </head> |
|
14 <body> |
|
15 <!-- |
|
16 <a href="https://github.com/yui/yui3"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a> |
|
17 --> |
|
18 <div id="doc"> |
|
19 <div id="hd"> |
|
20 <h1><img src="http://yuilibrary.com/img/yui-logo.png"></h1> |
|
21 </div> |
|
22 |
|
23 |
|
24 <h1>Example: Extending the Base Widget Class</h1> |
|
25 <div class="yui3-g"> |
|
26 <div class="yui3-u-3-4"> |
|
27 <div id="main"> |
|
28 <div class="content"><style type="text/css" scoped> |
|
29 .yui3-js-enabled .yui3-spinner-loading { |
|
30 display:none; |
|
31 } |
|
32 |
|
33 .yui3-spinner-hidden { |
|
34 display:none; |
|
35 } |
|
36 |
|
37 .yui3-spinner { |
|
38 display:-moz-inline-stack; |
|
39 display:inline-block; |
|
40 zoom:1; |
|
41 *display:inline; |
|
42 vertical-align:middle; |
|
43 } |
|
44 |
|
45 .yui3-spinner-content { |
|
46 padding:1px; |
|
47 position:relative; |
|
48 } |
|
49 |
|
50 .yui3-spinner-value { |
|
51 width:2em; |
|
52 height:1.5em; |
|
53 text-align:right; |
|
54 margin-right:22px; |
|
55 vertical-align:top; |
|
56 border:1px solid #000; |
|
57 padding:2px; |
|
58 } |
|
59 |
|
60 .yui3-spinner-increment, .yui3-spinner-decrement { |
|
61 position:absolute; |
|
62 height:1em; |
|
63 width:22px; |
|
64 overflow:hidden; |
|
65 text-indent:-10em; |
|
66 border:1px solid #999; |
|
67 margin:0; |
|
68 padding:0px; |
|
69 } |
|
70 |
|
71 .yui3-spinner-increment { |
|
72 top:1px; |
|
73 *top:2px; |
|
74 right:1px; |
|
75 background:#ddd url(../assets/widget/arrows.png) no-repeat 50% 0px; |
|
76 } |
|
77 |
|
78 .yui3-spinner-decrement { |
|
79 bottom:1px; |
|
80 *bottom:2px; |
|
81 right:1px; |
|
82 background:#ddd url(../assets/widget/arrows.png) no-repeat 50% -20px; |
|
83 } |
|
84 |
|
85 #widget-extend-example { |
|
86 padding:5px; |
|
87 } |
|
88 |
|
89 #widget-extend-example .hint { |
|
90 margin-top:10px; |
|
91 font-size:85%; |
|
92 color:#00a; |
|
93 } |
|
94 |
|
95 </style> |
|
96 |
|
97 <div class="intro"> |
|
98 <p>This example shows how to extend the base <code>Widget</code> class to create a simple, re-usable spinner control. The <code>Spinner</code> class created in the example is not intended to be a fully featured spinner. It is used here as a concrete example, to convey some of the key concepts to keep in mind when extending the <code>Widget</code> class.</p> |
|
99 </div> |
|
100 |
|
101 <div class="example"> |
|
102 <div id="widget-extend-example"> |
|
103 A basic spinner widget: <input type="text" id="numberField" class="yui3-spinner-loading" value="20" /> |
|
104 <p class="hint">Click the buttons, or the arrow up/down and page up/down keys on your keyboard to change the spinner's value</p> |
|
105 </div> |
|
106 |
|
107 <script type="text/javascript"> |
|
108 YUI().use("event-key", "widget", function(Y) { |
|
109 |
|
110 var Lang = Y.Lang, |
|
111 Widget = Y.Widget, |
|
112 Node = Y.Node; |
|
113 |
|
114 /* Spinner class constructor */ |
|
115 function Spinner(config) { |
|
116 Spinner.superclass.constructor.apply(this, arguments); |
|
117 } |
|
118 |
|
119 /* |
|
120 * Required NAME static field, to identify the Widget class and |
|
121 * used as an event prefix, to generate class names etc. (set to the |
|
122 * class name in camel case). |
|
123 */ |
|
124 Spinner.NAME = "spinner"; |
|
125 |
|
126 /* |
|
127 * The attribute configuration for the Spinner widget. Attributes can be |
|
128 * defined with default values, get/set functions and validator functions |
|
129 * as with any other class extending Base. |
|
130 */ |
|
131 Spinner.ATTRS = { |
|
132 // The minimum value for the spinner. |
|
133 min : { |
|
134 value:0 |
|
135 }, |
|
136 |
|
137 // The maximum value for the spinner. |
|
138 max : { |
|
139 value:100 |
|
140 }, |
|
141 |
|
142 // The current value of the spinner. |
|
143 value : { |
|
144 value:0, |
|
145 validator: function(val) { |
|
146 return this._validateValue(val); |
|
147 } |
|
148 }, |
|
149 |
|
150 // Amount to increment/decrement the spinner when the buttons or arrow up/down keys are pressed. |
|
151 minorStep : { |
|
152 value:1 |
|
153 }, |
|
154 |
|
155 // Amount to increment/decrement the spinner when the page up/down keys are pressed. |
|
156 majorStep : { |
|
157 value:10 |
|
158 }, |
|
159 |
|
160 // override default ("null"), required for focus() |
|
161 tabIndex: { |
|
162 value: 0 |
|
163 }, |
|
164 |
|
165 // The strings for the spinner UI. This attribute is |
|
166 // defined by the base Widget class but has an empty value. The |
|
167 // spinner is simply providing a default value for the attribute. |
|
168 strings: { |
|
169 value: { |
|
170 tooltip: "Press the arrow up/down keys for minor increments, page up/down for major increments.", |
|
171 increment: "Increment", |
|
172 decrement: "Decrement" |
|
173 } |
|
174 } |
|
175 }; |
|
176 |
|
177 /* Static constant used to identify the classname applied to the spinners value field */ |
|
178 Spinner.INPUT_CLASS = Y.ClassNameManager.getClassName(Spinner.NAME, "value"); |
|
179 |
|
180 /* Static constants used to define the markup templates used to create Spinner DOM elements */ |
|
181 Spinner.INPUT_TEMPLATE = '<input type="text" class="' + Spinner.INPUT_CLASS + '">'; |
|
182 Spinner.BTN_TEMPLATE = '<button type="button"></button>'; |
|
183 |
|
184 /* |
|
185 * The HTML_PARSER static constant is used by the Widget base class to populate |
|
186 * the configuration for the spinner instance from markup already on the page. |
|
187 * |
|
188 * The Spinner class attempts to set the value of the spinner widget if it |
|
189 * finds the appropriate input element on the page. |
|
190 */ |
|
191 Spinner.HTML_PARSER = { |
|
192 value: function (srcNode) { |
|
193 var val = parseInt(srcNode.get("value")); |
|
194 return Y.Lang.isNumber(val) ? val : null; |
|
195 } |
|
196 }; |
|
197 |
|
198 /* Spinner extends the base Widget class */ |
|
199 Y.extend(Spinner, Widget, { |
|
200 |
|
201 /* |
|
202 * initializer is part of the lifecycle introduced by |
|
203 * the Widget class. It is invoked during construction, |
|
204 * and can be used to setup instance specific state. |
|
205 * |
|
206 * The Spinner class does not need to perform anything |
|
207 * specific in this method, but it is left in as an example. |
|
208 */ |
|
209 initializer: function() { |
|
210 // Not doing anything special during initialization |
|
211 }, |
|
212 |
|
213 /* |
|
214 * destructor is part of the lifecycle introduced by |
|
215 * the Widget class. It is invoked during destruction, |
|
216 * and can be used to cleanup instance specific state. |
|
217 * |
|
218 * The spinner cleans up any node references it's holding |
|
219 * onto. The Widget classes destructor will purge the |
|
220 * widget's bounding box of event listeners, so spinner |
|
221 * only needs to clean up listeners it attaches outside of |
|
222 * the bounding box. |
|
223 */ |
|
224 destructor : function() { |
|
225 this._documentMouseUpHandle.detach(); |
|
226 |
|
227 this.inputNode = null; |
|
228 this.incrementNode = null; |
|
229 this.decrementNode = null; |
|
230 }, |
|
231 |
|
232 /* |
|
233 * renderUI is part of the lifecycle introduced by the |
|
234 * Widget class. Widget's renderer method invokes: |
|
235 * |
|
236 * renderUI() |
|
237 * bindUI() |
|
238 * syncUI() |
|
239 * |
|
240 * renderUI is intended to be used by the Widget subclass |
|
241 * to create or insert new elements into the DOM. |
|
242 * |
|
243 * For spinner the method adds the input (if it's not already |
|
244 * present in the markup), and creates the inc/dec buttons |
|
245 */ |
|
246 renderUI : function() { |
|
247 this._renderInput(); |
|
248 this._renderButtons(); |
|
249 }, |
|
250 |
|
251 /* |
|
252 * bindUI is intended to be used by the Widget subclass |
|
253 * to bind any event listeners which will drive the Widget UI. |
|
254 * |
|
255 * It will generally bind event listeners for attribute change |
|
256 * events, to update the state of the rendered UI in response |
|
257 * to attribute value changes, and also attach any DOM events, |
|
258 * to activate the UI. |
|
259 * |
|
260 * For spinner, the method: |
|
261 * |
|
262 * - Sets up the attribute change listener for the "value" attribute |
|
263 * |
|
264 * - Binds key listeners for the arrow/page keys |
|
265 * - Binds mouseup/down listeners on the boundingBox, document respectively. |
|
266 * - Binds a simple change listener on the input box. |
|
267 */ |
|
268 bindUI : function() { |
|
269 this.after("valueChange", this._afterValueChange); |
|
270 |
|
271 var boundingBox = this.get("boundingBox"); |
|
272 |
|
273 // Looking for a key event which will fire continuously across browsers while the key is held down. 38, 40 = arrow up/down, 33, 34 = page up/down |
|
274 var keyEventSpec = (!Y.UA.opera) ? "down:" : "press:"; |
|
275 keyEventSpec += "38, 40, 33, 34"; |
|
276 |
|
277 Y.on("key", Y.bind(this._onDirectionKey, this), boundingBox, keyEventSpec); |
|
278 Y.on("mousedown", Y.bind(this._onMouseDown, this), boundingBox); |
|
279 |
|
280 this._documentMouseUpHandle = Y.on("mouseup", Y.bind(this._onDocMouseUp, this), boundingBox.get("ownerDocument")); |
|
281 |
|
282 Y.on("change", Y.bind(this._onInputChange, this), this.inputNode); |
|
283 }, |
|
284 |
|
285 /* |
|
286 * syncUI is intended to be used by the Widget subclass to |
|
287 * update the UI to reflect the current state of the widget. |
|
288 * |
|
289 * For spinner, the method sets the value of the input field, |
|
290 * to match the current state of the value attribute. |
|
291 */ |
|
292 syncUI : function() { |
|
293 this._uiSetValue(this.get("value")); |
|
294 }, |
|
295 |
|
296 /* |
|
297 * Creates the input control for the spinner and adds it to |
|
298 * the widget's content box, if not already in the markup. |
|
299 */ |
|
300 _renderInput : function() { |
|
301 var contentBox = this.get("contentBox"), |
|
302 input = contentBox.one("." + Spinner.INPUT_CLASS), |
|
303 strings = this.get("strings"); |
|
304 |
|
305 if (!input) { |
|
306 input = Node.create(Spinner.INPUT_TEMPLATE); |
|
307 contentBox.appendChild(input); |
|
308 } |
|
309 |
|
310 input.set("title", strings.tooltip); |
|
311 this.inputNode = input; |
|
312 }, |
|
313 |
|
314 /* |
|
315 * Creates the button controls for the spinner and add them to |
|
316 * the widget's content box, if not already in the markup. |
|
317 */ |
|
318 _renderButtons : function() { |
|
319 var contentBox = this.get("contentBox"), |
|
320 strings = this.get("strings"); |
|
321 |
|
322 var inc = this._createButton(strings.increment, this.getClassName("increment")); |
|
323 var dec = this._createButton(strings.decrement, this.getClassName("decrement")); |
|
324 |
|
325 this.incrementNode = contentBox.appendChild(inc); |
|
326 this.decrementNode = contentBox.appendChild(dec); |
|
327 }, |
|
328 |
|
329 /* |
|
330 * Utility method, to create a spinner button |
|
331 */ |
|
332 _createButton : function(text, className) { |
|
333 |
|
334 var btn = Y.Node.create(Spinner.BTN_TEMPLATE); |
|
335 btn.set("innerHTML", text); |
|
336 btn.set("title", text); |
|
337 btn.addClass(className); |
|
338 |
|
339 return btn; |
|
340 }, |
|
341 |
|
342 /* |
|
343 * Bounding box mouse down handler. Will determine if the mouse down |
|
344 * is on one of the spinner buttons, and increment/decrement the value |
|
345 * accordingly. |
|
346 * |
|
347 * The method also sets up a timer, to support the user holding the mouse |
|
348 * down on the spinner buttons. The timer is cleared when a mouse up event |
|
349 * is detected. |
|
350 */ |
|
351 _onMouseDown : function(e) { |
|
352 var node = e.target, |
|
353 dir, |
|
354 handled = false, |
|
355 currVal = this.get("value"), |
|
356 minorStep = this.get("minorStep"); |
|
357 |
|
358 if (node.hasClass(this.getClassName("increment"))) { |
|
359 this.set("value", currVal + minorStep); |
|
360 dir = 1; |
|
361 handled = true; |
|
362 } else if (node.hasClass(this.getClassName("decrement"))) { |
|
363 this.set("value", currVal - minorStep); |
|
364 dir = -1; |
|
365 handled = true; |
|
366 } |
|
367 |
|
368 if (handled) { |
|
369 this._setMouseDownTimers(dir, minorStep); |
|
370 } |
|
371 }, |
|
372 |
|
373 /* |
|
374 * Override the default content box value, since we don't want the srcNode |
|
375 * to be the content box for spinner. |
|
376 */ |
|
377 _defaultCB : function() { |
|
378 return null; |
|
379 }, |
|
380 |
|
381 /* |
|
382 * Document mouse up handler. Clears the timers supporting |
|
383 * the "mouse held down" behavior. |
|
384 */ |
|
385 _onDocMouseUp : function(e) { |
|
386 this._clearMouseDownTimers(); |
|
387 }, |
|
388 |
|
389 /* |
|
390 * Bounding box Arrow up/down, Page up/down key listener. |
|
391 * |
|
392 * Increments/Decrement the spinner value, based on the key pressed. |
|
393 */ |
|
394 _onDirectionKey : function(e) { |
|
395 |
|
396 e.preventDefault(); |
|
397 |
|
398 var currVal = this.get("value"), |
|
399 newVal = currVal, |
|
400 minorStep = this.get("minorStep"), |
|
401 majorStep = this.get("majorStep"); |
|
402 |
|
403 switch (e.charCode) { |
|
404 case 38: |
|
405 newVal += minorStep; |
|
406 break; |
|
407 case 40: |
|
408 newVal -= minorStep; |
|
409 break; |
|
410 case 33: |
|
411 newVal += majorStep; |
|
412 newVal = Math.min(newVal, this.get("max")); |
|
413 break; |
|
414 case 34: |
|
415 newVal -= majorStep; |
|
416 newVal = Math.max(newVal, this.get("min")); |
|
417 break; |
|
418 } |
|
419 |
|
420 if (newVal !== currVal) { |
|
421 this.set("value", newVal); |
|
422 } |
|
423 }, |
|
424 |
|
425 /* |
|
426 * Simple change handler, to make sure user does not input an invalid value |
|
427 */ |
|
428 _onInputChange : function(e) { |
|
429 if (!this._validateValue(this.inputNode.get("value"))) { |
|
430 this.syncUI(); |
|
431 } |
|
432 }, |
|
433 |
|
434 /* |
|
435 * Initiates mouse down timers, to increment slider, while mouse button |
|
436 * is held down |
|
437 */ |
|
438 _setMouseDownTimers : function(dir, step) { |
|
439 this._mouseDownTimer = Y.later(500, this, function() { |
|
440 this._mousePressTimer = Y.later(100, this, function() { |
|
441 this.set("value", this.get("value") + (dir * step)); |
|
442 }, null, true) |
|
443 }); |
|
444 }, |
|
445 |
|
446 /* |
|
447 * Clears timers used to support the "mouse held down" behavior |
|
448 */ |
|
449 _clearMouseDownTimers : function() { |
|
450 if (this._mouseDownTimer) { |
|
451 this._mouseDownTimer.cancel(); |
|
452 this._mouseDownTimer = null; |
|
453 } |
|
454 if (this._mousePressTimer) { |
|
455 this._mousePressTimer.cancel(); |
|
456 this._mousePressTimer = null; |
|
457 } |
|
458 }, |
|
459 |
|
460 /* |
|
461 * value attribute change listener. Updates the |
|
462 * value in the rendered input box, whenever the |
|
463 * attribute value changes. |
|
464 */ |
|
465 _afterValueChange : function(e) { |
|
466 this._uiSetValue(e.newVal); |
|
467 }, |
|
468 |
|
469 /* |
|
470 * Updates the value of the input box to reflect |
|
471 * the value passed in |
|
472 */ |
|
473 _uiSetValue : function(val) { |
|
474 this.inputNode.set("value", val); |
|
475 }, |
|
476 |
|
477 /* |
|
478 * value attribute default validator. Verifies that |
|
479 * the value being set lies between the min/max value |
|
480 */ |
|
481 _validateValue: function(val) { |
|
482 var min = this.get("min"), |
|
483 max = this.get("max"); |
|
484 |
|
485 return (Lang.isNumber(val) && val >= min && val <= max); |
|
486 } |
|
487 }); |
|
488 |
|
489 // Create a new Spinner instance, drawing it's |
|
490 // starting value from an input field already on the |
|
491 // page (the #numberField text box) |
|
492 var spinner = new Spinner({ |
|
493 srcNode: "#numberField", |
|
494 max:100, |
|
495 min:0 |
|
496 }); |
|
497 spinner.render(); |
|
498 spinner.focus(); |
|
499 }); |
|
500 </script> |
|
501 |
|
502 </div> |
|
503 |
|
504 <h2>Extending The <code>Widget</code> Class</h2> |
|
505 |
|
506 <h3>Basic Class Structure</h3> |
|
507 |
|
508 <p>Widgets classes follow the general pattern implemented by the <code>Spinner</code> class, shown in the code snippet below. The basic pattern for setting up a new widget class involves:</p> |
|
509 |
|
510 <ol> |
|
511 <li>Defining the constructor function for the new widget class, which invokes the superclass constructor to kick off the initialization chain <em>(line 2)</em></li> |
|
512 <li>Defining the static <code>NAME</code> property for the class, which is normally the class name in camel case, and is used to prefix events and CSS classes fired/created by the class <em>(line 11)</em></li> |
|
513 <li>Defining the static <code>ATTRS</code> property for the class, which defines the set of attributes which the class will introduce, in addition to the superclass attributes <em>(line 18-57)</em></li> |
|
514 <li>Extending the <code>Widget</code> class, and adding/overriding any prototype properties/methods <em>(line 61)</em></li> |
|
515 </ol> |
|
516 |
|
517 <pre class="code prettyprint">/* Spinner class constructor */ |
|
518 function Spinner(config) { |
|
519 Spinner.superclass.constructor.apply(this, arguments); |
|
520 } |
|
521 |
|
522 /* |
|
523 * Required NAME static field, to identify the Widget class and |
|
524 * used as an event prefix, to generate class names etc. (set to the |
|
525 * class name in camel case). |
|
526 */ |
|
527 Spinner.NAME = "spinner"; |
|
528 |
|
529 /* |
|
530 * The attribute configuration for the Spinner widget. Attributes can be |
|
531 * defined with default values, get/set functions and validator functions |
|
532 * as with any other class extending Base. |
|
533 */ |
|
534 Spinner.ATTRS = { |
|
535 // The minimum value for the spinner. |
|
536 min : { |
|
537 value:0 |
|
538 }, |
|
539 |
|
540 // The maximum value for the spinner. |
|
541 max : { |
|
542 value:100 |
|
543 }, |
|
544 |
|
545 // The current value of the spinner. |
|
546 value : { |
|
547 value:0, |
|
548 validator: function(val) { |
|
549 return this._validateValue(val); |
|
550 } |
|
551 }, |
|
552 |
|
553 // Amount to increment/decrement the spinner when the buttons, |
|
554 // or arrow up/down keys are pressed. |
|
555 minorStep : { |
|
556 value:1 |
|
557 }, |
|
558 |
|
559 // Amount to increment/decrement the spinner when the page up/down keys are pressed. |
|
560 majorStep : { |
|
561 value:10 |
|
562 }, |
|
563 |
|
564 // The localizable strings for the spinner. This attribute is |
|
565 // defined by the base Widget class but has an empty value. The |
|
566 // spinner is simply providing a default value for the attribute. |
|
567 strings: { |
|
568 value: { |
|
569 tooltip: "Press the arrow up/down keys for minor increments, \ |
|
570 page up/down for major increments.", |
|
571 increment: "Increment", |
|
572 decrement: "Decrement" |
|
573 } |
|
574 } |
|
575 }; |
|
576 |
|
577 Y.extend(Spinner, Widget, { |
|
578 // Methods/properties to add to the prototype of the new class |
|
579 ... |
|
580 });</pre> |
|
581 |
|
582 |
|
583 <p>Note that these steps are the same for any class which is derived from <a href="../base/index.html"><code>Base</code></a>, nothing Widget-specific is involved yet. |
|
584 Widget adds the concept of a rendered UI to the existing Base lifecycle (viz. init, destroy and attribute state configuration), which we'll see show up in Widget-specific areas below.</p> |
|
585 |
|
586 <h4>A Note On Externalizing Strings and Internationalization</h4> |
|
587 |
|
588 <p>For the scope of this example we won't get into packaging your custom widget code as a YUI module, but when you do, you can leverage the <a href="../intl/index.html">Internationalization</a> infrastructure |
|
589 to bundle the strings for your widget separately from the code, and avoid hard-coding them into the widget's code base as we're doing above.</p> |
|
590 |
|
591 <p>When packaged separately from the code, the <code>strings</code> attribute can be changed to be:</p> |
|
592 |
|
593 <pre class="code prettyprint">Spinner.ATTRS = { |
|
594 ... |
|
595 strings : { |
|
596 valueFn : function() { |
|
597 return Y.Intl.get("myspinner"); // Assuming "myspinner" is the name of your widget's module. |
|
598 } |
|
599 } |
|
600 ... |
|
601 }</pre> |
|
602 |
|
603 |
|
604 <p>Loader will deliver the language specific bundle for your widget along with the code when someone uses your <code>myspinner</code> module. The language specific strings can be retrieved through the <code>Y.Intl.get(modulename)</code> call.</p> |
|
605 |
|
606 <p>The <a href="../intl/intl-basic.html">Language Resource Bundles</a> example goes into more detail about the structure of the langauge bundles, how they are built and how to configure your YUI instance to deliver them. <a href="https://github.com/yui/yui3/tree/master/build/calendar-base">Calendar's source code</a> is also a good example of how this infrastructure is used.</p> |
|
607 |
|
608 <h3>The HTML_PARSER Property</h3> |
|
609 |
|
610 <p> |
|
611 The first Widget-specific property <code>Spinner</code> implements is the static <a href="http://yuilibrary.com/yui/docs/api/Widget.html#property_Widget.HTML_PARSER"><code>HTML_PARSER</code></a> property. It is used to set the initial widget configuration based on markup, providing basic progressive enhancement support. |
|
612 </p> |
|
613 <p> |
|
614 The value of the <code>HTML_PARSER</code> property is an object literal, where each property is a widget attribute name, and the value is either a selector string (if the attribute is a node reference) or a function which is executed to provide |
|
615 a value for the attribute from the markup on the page. Markup is essentially thought of as an additional data source for the user to set initial attribute values, outside of the configuration object passed to the constructor |
|
616 <em>(values passed to the constructor will take precedence over values picked up from markup)</em>. |
|
617 </p> |
|
618 |
|
619 <p>For <code>Spinner</code>, <code>HTML_PARSER</code> defines a function for the <code>value</code> attribute, which sets the initial value of the spinner based on an input field if present in the markup.</p> |
|
620 |
|
621 <pre class="code prettyprint">/* |
|
622 * The HTML_PARSER static constant is used by the Widget base class to populate |
|
623 * the configuration for the spinner instance from markup already on the page. |
|
624 * |
|
625 * The Spinner class attempts to set the value of the spinner widget if it |
|
626 * finds the appropriate input element on the page. |
|
627 */ |
|
628 Spinner.HTML_PARSER = { |
|
629 value: function (contentBox) { |
|
630 var node = contentBox.one("." + Spinner.INPUT_CLASS); |
|
631 return (node) ? parseInt(node.get("value")) : null; |
|
632 } |
|
633 };</pre> |
|
634 |
|
635 |
|
636 <h3>Lifecycle Methods: initializer, destructor</h3> |
|
637 |
|
638 <p>The <code>initializer</code> and <code>destructor</code> lifecycle methods are carried over from <code>Base</code>, and can be used to set up initial state during construction, and clean up state during destruction respectively.</p> |
|
639 |
|
640 <p>For <code>Spinner</code>, there is nothing special we need to do in the <code>initializer</code> (attribute setup is already taken care of), but it's left in the example to round out the lifecycle discussion.</p> |
|
641 |
|
642 <p>The <code>destructor</code> takes care of detaching any event listeners <code>Spinner</code> adds outside of the bounding box (event listeners on/inside the bounding box are purged by <code>Widget</code>'s <code>destructor</code>).</p> |
|
643 |
|
644 <pre class="code prettyprint">/* |
|
645 * initializer is part of the lifecycle introduced by |
|
646 * the Widget class. It is invoked during construction, |
|
647 * and can be used to set up instance specific state. |
|
648 * |
|
649 * The Spinner class does not need to perform anything |
|
650 * specific in this method, but it is left in as an example. |
|
651 */ |
|
652 initializer: function(config) { |
|
653 // Not doing anything special during initialization |
|
654 }, |
|
655 |
|
656 /* |
|
657 * destructor is part of the lifecycle introduced by |
|
658 * the Widget class. It is invoked during destruction, |
|
659 * and can be used to clean up instance specific state. |
|
660 * |
|
661 * The spinner cleans up any node references it's holding |
|
662 * onto. The Widget classes destructor will purge the |
|
663 * widget's bounding box of event listeners, so spinner |
|
664 * only needs to clean up listeners it attaches outside of |
|
665 * the bounding box. |
|
666 */ |
|
667 destructor : function() { |
|
668 this._documentMouseUpHandle.detach(); |
|
669 |
|
670 this.inputNode = null; |
|
671 this.incrementNode = null; |
|
672 this.decrementNode = null; |
|
673 }</pre> |
|
674 |
|
675 |
|
676 <h3>Rendering Lifecycle Methods: renderer, renderUI, bindUI, syncUI</h3> |
|
677 |
|
678 <p>Widget adds a <code>render</code> method to the <code>init</code> and <code>destroy</code> lifecycle methods provided by Base. The <code>init</code> and <code>destroy</code> methods invoke the corresponding <code>initializer</code> and <code>destructor</code> implementations for the widget. Similarly, the <code>render</code> method invokes the <code>renderer</code> implementation for the widget. Note that the <code>renderer</code> method is not chained automatically, unlike the <code>initializer</code> and <code>destructor</code> methods.</p> |
|
679 |
|
680 <p>The <code>Widget</code> class already provides a default <code>renderer</code> implementation, which invokes the following abstract methods in the order shown <em>(with their respective responsibilities)</em>:</p> |
|
681 |
|
682 <ol> |
|
683 <li><code>renderUI()</code> : responsible for creating/adding elements to the DOM to render the widget.</li> |
|
684 <li><code>bindUI()</code> : responsible for binding event listeners (both attribute change and DOM event listeners) to 'activate' the rendered UI.</li> |
|
685 <li><code>syncUI()</code> : responsible for updating the rendered UI based on the current state of the widget.</li> |
|
686 </ol> |
|
687 |
|
688 <p>Since the <code>Spinner</code> class has no need to modify the <code>Widget</code> <code>renderer</code> implementation, it simply implements the above 3 methods to handle the render phase:</p> |
|
689 |
|
690 <pre class="code prettyprint">/* |
|
691 * For spinner the method adds the input (if it's not already |
|
692 * present in the markup), and creates the increment/decrement buttons |
|
693 */ |
|
694 renderUI : function() { |
|
695 this._renderInput(); |
|
696 this._renderButtons(); |
|
697 }, |
|
698 |
|
699 /* |
|
700 * For spinner, the method: |
|
701 * |
|
702 * - Sets up the attribute change listener for the "value" attribute |
|
703 * |
|
704 * - Binds key listeners for the arrow/page keys |
|
705 * - Binds mouseup/down listeners on the boundingBox, document respectively. |
|
706 * - Binds a simple change listener on the input box. |
|
707 */ |
|
708 bindUI : function() { |
|
709 this.after("valueChange", this._afterValueChange); |
|
710 |
|
711 var boundingBox = this.get("boundingBox"); |
|
712 |
|
713 // Looking for a key event which will fire continuously across browsers |
|
714 // while the key is held down. 38, 40 = arrow up/down, 33, 34 = page up/down |
|
715 var keyEventSpec = (!Y.UA.opera) ? "down:" : "press:"; |
|
716 keyEventSpec += "38, 40, 33, 34"; |
|
717 |
|
718 |
|
719 Y.on("change", Y.bind(this._onInputChange, this), this.inputNode); |
|
720 Y.on("key", Y.bind(this._onDirectionKey, this), boundingBox, keyEventSpec); |
|
721 Y.on("mousedown", Y.bind(this._onMouseDown, this), boundingBox); |
|
722 this._documentMouseUpHandle = Y.on("mouseup", Y.bind(this._onDocMouseUp, this), |
|
723 boundingBox.get("ownerDocument")); |
|
724 }, |
|
725 |
|
726 /* |
|
727 * For spinner, the method sets the value of the input field, |
|
728 * to match the current state of the value attribute. |
|
729 */ |
|
730 syncUI : function() { |
|
731 this._uiSetValue(this.get("value")); |
|
732 }</pre> |
|
733 |
|
734 |
|
735 <h4>A Note On Key Event Listeners</h4> |
|
736 |
|
737 <p>The <code>Spinner</code> uses Event's <code>"key"</code> support, to set up a listener for arrow up/down and page up/down keys on the spinner's bounding box (line 31).</p> |
|
738 |
|
739 <p>Event's <code>"key"</code> support allows <code>Spinner</code> to define a single listener, which is only invoked for the key specification provided. The key specification in the above use case is <code>"down:38, 40, 33, 34"</code> for most browsers, indicating that |
|
740 the <code>_onDirectionKey</code> method should only be called if the bounding box receives a keydown event with a character code which is either 38, 40, 33 or 34. <code>"key"</code> specifications can also contain more <a href="http://yuilibrary.com/yui/docs/api/YUI.html#event_key">advanced filter criteria</a>, involving modifiers such as CTRL and SHIFT.</p> |
|
741 |
|
742 <p>For the Spinner widget, we're looking for a key event which fires repeatedly while the key is held down. This differs for Opera, so we need to fork for the key event we're interested in. Future versions of <code>"key"</code> support will aim to provide this type of higher level cross-browser abstraction also.</p> |
|
743 |
|
744 <h3>Attribute Supporting Methods</h3> |
|
745 |
|
746 <p>Since all widgets are attribute-driven, they all follow a pretty similar pattern when it comes to how those attributes are used. For a given attribute, widgets will generally have:</p> |
|
747 <ul> |
|
748 <li>A prototype method to listen for changes in the attribute</li> |
|
749 <li>A prototype method to update the state of the rendered UI, to reflect the value of an attribute.</li> |
|
750 <li>A prototype method used to set/get/validate the attribute.</li> |
|
751 </ul> |
|
752 |
|
753 <p>These methods are kept on the prototype to facilitate customization at any of the levels - event handling, ui updates, set/get/validation logic.</p> |
|
754 |
|
755 <p>For <code>Spinner</code>, these corresponding methods for the <code>value</code> attribute are: <code>_afterValueChange</code>, <code>_uiSetValue</code> and <code>_validateValue</code>:</p> |
|
756 |
|
757 <pre class="code prettyprint">/* |
|
758 * value attribute change listener. Updates the |
|
759 * value in the rendered input box, whenever the |
|
760 * attribute value changes. |
|
761 */ |
|
762 _afterValueChange : function(e) { |
|
763 this._uiSetValue(e.newVal); |
|
764 }, |
|
765 |
|
766 /* |
|
767 * Updates the value of the input box to reflect |
|
768 * the value passed in |
|
769 */ |
|
770 _uiSetValue : function(val) { |
|
771 this.inputNode.set("value", val); |
|
772 }, |
|
773 |
|
774 /* |
|
775 * value attribute default validator. Verifies that |
|
776 * the value being set lies between the min/max value |
|
777 */ |
|
778 _validateValue: function(val) { |
|
779 var min = this.get("min"), |
|
780 max = this.get("max"); |
|
781 |
|
782 return (Lang.isNumber(val) && val >= min && val <= max); |
|
783 }</pre> |
|
784 |
|
785 |
|
786 <p>Since this example focuses on general patterns for widget development, validator/set/get functions are not defined for attributes such as min/max in the interests of keeping the example simple, but could be, in a production ready spinner.</p> |
|
787 |
|
788 <h3>Rendering Support Methods</h3> |
|
789 |
|
790 <p><code>Spinner</code>'s <code>renderUI</code> method hands off creation of the input field and buttons to the following helpers which use markup templates to generate node instances:</p> |
|
791 |
|
792 <pre class="code prettyprint">/* |
|
793 * Creates the input field for the spinner and adds it to |
|
794 * the widget's content box, if not already in the markup. |
|
795 */ |
|
796 _renderInput : function() { |
|
797 var contentBox = this.get("contentBox"), |
|
798 input = contentBox.one("." + Spinner.INPUT_CLASS), |
|
799 strings = this.get("strings"); |
|
800 |
|
801 if (!input) { |
|
802 input = Node.create(Spinner.INPUT_TEMPLATE); |
|
803 contentBox.appendChild(input); |
|
804 } |
|
805 |
|
806 input.set("title", strings.tooltip); |
|
807 this.inputNode = input; |
|
808 }, |
|
809 |
|
810 /* |
|
811 * Creates the button controls for the spinner and adds them to |
|
812 * the widget's content box, if not already in the markup. |
|
813 */ |
|
814 _renderButtons : function() { |
|
815 var contentBox = this.get("contentBox"), |
|
816 strings = this.get("strings"); |
|
817 |
|
818 var inc = this._createButton(strings.increment, this.getClassName("increment")); |
|
819 var dec = this._createButton(strings.decrement, this.getClassName("decrement")); |
|
820 |
|
821 this.incrementNode = contentBox.appendChild(inc); |
|
822 this.decrementNode = contentBox.appendChild(dec); |
|
823 }, |
|
824 |
|
825 /* |
|
826 * Utility method, to create a spinner button |
|
827 */ |
|
828 _createButton : function(text, className) { |
|
829 |
|
830 var btn = Y.Node.create(Spinner.BTN_TEMPLATE); |
|
831 btn.set("innerHTML", text); |
|
832 btn.set("title", text); |
|
833 btn.addClass(className); |
|
834 |
|
835 return btn; |
|
836 }</pre> |
|
837 |
|
838 |
|
839 <h3>DOM Event Listeners</h3> |
|
840 |
|
841 <p>The DOM event listeners attached during <code>bindUI</code> are straightforward event listeners, which receive the event facade for the DOM event, and update the spinner state accordingly.</p> |
|
842 |
|
843 <p>A couple of interesting points worth noting: In the <code>"key"</code> listener we set up, we can call <code>e.preventDefault()</code> without having to check the character code, since the <code>"key"</code> event specifier will only invoke the listener |
|
844 if one of the specified keys is pressed (arrow/page up/down)</p> |
|
845 |
|
846 <p>Also, to allow the spinner to update its value while the mouse button is held down, we set up a timer, which gets cleared out when we receive a mouseup event on the document.</p> |
|
847 |
|
848 <pre class="code prettyprint">/* |
|
849 * Bounding box Arrow up/down, Page up/down key listener. |
|
850 * |
|
851 * Increments/Decrements the spinner value, based on the key pressed. |
|
852 */ |
|
853 _onDirectionKey : function(e) { |
|
854 e.preventDefault(); |
|
855 ... |
|
856 switch (e.charCode) { |
|
857 case 38: |
|
858 newVal += minorStep; |
|
859 break; |
|
860 case 40: |
|
861 newVal -= minorStep; |
|
862 break; |
|
863 case 33: |
|
864 newVal += majorStep; |
|
865 newVal = Math.min(newVal, this.get("max")); |
|
866 break; |
|
867 case 34: |
|
868 newVal -= majorStep; |
|
869 newVal = Math.max(newVal, this.get("min")); |
|
870 break; |
|
871 } |
|
872 |
|
873 if (newVal !== currVal) { |
|
874 this.set("value", newVal); |
|
875 } |
|
876 }, |
|
877 |
|
878 /* |
|
879 * Bounding box mouse down handler. Will determine if the mouse down |
|
880 * is on one of the spinner buttons, and increment/decrement the value |
|
881 * accordingly. |
|
882 * |
|
883 * The method also sets up a timer, to support the user holding the mouse |
|
884 * down on the spinner buttons. The timer is cleared when a mouse up event |
|
885 * is detected. |
|
886 */ |
|
887 _onMouseDown : function(e) { |
|
888 var node = e.target |
|
889 ... |
|
890 if (node.hasClass(this.getClassName("increment"))) { |
|
891 this.set("value", currVal + minorStep); |
|
892 ... |
|
893 } else if (node.hasClass(this.getClassName("decrement"))) { |
|
894 this.set("value", currVal - minorStep); |
|
895 ... |
|
896 } |
|
897 |
|
898 if (handled) { |
|
899 this._setMouseDownTimers(dir); |
|
900 } |
|
901 }, |
|
902 |
|
903 /* |
|
904 * Document mouse up handler. Clears the timers supporting |
|
905 * the "mouse held down" behavior. |
|
906 */ |
|
907 _onDocMouseUp : function(e) { |
|
908 this._clearMouseDownTimers(); |
|
909 }, |
|
910 |
|
911 /* |
|
912 * Simple change handler, to make sure user does not input an invalid value |
|
913 */ |
|
914 _onInputChange : function(e) { |
|
915 if (!this._validateValue(this.inputNode.get("value"))) { |
|
916 // If the entered value is not valid, re-display the stored value |
|
917 this.syncUI(); |
|
918 } |
|
919 }</pre> |
|
920 |
|
921 |
|
922 <h3>ClassName Support Methods</h3> |
|
923 |
|
924 <p>A key part of developing widgets which work with the DOM is defining class names which it will use to mark the nodes it renders. These class names could be used to mark a node for later retrieval/lookup, for CSS application (both functional as well as cosmetic) or to indicate the current state of the widget.</p> |
|
925 |
|
926 <p>The widget infrastructure uses the <code>ClassNameManager</code> utility, to generate consistently named classes to apply to the nodes it adds to the page:</p> |
|
927 |
|
928 <pre class="code prettyprint">Y.ClassNameManager.getClassName(Spinner.NAME, "value"); |
|
929 ... |
|
930 this.getClassName("increment");</pre> |
|
931 |
|
932 |
|
933 <p> |
|
934 Class names generated by the Widget's <code>getClassName</code> prototype method use the NAME field of the widget, to generate a prefixed classname through <code>ClassNameManager</code> - e.g. for spinner the <code>this.getClassName("increment")</code> above will generate the class name <code>yui3-spinner-increment</code> ("yui" being the system level prefix, "spinner" being the widget name). |
|
935 When you need to generate standard class names in static code (where you don't have a reference to <code>this.getClassName()</code>), you can use the ClassNameManager directly, as shown in line 1 above, to achieve the same results. |
|
936 </p> |
|
937 |
|
938 <h3>CSS Considerations</h3> |
|
939 |
|
940 <p>Since widget uses the <code>getClassName</code> method to generate state-related class names and to mark the bounding box/content box of the widget (e.g. "yui3-[widgetname]-content", "yui3-[widgetname]-hidden", "yui3-[widgetname]-disabled"), we need to provide the default CSS handling for states we're interested in handling for the new Spinner widget. The "yui3-[widgetname]-hidden" class is probably one state class, which all widgets will provide implementations for.</p> |
|
941 |
|
942 <pre class="code prettyprint">/* Progressive enhancement support, to hide the text box, if JavaScript is enabled, while we instantiate the rich control */ |
|
943 .yui3-js-enabled .yui3-spinner-loading { |
|
944 display:none; |
|
945 } |
|
946 |
|
947 /* Controlling show/hide state using display (since this control is inline-block) */ |
|
948 .yui3-spinner-hidden { |
|
949 display:none; |
|
950 } |
|
951 |
|
952 /* Bounding Box - Set the bounding box to be "inline block" for spinner */ |
|
953 .yui3-spinner { |
|
954 display:inline-block; |
|
955 zoom:1; |
|
956 *display:inline; |
|
957 } |
|
958 |
|
959 /* Content Box - Start adding visual treatment for the spinner */ |
|
960 .yui3-spinner-content { |
|
961 padding:1px; |
|
962 } |
|
963 |
|
964 /* Input Text Box, generated through getClassName("value") */ |
|
965 .yui3-spinner-value { |
|
966 ... |
|
967 } |
|
968 |
|
969 /* Button controls, generated through getClassName("increment") */ |
|
970 .yui3-spinner-increment, .yui3-spinner-decrement { |
|
971 ... |
|
972 }</pre> |
|
973 |
|
974 |
|
975 <h3>Using The Spinner Widget</h3> |
|
976 |
|
977 <p>For the example, we have an input field already on the page, which we'd like to enhance to create a Spinner instance. We mark it with a yui3-spinner-loading class, so that if JavaScript is enabled, we can hide it while we're instantiating the rich control:</p> |
|
978 |
|
979 <pre class="code prettyprint"><input type="text" id="numberField" class="yui3-spinner-loading" value="20"></pre> |
|
980 |
|
981 |
|
982 <p>We provide the constructor for the Spinner with the <code>srcNode</code> which contains the input field with our initial value. The <code>HTML_PARSER</code> code we saw earlier will extract the value from the input field, and use it as the initial value for the Spinner instance:</p> |
|
983 |
|
984 <pre class="code prettyprint">// Create a new Spinner instance, drawing its |
|
985 // starting value from an input field already on the |
|
986 // page (the #numberField text input box) |
|
987 var spinner = new Spinner({ |
|
988 srcNode: "#numberField", |
|
989 max:100, |
|
990 min:0 |
|
991 }); |
|
992 spinner.render(); |
|
993 spinner.focus();</pre> |
|
994 |
|
995 |
|
996 <p>The custom widget class structure discussed above is captured in this <a href="../assets/widget/mywidget.js.txt">"MyWidget" template file</a>, which you can use as a starting point to develop your own widgets.</p> |
|
997 |
|
998 <h2>Complete Example Source</h2> |
|
999 <pre class="code prettyprint"><div id="widget-extend-example"> |
|
1000 A basic spinner widget: <input type="text" id="numberField" class="yui3-spinner-loading" value="20" /> |
|
1001 <p class="hint">Click the buttons, or the arrow up/down and page up/down keys on your keyboard to change the spinner's value</p> |
|
1002 </div> |
|
1003 |
|
1004 <script type="text/javascript"> |
|
1005 YUI().use("event-key", "widget", function(Y) { |
|
1006 |
|
1007 var Lang = Y.Lang, |
|
1008 Widget = Y.Widget, |
|
1009 Node = Y.Node; |
|
1010 |
|
1011 /* Spinner class constructor */ |
|
1012 function Spinner(config) { |
|
1013 Spinner.superclass.constructor.apply(this, arguments); |
|
1014 } |
|
1015 |
|
1016 /* |
|
1017 * Required NAME static field, to identify the Widget class and |
|
1018 * used as an event prefix, to generate class names etc. (set to the |
|
1019 * class name in camel case). |
|
1020 */ |
|
1021 Spinner.NAME = "spinner"; |
|
1022 |
|
1023 /* |
|
1024 * The attribute configuration for the Spinner widget. Attributes can be |
|
1025 * defined with default values, get/set functions and validator functions |
|
1026 * as with any other class extending Base. |
|
1027 */ |
|
1028 Spinner.ATTRS = { |
|
1029 // The minimum value for the spinner. |
|
1030 min : { |
|
1031 value:0 |
|
1032 }, |
|
1033 |
|
1034 // The maximum value for the spinner. |
|
1035 max : { |
|
1036 value:100 |
|
1037 }, |
|
1038 |
|
1039 // The current value of the spinner. |
|
1040 value : { |
|
1041 value:0, |
|
1042 validator: function(val) { |
|
1043 return this._validateValue(val); |
|
1044 } |
|
1045 }, |
|
1046 |
|
1047 // Amount to increment/decrement the spinner when the buttons or arrow up/down keys are pressed. |
|
1048 minorStep : { |
|
1049 value:1 |
|
1050 }, |
|
1051 |
|
1052 // Amount to increment/decrement the spinner when the page up/down keys are pressed. |
|
1053 majorStep : { |
|
1054 value:10 |
|
1055 }, |
|
1056 |
|
1057 // override default ("null"), required for focus() |
|
1058 tabIndex: { |
|
1059 value: 0 |
|
1060 }, |
|
1061 |
|
1062 // The strings for the spinner UI. This attribute is |
|
1063 // defined by the base Widget class but has an empty value. The |
|
1064 // spinner is simply providing a default value for the attribute. |
|
1065 strings: { |
|
1066 value: { |
|
1067 tooltip: "Press the arrow up/down keys for minor increments, page up/down for major increments.", |
|
1068 increment: "Increment", |
|
1069 decrement: "Decrement" |
|
1070 } |
|
1071 } |
|
1072 }; |
|
1073 |
|
1074 /* Static constant used to identify the classname applied to the spinners value field */ |
|
1075 Spinner.INPUT_CLASS = Y.ClassNameManager.getClassName(Spinner.NAME, "value"); |
|
1076 |
|
1077 /* Static constants used to define the markup templates used to create Spinner DOM elements */ |
|
1078 Spinner.INPUT_TEMPLATE = '<input type="text" class="' + Spinner.INPUT_CLASS + '">'; |
|
1079 Spinner.BTN_TEMPLATE = '<button type="button"></button>'; |
|
1080 |
|
1081 /* |
|
1082 * The HTML_PARSER static constant is used by the Widget base class to populate |
|
1083 * the configuration for the spinner instance from markup already on the page. |
|
1084 * |
|
1085 * The Spinner class attempts to set the value of the spinner widget if it |
|
1086 * finds the appropriate input element on the page. |
|
1087 */ |
|
1088 Spinner.HTML_PARSER = { |
|
1089 value: function (srcNode) { |
|
1090 var val = parseInt(srcNode.get("value")); |
|
1091 return Y.Lang.isNumber(val) ? val : null; |
|
1092 } |
|
1093 }; |
|
1094 |
|
1095 /* Spinner extends the base Widget class */ |
|
1096 Y.extend(Spinner, Widget, { |
|
1097 |
|
1098 /* |
|
1099 * initializer is part of the lifecycle introduced by |
|
1100 * the Widget class. It is invoked during construction, |
|
1101 * and can be used to setup instance specific state. |
|
1102 * |
|
1103 * The Spinner class does not need to perform anything |
|
1104 * specific in this method, but it is left in as an example. |
|
1105 */ |
|
1106 initializer: function() { |
|
1107 // Not doing anything special during initialization |
|
1108 }, |
|
1109 |
|
1110 /* |
|
1111 * destructor is part of the lifecycle introduced by |
|
1112 * the Widget class. It is invoked during destruction, |
|
1113 * and can be used to cleanup instance specific state. |
|
1114 * |
|
1115 * The spinner cleans up any node references it's holding |
|
1116 * onto. The Widget classes destructor will purge the |
|
1117 * widget's bounding box of event listeners, so spinner |
|
1118 * only needs to clean up listeners it attaches outside of |
|
1119 * the bounding box. |
|
1120 */ |
|
1121 destructor : function() { |
|
1122 this._documentMouseUpHandle.detach(); |
|
1123 |
|
1124 this.inputNode = null; |
|
1125 this.incrementNode = null; |
|
1126 this.decrementNode = null; |
|
1127 }, |
|
1128 |
|
1129 /* |
|
1130 * renderUI is part of the lifecycle introduced by the |
|
1131 * Widget class. Widget's renderer method invokes: |
|
1132 * |
|
1133 * renderUI() |
|
1134 * bindUI() |
|
1135 * syncUI() |
|
1136 * |
|
1137 * renderUI is intended to be used by the Widget subclass |
|
1138 * to create or insert new elements into the DOM. |
|
1139 * |
|
1140 * For spinner the method adds the input (if it's not already |
|
1141 * present in the markup), and creates the inc/dec buttons |
|
1142 */ |
|
1143 renderUI : function() { |
|
1144 this._renderInput(); |
|
1145 this._renderButtons(); |
|
1146 }, |
|
1147 |
|
1148 /* |
|
1149 * bindUI is intended to be used by the Widget subclass |
|
1150 * to bind any event listeners which will drive the Widget UI. |
|
1151 * |
|
1152 * It will generally bind event listeners for attribute change |
|
1153 * events, to update the state of the rendered UI in response |
|
1154 * to attribute value changes, and also attach any DOM events, |
|
1155 * to activate the UI. |
|
1156 * |
|
1157 * For spinner, the method: |
|
1158 * |
|
1159 * - Sets up the attribute change listener for the "value" attribute |
|
1160 * |
|
1161 * - Binds key listeners for the arrow/page keys |
|
1162 * - Binds mouseup/down listeners on the boundingBox, document respectively. |
|
1163 * - Binds a simple change listener on the input box. |
|
1164 */ |
|
1165 bindUI : function() { |
|
1166 this.after("valueChange", this._afterValueChange); |
|
1167 |
|
1168 var boundingBox = this.get("boundingBox"); |
|
1169 |
|
1170 // Looking for a key event which will fire continuously across browsers while the key is held down. 38, 40 = arrow up/down, 33, 34 = page up/down |
|
1171 var keyEventSpec = (!Y.UA.opera) ? "down:" : "press:"; |
|
1172 keyEventSpec += "38, 40, 33, 34"; |
|
1173 |
|
1174 Y.on("key", Y.bind(this._onDirectionKey, this), boundingBox, keyEventSpec); |
|
1175 Y.on("mousedown", Y.bind(this._onMouseDown, this), boundingBox); |
|
1176 |
|
1177 this._documentMouseUpHandle = Y.on("mouseup", Y.bind(this._onDocMouseUp, this), boundingBox.get("ownerDocument")); |
|
1178 |
|
1179 Y.on("change", Y.bind(this._onInputChange, this), this.inputNode); |
|
1180 }, |
|
1181 |
|
1182 /* |
|
1183 * syncUI is intended to be used by the Widget subclass to |
|
1184 * update the UI to reflect the current state of the widget. |
|
1185 * |
|
1186 * For spinner, the method sets the value of the input field, |
|
1187 * to match the current state of the value attribute. |
|
1188 */ |
|
1189 syncUI : function() { |
|
1190 this._uiSetValue(this.get("value")); |
|
1191 }, |
|
1192 |
|
1193 /* |
|
1194 * Creates the input control for the spinner and adds it to |
|
1195 * the widget's content box, if not already in the markup. |
|
1196 */ |
|
1197 _renderInput : function() { |
|
1198 var contentBox = this.get("contentBox"), |
|
1199 input = contentBox.one("." + Spinner.INPUT_CLASS), |
|
1200 strings = this.get("strings"); |
|
1201 |
|
1202 if (!input) { |
|
1203 input = Node.create(Spinner.INPUT_TEMPLATE); |
|
1204 contentBox.appendChild(input); |
|
1205 } |
|
1206 |
|
1207 input.set("title", strings.tooltip); |
|
1208 this.inputNode = input; |
|
1209 }, |
|
1210 |
|
1211 /* |
|
1212 * Creates the button controls for the spinner and add them to |
|
1213 * the widget's content box, if not already in the markup. |
|
1214 */ |
|
1215 _renderButtons : function() { |
|
1216 var contentBox = this.get("contentBox"), |
|
1217 strings = this.get("strings"); |
|
1218 |
|
1219 var inc = this._createButton(strings.increment, this.getClassName("increment")); |
|
1220 var dec = this._createButton(strings.decrement, this.getClassName("decrement")); |
|
1221 |
|
1222 this.incrementNode = contentBox.appendChild(inc); |
|
1223 this.decrementNode = contentBox.appendChild(dec); |
|
1224 }, |
|
1225 |
|
1226 /* |
|
1227 * Utility method, to create a spinner button |
|
1228 */ |
|
1229 _createButton : function(text, className) { |
|
1230 |
|
1231 var btn = Y.Node.create(Spinner.BTN_TEMPLATE); |
|
1232 btn.set("innerHTML", text); |
|
1233 btn.set("title", text); |
|
1234 btn.addClass(className); |
|
1235 |
|
1236 return btn; |
|
1237 }, |
|
1238 |
|
1239 /* |
|
1240 * Bounding box mouse down handler. Will determine if the mouse down |
|
1241 * is on one of the spinner buttons, and increment/decrement the value |
|
1242 * accordingly. |
|
1243 * |
|
1244 * The method also sets up a timer, to support the user holding the mouse |
|
1245 * down on the spinner buttons. The timer is cleared when a mouse up event |
|
1246 * is detected. |
|
1247 */ |
|
1248 _onMouseDown : function(e) { |
|
1249 var node = e.target, |
|
1250 dir, |
|
1251 handled = false, |
|
1252 currVal = this.get("value"), |
|
1253 minorStep = this.get("minorStep"); |
|
1254 |
|
1255 if (node.hasClass(this.getClassName("increment"))) { |
|
1256 this.set("value", currVal + minorStep); |
|
1257 dir = 1; |
|
1258 handled = true; |
|
1259 } else if (node.hasClass(this.getClassName("decrement"))) { |
|
1260 this.set("value", currVal - minorStep); |
|
1261 dir = -1; |
|
1262 handled = true; |
|
1263 } |
|
1264 |
|
1265 if (handled) { |
|
1266 this._setMouseDownTimers(dir, minorStep); |
|
1267 } |
|
1268 }, |
|
1269 |
|
1270 /* |
|
1271 * Override the default content box value, since we don't want the srcNode |
|
1272 * to be the content box for spinner. |
|
1273 */ |
|
1274 _defaultCB : function() { |
|
1275 return null; |
|
1276 }, |
|
1277 |
|
1278 /* |
|
1279 * Document mouse up handler. Clears the timers supporting |
|
1280 * the "mouse held down" behavior. |
|
1281 */ |
|
1282 _onDocMouseUp : function(e) { |
|
1283 this._clearMouseDownTimers(); |
|
1284 }, |
|
1285 |
|
1286 /* |
|
1287 * Bounding box Arrow up/down, Page up/down key listener. |
|
1288 * |
|
1289 * Increments/Decrement the spinner value, based on the key pressed. |
|
1290 */ |
|
1291 _onDirectionKey : function(e) { |
|
1292 |
|
1293 e.preventDefault(); |
|
1294 |
|
1295 var currVal = this.get("value"), |
|
1296 newVal = currVal, |
|
1297 minorStep = this.get("minorStep"), |
|
1298 majorStep = this.get("majorStep"); |
|
1299 |
|
1300 switch (e.charCode) { |
|
1301 case 38: |
|
1302 newVal += minorStep; |
|
1303 break; |
|
1304 case 40: |
|
1305 newVal -= minorStep; |
|
1306 break; |
|
1307 case 33: |
|
1308 newVal += majorStep; |
|
1309 newVal = Math.min(newVal, this.get("max")); |
|
1310 break; |
|
1311 case 34: |
|
1312 newVal -= majorStep; |
|
1313 newVal = Math.max(newVal, this.get("min")); |
|
1314 break; |
|
1315 } |
|
1316 |
|
1317 if (newVal !== currVal) { |
|
1318 this.set("value", newVal); |
|
1319 } |
|
1320 }, |
|
1321 |
|
1322 /* |
|
1323 * Simple change handler, to make sure user does not input an invalid value |
|
1324 */ |
|
1325 _onInputChange : function(e) { |
|
1326 if (!this._validateValue(this.inputNode.get("value"))) { |
|
1327 this.syncUI(); |
|
1328 } |
|
1329 }, |
|
1330 |
|
1331 /* |
|
1332 * Initiates mouse down timers, to increment slider, while mouse button |
|
1333 * is held down |
|
1334 */ |
|
1335 _setMouseDownTimers : function(dir, step) { |
|
1336 this._mouseDownTimer = Y.later(500, this, function() { |
|
1337 this._mousePressTimer = Y.later(100, this, function() { |
|
1338 this.set("value", this.get("value") + (dir * step)); |
|
1339 }, null, true) |
|
1340 }); |
|
1341 }, |
|
1342 |
|
1343 /* |
|
1344 * Clears timers used to support the "mouse held down" behavior |
|
1345 */ |
|
1346 _clearMouseDownTimers : function() { |
|
1347 if (this._mouseDownTimer) { |
|
1348 this._mouseDownTimer.cancel(); |
|
1349 this._mouseDownTimer = null; |
|
1350 } |
|
1351 if (this._mousePressTimer) { |
|
1352 this._mousePressTimer.cancel(); |
|
1353 this._mousePressTimer = null; |
|
1354 } |
|
1355 }, |
|
1356 |
|
1357 /* |
|
1358 * value attribute change listener. Updates the |
|
1359 * value in the rendered input box, whenever the |
|
1360 * attribute value changes. |
|
1361 */ |
|
1362 _afterValueChange : function(e) { |
|
1363 this._uiSetValue(e.newVal); |
|
1364 }, |
|
1365 |
|
1366 /* |
|
1367 * Updates the value of the input box to reflect |
|
1368 * the value passed in |
|
1369 */ |
|
1370 _uiSetValue : function(val) { |
|
1371 this.inputNode.set("value", val); |
|
1372 }, |
|
1373 |
|
1374 /* |
|
1375 * value attribute default validator. Verifies that |
|
1376 * the value being set lies between the min/max value |
|
1377 */ |
|
1378 _validateValue: function(val) { |
|
1379 var min = this.get("min"), |
|
1380 max = this.get("max"); |
|
1381 |
|
1382 return (Lang.isNumber(val) && val >= min && val <= max); |
|
1383 } |
|
1384 }); |
|
1385 |
|
1386 // Create a new Spinner instance, drawing it's |
|
1387 // starting value from an input field already on the |
|
1388 // page (the #numberField text box) |
|
1389 var spinner = new Spinner({ |
|
1390 srcNode: "#numberField", |
|
1391 max:100, |
|
1392 min:0 |
|
1393 }); |
|
1394 spinner.render(); |
|
1395 spinner.focus(); |
|
1396 }); |
|
1397 </script></pre> |
|
1398 |
|
1399 </div> |
|
1400 </div> |
|
1401 </div> |
|
1402 |
|
1403 <div class="yui3-u-1-4"> |
|
1404 <div class="sidebar"> |
|
1405 |
|
1406 |
|
1407 |
|
1408 <div class="sidebox"> |
|
1409 <div class="hd"> |
|
1410 <h2 class="no-toc">Examples</h2> |
|
1411 </div> |
|
1412 |
|
1413 <div class="bd"> |
|
1414 <ul class="examples"> |
|
1415 |
|
1416 |
|
1417 <li data-description="Shows how to extend the base widget class, to create your own Widgets."> |
|
1418 <a href="widget-extend.html">Extending the Base Widget Class</a> |
|
1419 </li> |
|
1420 |
|
1421 |
|
1422 |
|
1423 <li data-description="Shows how to use Base.create and mix/match extensions to create custom Widget classes."> |
|
1424 <a href="widget-build.html">Creating Custom Widget Classes With Extensions</a> |
|
1425 </li> |
|
1426 |
|
1427 |
|
1428 |
|
1429 <li data-description="Shows how to create an IO plugin for Widget."> |
|
1430 <a href="widget-plugin.html">Creating a Widget Plugin</a> |
|
1431 </li> |
|
1432 |
|
1433 |
|
1434 |
|
1435 <li data-description="Shows how to extend the Widget class, and add WidgetPosition and WidgetStack to create a Tooltip widget class."> |
|
1436 <a href="widget-tooltip.html">Creating a Simple Tooltip Widget With Extensions</a> |
|
1437 </li> |
|
1438 |
|
1439 |
|
1440 |
|
1441 <li data-description="Shows how to extend the Widget class, and add WidgetParent and WidgetChild to create a simple ListBox widget."> |
|
1442 <a href="widget-parentchild-listbox.html">Creating a Hierarchical ListBox Widget</a> |
|
1443 </li> |
|
1444 |
|
1445 |
|
1446 </ul> |
|
1447 </div> |
|
1448 </div> |
|
1449 |
|
1450 |
|
1451 |
|
1452 </div> |
|
1453 </div> |
|
1454 </div> |
|
1455 </div> |
|
1456 |
|
1457 <script src="../assets/vendor/prettify/prettify-min.js"></script> |
|
1458 <script>prettyPrint();</script> |
|
1459 |
|
1460 <script> |
|
1461 YUI.Env.Tests = { |
|
1462 examples: [], |
|
1463 project: '../assets', |
|
1464 assets: '../assets/widget', |
|
1465 name: 'widget-extend', |
|
1466 title: 'Extending the Base Widget Class', |
|
1467 newWindow: '', |
|
1468 auto: false |
|
1469 }; |
|
1470 YUI.Env.Tests.examples.push('widget-extend'); |
|
1471 YUI.Env.Tests.examples.push('widget-build'); |
|
1472 YUI.Env.Tests.examples.push('widget-plugin'); |
|
1473 YUI.Env.Tests.examples.push('widget-tooltip'); |
|
1474 YUI.Env.Tests.examples.push('widget-parentchild-listbox'); |
|
1475 |
|
1476 </script> |
|
1477 <script src="../assets/yui/test-runner.js"></script> |
|
1478 |
|
1479 |
|
1480 |
|
1481 </body> |
|
1482 </html> |