src/cm/media/js/lib/yui/yui_3.10.3/docs/widget/widget-extend.html
changeset 525 89ef5ed3c48b
equal deleted inserted replaced
524:322d0feea350 525:89ef5ed3c48b
       
     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">&#x2F;* Spinner class constructor *&#x2F;
       
   518 function Spinner(config) {
       
   519     Spinner.superclass.constructor.apply(this, arguments);
       
   520 }
       
   521 
       
   522 &#x2F;* 
       
   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  *&#x2F;
       
   527 Spinner.NAME = &quot;spinner&quot;;
       
   528 
       
   529 &#x2F;*
       
   530  * The attribute configuration for the Spinner widget. Attributes can be
       
   531  * defined with default values, get&#x2F;set functions and validator functions
       
   532  * as with any other class extending Base.
       
   533  *&#x2F;
       
   534 Spinner.ATTRS = {
       
   535     &#x2F;&#x2F; The minimum value for the spinner.
       
   536     min : {
       
   537         value:0
       
   538     },
       
   539 
       
   540     &#x2F;&#x2F; The maximum value for the spinner.
       
   541     max : {
       
   542         value:100
       
   543     },
       
   544 
       
   545     &#x2F;&#x2F; The current value of the spinner.
       
   546     value : {
       
   547         value:0,
       
   548         validator: function(val) {
       
   549             return this._validateValue(val);
       
   550         }
       
   551     },
       
   552 
       
   553     &#x2F;&#x2F; Amount to increment&#x2F;decrement the spinner when the buttons, 
       
   554     &#x2F;&#x2F; or arrow up&#x2F;down keys are pressed.
       
   555     minorStep : {
       
   556         value:1
       
   557     },
       
   558 
       
   559     &#x2F;&#x2F; Amount to increment&#x2F;decrement the spinner when the page up&#x2F;down keys are pressed.
       
   560     majorStep : {
       
   561         value:10
       
   562     },
       
   563 
       
   564     &#x2F;&#x2F; The localizable strings for the spinner. This attribute is 
       
   565     &#x2F;&#x2F; defined by the base Widget class but has an empty value. The
       
   566     &#x2F;&#x2F; spinner is simply providing a default value for the attribute.
       
   567     strings: {
       
   568         value: {
       
   569             tooltip: &quot;Press the arrow up&#x2F;down keys for minor increments, \ 
       
   570                       page up&#x2F;down for major increments.&quot;,
       
   571             increment: &quot;Increment&quot;,
       
   572             decrement: &quot;Decrement&quot;
       
   573         }
       
   574     }
       
   575 };
       
   576 
       
   577 Y.extend(Spinner, Widget, {
       
   578     &#x2F;&#x2F; Methods&#x2F;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(&quot;myspinner&quot;); &#x2F;&#x2F; Assuming &quot;myspinner&quot; is the name of your widget&#x27;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">&#x2F;* 
       
   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  *&#x2F;
       
   628 Spinner.HTML_PARSER = {
       
   629     value: function (contentBox) {
       
   630         var node = contentBox.one(&quot;.&quot; + Spinner.INPUT_CLASS);
       
   631         return (node) ? parseInt(node.get(&quot;value&quot;)) : 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">&#x2F;*
       
   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  *&#x2F;
       
   652 initializer: function(config) {
       
   653     &#x2F;&#x2F; Not doing anything special during initialization
       
   654 },
       
   655 
       
   656 &#x2F;*
       
   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&#x27;s holding
       
   662  * onto. The Widget classes destructor will purge the 
       
   663  * widget&#x27;s bounding box of event listeners, so spinner 
       
   664  * only needs to clean up listeners it attaches outside of 
       
   665  * the bounding box.
       
   666  *&#x2F;
       
   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">&#x2F;*
       
   691  * For spinner the method adds the input (if it&#x27;s not already 
       
   692  * present in the markup), and creates the increment&#x2F;decrement buttons
       
   693  *&#x2F;
       
   694 renderUI : function() {
       
   695     this._renderInput();
       
   696     this._renderButtons();
       
   697 },
       
   698 
       
   699 &#x2F;*
       
   700  * For spinner, the method:
       
   701  *
       
   702  * - Sets up the attribute change listener for the &quot;value&quot; attribute
       
   703  *
       
   704  * - Binds key listeners for the arrow&#x2F;page keys
       
   705  * - Binds mouseup&#x2F;down listeners on the boundingBox, document respectively.
       
   706  * - Binds a simple change listener on the input box.
       
   707  *&#x2F;
       
   708 bindUI : function() {
       
   709     this.after(&quot;valueChange&quot;, this._afterValueChange);
       
   710 
       
   711     var boundingBox = this.get(&quot;boundingBox&quot;);
       
   712 
       
   713     &#x2F;&#x2F; Looking for a key event which will fire continuously across browsers 
       
   714     &#x2F;&#x2F; while the key is held down. 38, 40 = arrow up&#x2F;down, 33, 34 = page up&#x2F;down
       
   715     var keyEventSpec = (!Y.UA.opera) ? &quot;down:&quot; : &quot;press:&quot;;
       
   716     keyEventSpec += &quot;38, 40, 33, 34&quot;;
       
   717 
       
   718 
       
   719     Y.on(&quot;change&quot;, Y.bind(this._onInputChange, this), this.inputNode);
       
   720     Y.on(&quot;key&quot;, Y.bind(this._onDirectionKey, this), boundingBox, keyEventSpec);
       
   721     Y.on(&quot;mousedown&quot;, Y.bind(this._onMouseDown, this), boundingBox);
       
   722     this._documentMouseUpHandle = Y.on(&quot;mouseup&quot;, Y.bind(this._onDocMouseUp, this), 
       
   723                 boundingBox.get(&quot;ownerDocument&quot;));
       
   724 },
       
   725 
       
   726 &#x2F;*
       
   727  * For spinner, the method sets the value of the input field,
       
   728  * to match the current state of the value attribute.
       
   729  *&#x2F;
       
   730 syncUI : function() {
       
   731     this._uiSetValue(this.get(&quot;value&quot;));
       
   732 }</pre>
       
   733 
       
   734 
       
   735 <h4>A Note On Key Event Listeners</h4>
       
   736 
       
   737 <p>The <code>Spinner</code> uses Event's <code>&quot;key&quot;</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>&quot;key&quot;</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>&quot;down:38, 40, 33, 34&quot;</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>&quot;key&quot;</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>&quot;key&quot;</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">&#x2F;*
       
   758  * value attribute change listener. Updates the 
       
   759  * value in the rendered input box, whenever the 
       
   760  * attribute value changes.
       
   761  *&#x2F;
       
   762 _afterValueChange : function(e) {
       
   763     this._uiSetValue(e.newVal);
       
   764 },
       
   765 
       
   766 &#x2F;*
       
   767  * Updates the value of the input box to reflect 
       
   768  * the value passed in
       
   769  *&#x2F;
       
   770 _uiSetValue : function(val) {
       
   771     this.inputNode.set(&quot;value&quot;, val);
       
   772 },
       
   773 
       
   774 &#x2F;*
       
   775  * value attribute default validator. Verifies that
       
   776  * the value being set lies between the min&#x2F;max value
       
   777  *&#x2F;
       
   778 _validateValue: function(val) {
       
   779     var min = this.get(&quot;min&quot;),
       
   780         max = this.get(&quot;max&quot;);
       
   781 
       
   782     return (Lang.isNumber(val) &amp;&amp; val &gt;= min &amp;&amp; val &lt;= 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">&#x2F;*
       
   793  * Creates the input field for the spinner and adds it to
       
   794  * the widget&#x27;s content box, if not already in the markup.
       
   795  *&#x2F;
       
   796 _renderInput : function() {
       
   797     var contentBox = this.get(&quot;contentBox&quot;),
       
   798         input = contentBox.one(&quot;.&quot; + Spinner.INPUT_CLASS),
       
   799         strings = this.get(&quot;strings&quot;);
       
   800 
       
   801     if (!input) {
       
   802         input = Node.create(Spinner.INPUT_TEMPLATE);
       
   803         contentBox.appendChild(input);
       
   804     }
       
   805 
       
   806     input.set(&quot;title&quot;, strings.tooltip);
       
   807     this.inputNode = input;
       
   808 },
       
   809 
       
   810 &#x2F;*
       
   811  * Creates the button controls for the spinner and adds them to
       
   812  * the widget&#x27;s content box, if not already in the markup.
       
   813  *&#x2F;
       
   814 _renderButtons : function() {
       
   815     var contentBox = this.get(&quot;contentBox&quot;),
       
   816         strings = this.get(&quot;strings&quot;);
       
   817 
       
   818     var inc = this._createButton(strings.increment, this.getClassName(&quot;increment&quot;));
       
   819     var dec = this._createButton(strings.decrement, this.getClassName(&quot;decrement&quot;));
       
   820 
       
   821     this.incrementNode = contentBox.appendChild(inc);
       
   822     this.decrementNode = contentBox.appendChild(dec);
       
   823 },
       
   824 
       
   825 &#x2F;*
       
   826  * Utility method, to create a spinner button
       
   827  *&#x2F;
       
   828 _createButton : function(text, className) {
       
   829 
       
   830     var btn = Y.Node.create(Spinner.BTN_TEMPLATE);
       
   831     btn.set(&quot;innerHTML&quot;, text);
       
   832     btn.set(&quot;title&quot;, 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>&quot;key&quot;</code> listener we set up, we can call <code>e.preventDefault()</code> without having to check the character code, since the <code>&quot;key&quot;</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">&#x2F;*
       
   849  * Bounding box Arrow up&#x2F;down, Page up&#x2F;down key listener.
       
   850  *
       
   851  * Increments&#x2F;Decrements the spinner value, based on the key pressed.
       
   852  *&#x2F;
       
   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(&quot;max&quot;));
       
   866             break;
       
   867         case 34:
       
   868             newVal -= majorStep;
       
   869             newVal = Math.max(newVal, this.get(&quot;min&quot;));
       
   870             break;
       
   871     }
       
   872 
       
   873     if (newVal !== currVal) {
       
   874         this.set(&quot;value&quot;, newVal);
       
   875     }
       
   876 },
       
   877 
       
   878 &#x2F;*
       
   879  * Bounding box mouse down handler. Will determine if the mouse down
       
   880  * is on one of the spinner buttons, and increment&#x2F;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  *&#x2F;
       
   887 _onMouseDown : function(e) {
       
   888     var node = e.target
       
   889     ...
       
   890     if (node.hasClass(this.getClassName(&quot;increment&quot;))) {
       
   891         this.set(&quot;value&quot;, currVal + minorStep);
       
   892         ...
       
   893     } else if (node.hasClass(this.getClassName(&quot;decrement&quot;))) {
       
   894         this.set(&quot;value&quot;, currVal - minorStep);
       
   895         ...
       
   896     }
       
   897 
       
   898     if (handled) {
       
   899         this._setMouseDownTimers(dir);
       
   900     }
       
   901 },
       
   902 
       
   903 &#x2F;*
       
   904  * Document mouse up handler. Clears the timers supporting
       
   905  * the &quot;mouse held down&quot; behavior.
       
   906  *&#x2F;
       
   907 _onDocMouseUp : function(e) {
       
   908     this._clearMouseDownTimers();
       
   909 },
       
   910 
       
   911 &#x2F;*
       
   912  * Simple change handler, to make sure user does not input an invalid value
       
   913  *&#x2F;
       
   914 _onInputChange : function(e) {
       
   915     if (!this._validateValue(this.inputNode.get(&quot;value&quot;))) {
       
   916         &#x2F;&#x2F; 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, &quot;value&quot;);
       
   929 ...
       
   930 this.getClassName(&quot;increment&quot;);</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(&quot;increment&quot;)</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">&#x2F;* Progressive enhancement support, to hide the text box, if JavaScript is enabled, while we instantiate the rich control *&#x2F;
       
   943 .yui3-js-enabled .yui3-spinner-loading {
       
   944     display:none;
       
   945 }
       
   946 
       
   947 &#x2F;* Controlling show&#x2F;hide state using display (since this control is inline-block) *&#x2F;
       
   948 .yui3-spinner-hidden {
       
   949     display:none;
       
   950 }
       
   951 
       
   952 &#x2F;* Bounding Box - Set the bounding box to be &quot;inline block&quot; for spinner *&#x2F;
       
   953 .yui3-spinner {
       
   954     display:inline-block;
       
   955     zoom:1;
       
   956     *display:inline;
       
   957 }
       
   958 
       
   959 &#x2F;* Content Box - Start adding visual treatment for the spinner *&#x2F;
       
   960 .yui3-spinner-content {
       
   961     padding:1px;
       
   962 }
       
   963 
       
   964 &#x2F;* Input Text Box, generated through getClassName(&quot;value&quot;) *&#x2F;
       
   965 .yui3-spinner-value {
       
   966     ...
       
   967 }
       
   968 
       
   969 &#x2F;* Button controls, generated through getClassName(&quot;increment&quot;) *&#x2F;
       
   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">&lt;input type=&quot;text&quot; id=&quot;numberField&quot; class=&quot;yui3-spinner-loading&quot; value=&quot;20&quot;&gt;</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">&#x2F;&#x2F; Create a new Spinner instance, drawing its 
       
   985 &#x2F;&#x2F; starting value from an input field already on the 
       
   986 &#x2F;&#x2F; page (the #numberField text input box)
       
   987 var spinner = new Spinner({
       
   988     srcNode: &quot;#numberField&quot;,
       
   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">&lt;div id=&quot;widget-extend-example&quot;&gt;
       
  1000     A basic spinner widget: &lt;input type=&quot;text&quot; id=&quot;numberField&quot; class=&quot;yui3-spinner-loading&quot; value=&quot;20&quot; &#x2F;&gt;
       
  1001     &lt;p class=&quot;hint&quot;&gt;Click the buttons, or the arrow up&#x2F;down and page up&#x2F;down keys on your keyboard to change the spinner&#x27;s value&lt;&#x2F;p&gt;
       
  1002 &lt;&#x2F;div&gt;
       
  1003 
       
  1004 &lt;script type=&quot;text&#x2F;javascript&quot;&gt;
       
  1005 YUI().use(&quot;event-key&quot;, &quot;widget&quot;, function(Y) {
       
  1006 
       
  1007     var Lang = Y.Lang,
       
  1008         Widget = Y.Widget,
       
  1009         Node = Y.Node;
       
  1010 
       
  1011     &#x2F;* Spinner class constructor *&#x2F;
       
  1012     function Spinner(config) {
       
  1013         Spinner.superclass.constructor.apply(this, arguments);
       
  1014     }
       
  1015 
       
  1016     &#x2F;* 
       
  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      *&#x2F;
       
  1021     Spinner.NAME = &quot;spinner&quot;;
       
  1022 
       
  1023     &#x2F;*
       
  1024      * The attribute configuration for the Spinner widget. Attributes can be
       
  1025      * defined with default values, get&#x2F;set functions and validator functions
       
  1026      * as with any other class extending Base.
       
  1027      *&#x2F;
       
  1028     Spinner.ATTRS = {
       
  1029         &#x2F;&#x2F; The minimum value for the spinner.
       
  1030         min : {
       
  1031             value:0
       
  1032         },
       
  1033 
       
  1034         &#x2F;&#x2F; The maximum value for the spinner.
       
  1035         max : {
       
  1036             value:100
       
  1037         },
       
  1038 
       
  1039         &#x2F;&#x2F; The current value of the spinner.
       
  1040         value : {
       
  1041             value:0,
       
  1042             validator: function(val) {
       
  1043                 return this._validateValue(val);
       
  1044             }
       
  1045         },
       
  1046 
       
  1047         &#x2F;&#x2F; Amount to increment&#x2F;decrement the spinner when the buttons or arrow up&#x2F;down keys are pressed.
       
  1048         minorStep : {
       
  1049             value:1
       
  1050         },
       
  1051 
       
  1052         &#x2F;&#x2F; Amount to increment&#x2F;decrement the spinner when the page up&#x2F;down keys are pressed.
       
  1053         majorStep : {
       
  1054             value:10
       
  1055         },
       
  1056 
       
  1057         &#x2F;&#x2F; override default (&quot;null&quot;), required for focus()
       
  1058         tabIndex: {
       
  1059             value: 0
       
  1060         },
       
  1061 
       
  1062         &#x2F;&#x2F; The strings for the spinner UI. This attribute is 
       
  1063         &#x2F;&#x2F; defined by the base Widget class but has an empty value. The
       
  1064         &#x2F;&#x2F; spinner is simply providing a default value for the attribute.
       
  1065         strings: {
       
  1066             value: {
       
  1067                 tooltip: &quot;Press the arrow up&#x2F;down keys for minor increments, page up&#x2F;down for major increments.&quot;,
       
  1068                 increment: &quot;Increment&quot;,
       
  1069                 decrement: &quot;Decrement&quot;
       
  1070             }
       
  1071         }
       
  1072     };
       
  1073 
       
  1074     &#x2F;* Static constant used to identify the classname applied to the spinners value field *&#x2F;
       
  1075     Spinner.INPUT_CLASS = Y.ClassNameManager.getClassName(Spinner.NAME, &quot;value&quot;);
       
  1076 
       
  1077     &#x2F;* Static constants used to define the markup templates used to create Spinner DOM elements *&#x2F;
       
  1078     Spinner.INPUT_TEMPLATE = &#x27;&lt;input type=&quot;text&quot; class=&quot;&#x27; + Spinner.INPUT_CLASS + &#x27;&quot;&gt;&#x27;;
       
  1079     Spinner.BTN_TEMPLATE = &#x27;&lt;button type=&quot;button&quot;&gt;&lt;&#x2F;button&gt;&#x27;;
       
  1080 
       
  1081     &#x2F;* 
       
  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      *&#x2F;
       
  1088     Spinner.HTML_PARSER = {
       
  1089         value: function (srcNode) {
       
  1090             var val = parseInt(srcNode.get(&quot;value&quot;)); 
       
  1091             return Y.Lang.isNumber(val) ? val : null;
       
  1092         }
       
  1093     };
       
  1094 
       
  1095     &#x2F;* Spinner extends the base Widget class *&#x2F;
       
  1096     Y.extend(Spinner, Widget, {
       
  1097 
       
  1098         &#x2F;*
       
  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          *&#x2F;
       
  1106         initializer: function() {
       
  1107             &#x2F;&#x2F; Not doing anything special during initialization
       
  1108         },
       
  1109 
       
  1110         &#x2F;*
       
  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&#x27;s holding
       
  1116          * onto. The Widget classes destructor will purge the 
       
  1117          * widget&#x27;s bounding box of event listeners, so spinner 
       
  1118          * only needs to clean up listeners it attaches outside of 
       
  1119          * the bounding box.
       
  1120          *&#x2F;
       
  1121         destructor : function() {
       
  1122             this._documentMouseUpHandle.detach();
       
  1123 
       
  1124             this.inputNode = null;
       
  1125             this.incrementNode = null;
       
  1126             this.decrementNode = null;
       
  1127         },
       
  1128 
       
  1129         &#x2F;*
       
  1130          * renderUI is part of the lifecycle introduced by the
       
  1131          * Widget class. Widget&#x27;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&#x27;s not already 
       
  1141          * present in the markup), and creates the inc&#x2F;dec buttons
       
  1142          *&#x2F;
       
  1143         renderUI : function() {
       
  1144             this._renderInput();
       
  1145             this._renderButtons();
       
  1146         },
       
  1147 
       
  1148         &#x2F;*
       
  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 &quot;value&quot; attribute
       
  1160          *
       
  1161          * - Binds key listeners for the arrow&#x2F;page keys
       
  1162          * - Binds mouseup&#x2F;down listeners on the boundingBox, document respectively.
       
  1163          * - Binds a simple change listener on the input box.
       
  1164          *&#x2F;
       
  1165         bindUI : function() {
       
  1166             this.after(&quot;valueChange&quot;, this._afterValueChange);
       
  1167 
       
  1168             var boundingBox = this.get(&quot;boundingBox&quot;);
       
  1169 
       
  1170             &#x2F;&#x2F; Looking for a key event which will fire continuously across browsers while the key is held down. 38, 40 = arrow up&#x2F;down, 33, 34 = page up&#x2F;down
       
  1171             var keyEventSpec = (!Y.UA.opera) ? &quot;down:&quot; : &quot;press:&quot;;
       
  1172             keyEventSpec += &quot;38, 40, 33, 34&quot;;
       
  1173 
       
  1174             Y.on(&quot;key&quot;, Y.bind(this._onDirectionKey, this), boundingBox, keyEventSpec);
       
  1175             Y.on(&quot;mousedown&quot;, Y.bind(this._onMouseDown, this), boundingBox);
       
  1176 
       
  1177             this._documentMouseUpHandle = Y.on(&quot;mouseup&quot;, Y.bind(this._onDocMouseUp, this), boundingBox.get(&quot;ownerDocument&quot;));
       
  1178 
       
  1179             Y.on(&quot;change&quot;, Y.bind(this._onInputChange, this), this.inputNode);
       
  1180         },
       
  1181 
       
  1182         &#x2F;*
       
  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          *&#x2F;
       
  1189         syncUI : function() {
       
  1190             this._uiSetValue(this.get(&quot;value&quot;));
       
  1191         },
       
  1192 
       
  1193         &#x2F;*
       
  1194          * Creates the input control for the spinner and adds it to
       
  1195          * the widget&#x27;s content box, if not already in the markup.
       
  1196          *&#x2F;
       
  1197         _renderInput : function() {
       
  1198             var contentBox = this.get(&quot;contentBox&quot;),
       
  1199                 input = contentBox.one(&quot;.&quot; + Spinner.INPUT_CLASS),
       
  1200                 strings = this.get(&quot;strings&quot;);
       
  1201 
       
  1202             if (!input) {
       
  1203                 input = Node.create(Spinner.INPUT_TEMPLATE);
       
  1204                 contentBox.appendChild(input);
       
  1205             }
       
  1206 
       
  1207             input.set(&quot;title&quot;, strings.tooltip);
       
  1208             this.inputNode = input;
       
  1209         },
       
  1210 
       
  1211         &#x2F;*
       
  1212          * Creates the button controls for the spinner and add them to
       
  1213          * the widget&#x27;s content box, if not already in the markup.
       
  1214          *&#x2F;
       
  1215         _renderButtons : function() {
       
  1216             var contentBox = this.get(&quot;contentBox&quot;),
       
  1217                 strings = this.get(&quot;strings&quot;);
       
  1218 
       
  1219             var inc = this._createButton(strings.increment, this.getClassName(&quot;increment&quot;));
       
  1220             var dec = this._createButton(strings.decrement, this.getClassName(&quot;decrement&quot;));
       
  1221 
       
  1222             this.incrementNode = contentBox.appendChild(inc);
       
  1223             this.decrementNode = contentBox.appendChild(dec);
       
  1224         },
       
  1225 
       
  1226         &#x2F;*
       
  1227          * Utility method, to create a spinner button
       
  1228          *&#x2F;
       
  1229         _createButton : function(text, className) {
       
  1230 
       
  1231             var btn = Y.Node.create(Spinner.BTN_TEMPLATE);
       
  1232             btn.set(&quot;innerHTML&quot;, text);
       
  1233             btn.set(&quot;title&quot;, text);
       
  1234             btn.addClass(className);
       
  1235 
       
  1236             return btn;
       
  1237         },
       
  1238 
       
  1239         &#x2F;*
       
  1240          * Bounding box mouse down handler. Will determine if the mouse down
       
  1241          * is on one of the spinner buttons, and increment&#x2F;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          *&#x2F;
       
  1248         _onMouseDown : function(e) {
       
  1249             var node = e.target,
       
  1250                 dir,
       
  1251                 handled = false,
       
  1252                 currVal = this.get(&quot;value&quot;),
       
  1253                 minorStep = this.get(&quot;minorStep&quot;);
       
  1254 
       
  1255             if (node.hasClass(this.getClassName(&quot;increment&quot;))) {
       
  1256                 this.set(&quot;value&quot;, currVal + minorStep);
       
  1257                 dir = 1;
       
  1258                 handled = true;
       
  1259             } else if (node.hasClass(this.getClassName(&quot;decrement&quot;))) {
       
  1260                 this.set(&quot;value&quot;, currVal - minorStep);
       
  1261                 dir = -1;
       
  1262                 handled = true;
       
  1263             }
       
  1264 
       
  1265             if (handled) {
       
  1266                 this._setMouseDownTimers(dir, minorStep);
       
  1267             }
       
  1268         },
       
  1269 
       
  1270         &#x2F;*
       
  1271          * Override the default content box value, since we don&#x27;t want the srcNode
       
  1272          * to be the content box for spinner.
       
  1273          *&#x2F;
       
  1274         _defaultCB : function() {
       
  1275             return null;
       
  1276         },
       
  1277 
       
  1278         &#x2F;*
       
  1279          * Document mouse up handler. Clears the timers supporting
       
  1280          * the &quot;mouse held down&quot; behavior.
       
  1281          *&#x2F;
       
  1282         _onDocMouseUp : function(e) {
       
  1283             this._clearMouseDownTimers();
       
  1284         },
       
  1285 
       
  1286         &#x2F;*
       
  1287          * Bounding box Arrow up&#x2F;down, Page up&#x2F;down key listener.
       
  1288          *
       
  1289          * Increments&#x2F;Decrement the spinner value, based on the key pressed.
       
  1290          *&#x2F;
       
  1291         _onDirectionKey : function(e) {
       
  1292 
       
  1293             e.preventDefault();
       
  1294 
       
  1295             var currVal = this.get(&quot;value&quot;),
       
  1296                 newVal = currVal,
       
  1297                 minorStep = this.get(&quot;minorStep&quot;),
       
  1298                 majorStep = this.get(&quot;majorStep&quot;);
       
  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(&quot;max&quot;));
       
  1310                     break;
       
  1311                 case 34:
       
  1312                     newVal -= majorStep;
       
  1313                     newVal = Math.max(newVal, this.get(&quot;min&quot;));
       
  1314                     break;
       
  1315             }
       
  1316 
       
  1317             if (newVal !== currVal) {
       
  1318                 this.set(&quot;value&quot;, newVal);
       
  1319             }
       
  1320         },
       
  1321 
       
  1322         &#x2F;*
       
  1323          * Simple change handler, to make sure user does not input an invalid value
       
  1324          *&#x2F;
       
  1325         _onInputChange : function(e) {
       
  1326             if (!this._validateValue(this.inputNode.get(&quot;value&quot;))) {
       
  1327                 this.syncUI();
       
  1328             }
       
  1329         },
       
  1330 
       
  1331         &#x2F;*
       
  1332          * Initiates mouse down timers, to increment slider, while mouse button
       
  1333          * is held down
       
  1334          *&#x2F;
       
  1335         _setMouseDownTimers : function(dir, step) {
       
  1336             this._mouseDownTimer = Y.later(500, this, function() {
       
  1337                 this._mousePressTimer = Y.later(100, this, function() {
       
  1338                     this.set(&quot;value&quot;, this.get(&quot;value&quot;) + (dir * step));
       
  1339                 }, null, true)
       
  1340             });
       
  1341         },
       
  1342 
       
  1343         &#x2F;*
       
  1344          * Clears timers used to support the &quot;mouse held down&quot; behavior
       
  1345          *&#x2F;
       
  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         &#x2F;*
       
  1358          * value attribute change listener. Updates the 
       
  1359          * value in the rendered input box, whenever the 
       
  1360          * attribute value changes.
       
  1361          *&#x2F;
       
  1362         _afterValueChange : function(e) {
       
  1363             this._uiSetValue(e.newVal);
       
  1364         },
       
  1365 
       
  1366         &#x2F;*
       
  1367          * Updates the value of the input box to reflect 
       
  1368          * the value passed in
       
  1369          *&#x2F;
       
  1370         _uiSetValue : function(val) {
       
  1371             this.inputNode.set(&quot;value&quot;, val);
       
  1372         },
       
  1373 
       
  1374         &#x2F;*
       
  1375          * value attribute default validator. Verifies that
       
  1376          * the value being set lies between the min&#x2F;max value
       
  1377          *&#x2F;
       
  1378         _validateValue: function(val) {
       
  1379             var min = this.get(&quot;min&quot;),
       
  1380                 max = this.get(&quot;max&quot;);
       
  1381 
       
  1382             return (Lang.isNumber(val) &amp;&amp; val &gt;= min &amp;&amp; val &lt;= max);
       
  1383         }
       
  1384     });
       
  1385 
       
  1386     &#x2F;&#x2F; Create a new Spinner instance, drawing it&#x27;s 
       
  1387     &#x2F;&#x2F; starting value from an input field already on the 
       
  1388     &#x2F;&#x2F; page (the #numberField text box)
       
  1389     var spinner = new Spinner({
       
  1390         srcNode: &quot;#numberField&quot;,
       
  1391         max:100,
       
  1392         min:0
       
  1393     });
       
  1394     spinner.render();
       
  1395     spinner.focus();
       
  1396 });
       
  1397 &lt;&#x2F;script&gt;</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>