View
 

Spinner

Page history last edited by jerone 5 mos ago

 

type: widget

realease: ---

status: in planning

documentation: http://docs.jquery.com/UI/Spinner

demo: http://jquery-ui.googlecode.com/svn/branches/labs/spinner/demos/index.html#spinner|default

 


 

1 - Description:

 

A spinner (stepper) is a simple widget that allows users to increment or decrement the current text box value without having to input it manually. Although a spinner is typically used to adjust numeric values by whole numbers, it could also be used for non-numeric list values such as letter grades (A, A-, B+, B, B-, etc.),  stock risk rating (AAA, AA, A, etc.), or to toggle AM/PM.  Increments do not have to be whole numbers -- it could be set to decimal values (0.1) or large increments (5) for each click. For a detailed discussion of Spinner behavior, please read:

 

Apple Human Interface Guidelines: Stepper (Little Arrows)

MSDN UI Guidelines: Spin Controls

 

There is currently a spinner control in jQuery UI that will be re-factored as part of this re-design:

http://docs.jquery.com/UI/Spinner

 

 

Proposed features:

  • holding the mouse button down on an arrow for an extended period (>0.5 seconds) should start a repeating increment, much like holding down a key on a keyboard
  • the increment repeater could be linear (1,2,3,4,5,6...) or set to repeat at higher increments (1,2,3,4,5,10,15,20...) which can be helpful for allowing for quick movement through a large range shift.
  • the input box may have multiple focus points and essentially be several spinners combined into one.  Example: instead of using three separate spinners for hours/minutes/seconds, the input can contain all three (hh:mm:ss) and the user can focus on/change one segment at a time: http://www.filamentgroup.com/examples/timepicker/
  • we should consider building masked input features into editable spinners to help ensure valid entries: http://digitalbush.com/projects/masked-in
  • the spinner control should also be flexible enough to support different international numbering systems. e.g. $1,000.50 or RUB1.000,50 - This could be something the masked input feature handles or something build-in for internationalisation support.
  • maxlength attribute could be used to restrict character length.
  • support spinning via drag. This could be achieved by adding a small divider between the spinner buttons. This will allow the user to press and drag the divider up/down to change the spinner value. We could also spin according to speed of drag? This is a good example of spinning via drag: http://members.upc.nl/j.chim/ext/spinner2/ext-spinner.html

 

Spinners are frequently used in a situation where there is a 'masked input' which essentially breaks what looks like a single text into multiple regions or zones that may have custom formatting or constraints. Although this is not technically part of the core spinner control, it needs to be considered by the developer so it will work in this situation. A good example of this is a time picker which has 3 zones that are all controlled by a single spinner. See a working example (recommended by Filament Group) here:

http://www.filamentgroup.com/examples/timepicker/

 

A masked input (for use with editable spinners) plug-in is here:

http://digitalbush.com/projects/masked-input-plugin/

 

Brant Burnett coded a slick version that uses the CSS framework, DHTML styleguide for keyboard shortcuts and the widget factory so it's really far along, needs code review but the CSS and design are perfect:

http://btburnett.com/spinner/example/example.html

http://github.com/btburnett3/jquery.ui.spinner

 

An example of good keyboard interaction with support for other formats such as date and time:

http://members.upc.nl/j.chim/ext/spinner2/ext-spinner.html

 


 

2 - Visual Design:

 

 

 


 

3 - Functional Specifications/Requirements:

 

A spinner is useful not only as a self standing widget but also as an integrated part of more complex widgets such as colorpickers, timepickers and datepickers. Hence the spinner should be made as extensible as possible and flexible enough to merge seamlessly into other widgets. 

 

The DHTML style guide suggests making the right/left arrows increment/decrement the value as well, however this would make it difficult for a user to change the value of a multi-digit number manually via the text field.

 

When the spinner is not read only, the user may enter a value via the text field that varies from the scale. On manual input by a user the spinner will constrain input to the allowable characters but will not do validation on the actual inputted value, i.e. If min=1, max=100, step=10, user enters 3 and presses the up button the result will be 13.

 

 

The advanced spinner will use menu for dropdowns and slider for the scrubber.

 

Key:

Implemented

In Development

Not Implemented

 

Options:

Essential:

  • max (number, default: null)
    • maximum value allowed
    • element's max attribute used if null; falls back to Number.MAX_VALUE if it cannot be determined.
    • can be passed as string which will be parsed based on radix and radixPoint options.
    • if the max attribute exist when the plugin in instantiated, the attribute will be updated as the plugin's state is manipulated.
  • min (number, default: null)
    • minimum value allowed
    • element's min attribute used if null; falls back to -Number.MAX_VALUE if it cannot be determined.
    • can be passed as string which will be parsed based on radix and radixPoint options.
    • if the min attribute exist when the plugin in instantiated, the attribute will be updated as the plugin's state is manipulated.
  • page - page size (default 5 steps) 
    • page is defined as the number of steps. Example: If step=2 and page=5, the resulting value of a pageUp will be 10.
    • can be passed as string which will be parsed based on radix and radixPoint options.
    • required for pageUp/Down methods
  • spinnerClass (string, default: null)
    • when specified, adds a class to the generated wrapper.
  • step (number, default: null)
    • size of step to take when spinning via the button or via the stepUp()/stepDown() methods.
    • element's step attribute used if null; falls back to 1 if it cannot be determined.
    • can be passed as string which will be parsed based on radix and radixPoint options.
    • if the step attribute exist when the plugin in instantiated, the attribute will be updated as the plugin's state is manipulated.
  • value (number, default: null)
    • current value
    • can be passed as string which will be parsed based on radix and radixPoint options.
    • element's value attribute used if null; falls back to 0 if it cannot be determined.
    • if the value attribute exist when the plugin in instantiated, the attribute will be updated as the plugin's state is manipulated.
    • if the value is empty (not null) the spinner will start with an empty value.
  • width (pixel value, default: false)
    • width of spinner input box.

Important 

  • buttons - auto-hide buttons when not in focus (default show)
    • Accepted values are show|auto|hide|fast|slow|(\d+), where the default "show" just shows the buttons, "hide" always hides the buttons, "auto" hides them when the widget doesn't have focus (or mouseover) and fast, slow or integer customises the speed.
  • dir - text direction (default 'ltr')
    • This is used to set text direction and to reflow the input and button elements - the input element is always positioned infront of the buttons so if text direction changes from right-to-left then the buttons will be positioned towards the left of the input box.

Nice to have:

  • incremental (default false)
    • If incremental is set to true the stepping delta will increase when spun incessantly. i.e. spinning jumps up a notch at set increments.
  • items - accepts an array of objects for item list spinning (default null).
  • mouseWheel - enables mouse wheel support (default true).

Less important:

  • format - This is used for masked inputs. i.e. time - "%(h):%(i):%(s)", date - "%(mm)/%(dd)/%(yyyy)", currency - "$%([0-9])", custom segments - "%([0-9])-%([A-Z])" etc. Each %(name) denotes a spinnable segment. (I initially used [...] to denote a spinnable segment but square brackets are useful for cases where we want to use regex.) 
  • groupSeparator - The symbol to use as a thousand separator / digital group separator. (for internationalization support)
  • padding - Left pad the number with zeroes until it reaches this length.
    • Counts the decimal point and numbers after the decimal (defined in precision), but ignores the negative sign.
  • precision - The number of decimal places displayed.
  • radix - number base of the spinner (i.e. 16 for hexadecimal) (default 10)
  • radixPoint - The symbol to use as a decimal point. (for internationalization support)
  • currency (default false) - The symbol to use as a currency prefix.

 

Callbacks (listed in sequence):

  • start (event name: spinstart)
    • original event types: mousedown, keydown
    • triggered before a spin.
    • cancelable and prevents stop and change from firing if canceled.
  • spin (event name: spin)
    • original event types: mousedown, keydown
    • triggered during increment/decrement (to determine direction of spin compare current value with ui.value).
    • cancelable and prevents stop and change from firing if canceled.
  • stop (event name: spinstop)
    • original event types: mouseup, keyup
    • triggered after a spin.
  • change (event name: spinchange)
    • original event types: click, keyup
    • triggered when spinning completes.

 

Methods:

  • disable()
    • disables the widget and the input.
    • adds a disabled attribute to the input
    • the widget becomes non-interactive and prevents its value from being submitted but does not prevent scripted actions.
  • enable()
    • enables the widget and the input.
  • pageDown([pages])
    • decrements the currently active segment by the specified number of pages, as defined by the page-option.
    • the pages parameter defaults to 1.
    • follows same rules as stepDown().
  • pageUp([pages])
    • increments the currently active segment by the specified number of pages, as defined by the page-option.
    • the pages parameter defaults to 1.
    • follows same rules as stepUp().
  • stepDown([steps])
    • decrements the currently active segment by the specified number of steps.
    • the steps parameter defaults to 1.
    • if the number of steps is omitted, a single step will be taken.
    • if the resulting value is above the max or below the min, the value will be adjusted to the closests max or min.
  • stepUp([steps])
    • increments the currently active segment by the specified number of steps.
    • the steps parameter defaults to 1.
    • if the number of steps is omitted, a single step will be taken.
    • if the resulting value is above the max or below the min, the value will be adjusted to the closests max or min.
  • value([value])
    • Getter/setter for the current value.
    • If the value parameter is omitted, the method returns the current value.
    • If the value parameter is provided, the method sets the spinner's value to the value provided.
    • For a single segment spinner, should just return/accept the value.  For multiple segments, should return/accept an array of values.

 

Keyboard:

  • HOME - set to min value.
  • END - set to max value.
  • UP - increment with step size.
  • DOWN - decrement with step size.
  • PGUP and SHIFT+UP - increment with page step size.
  • PGDN and SHIFT+DOWN - decrement with page step size. 

 


 

4 - Markup & Style:

 

     4.1 Initial markup examples

 

4.1.1 Default / Alternate

 

<input type="text" id="s1" maxlength="2" value="10" />

 

4.1.2 Advanced

 

<select id="s2">

     <option class="ui-marker">0</option>

     <option>10</option>

     <option>20</option>

     <option>30</option>

     <option>40</option>

     <option class="ui-marker">50</option>

     <option>60</option>

     <option>70</option>

     <option>80</option>

     <option>90</option>

     <option class="ui-marker">100</option>

</select>

 

4.1.3 HTML5

<input type="number" id="s3" maxlength="2" min="0" max="100" value="0" step="5" />

 

     4.2 Recommended transformed HTML markup

 

4.2.1 Default / Alternate

 

<div id="ui-spinner-s1" class="ui-spinner" role="spinbutton" aria-valuemax="100" aria-valuemin="0" aria-valuenow="10">

     <input class="ui-spinner-box" type="text" id="s1" value="10" autocomplete="off" />

     <a class="ui-spinner-up" href="#"><span>â–²</span></a>

     <a class="ui-spinner-down" href="#"><span>â–¼</span></a>

</div>

 

4.2.2 Advanced

 

<div id="ui-spinner-s2" class="ui-spinner" role="spinbutton" aria-valuemax="100" aria-valuemin="0" aria-valuenow="40">

     <input class="ui-spinner-box" type="text" id="s2" value="40" autocomplete="off" />

     <a class="ui-spinner-up" href="#"><span>â–²</span></a>

     <a class="ui-spinner-down" href="#"><span>â–¼</span></a>

    

     <!-- THIS SECTION REQUIRES MORE THOUGHT -->

     <div class="ui-spinner-slider">

          <div class="ui-spinner-slider-scubber-bar">

               <button class="ui-spinner-slider-scubber-handle" type="buttom"><span>ï¿­</span></button>

          </div>

          <ul class="ui-spinner-slider-labels">

               <li class="ui-spinner-slider-marker">10</li>

               <li>20</li>

               <li>30</li>

               <li class="ui-spinner-label-selected">40</li>

               <li class="ui-spinner-slider-marker">50</li>

               <li>60</li>

               <li>70</li>

               <li>80</li>

               <li>90</li>

               <li class="ui-spinner-slider-marker">100</li>

          </ul>

     </div>

 

</div>

 

     4.3 Accessibility recommendation

 

ARIA attributes:

  • role = spinbutton
  • valuemin = value of min attribute
  • valuemax = value of max attribute
  • valuenow = current value

Keyboard functionality described above in section three.

Focus should stay in the edit field, even after using the mouse to click one of the spin buttons.

 

     4.4 CSS & Theme

 

 

 


 

5 - Latest version of plugin:

 

 

 


 

6 - Open issues being discussed

 

  • How to tie a spinner into a masked input like the time picker -- need to define mechanisms, but should not include masked input as part of the actual spinner widget (keep them separate but connectable)

    Current thoughts: Provide global settings in $.ui.spinner.types, each value (such as h, m, etc) having an object that defines parameters of that type of value (range of valid values, number of decimal places, left zero padding, list of items such as am/pm, etc).  Then the user can extend it with different types for their purposes (segments of latitude and longitude, for example).  These types can then be referenced by the format string, each one being referenced acting as an independent spin zone.  We can also allow you to override the global $.ui.spinner.types class in options to define types that are specific to a widget rather than global to the page, and of course common types like time segments would be predefined.

     
  • Should we make the Spinner extendible enough to support other numbering systems or use masked input for this aswell.
    • A proposed format: This is used for masked inputs. i.e. time - "%(h):%(i):%(s)", date - "%(mm)/%(dd)/%(yyyy)", currency - "$%([0-9])", custom segments - "%([0-9])-%([A-Z])" etc.
    • Should the spinner support multiple segments or delegate all to a maksed input widget?
    • Can we figure out how to make segment spinning work or is this too complex to cover as a core spinner use case?
    • Is the ability to increment 2 digit R,G and B numbers an edge case? If so would it be better to leave this for extension?
    • Parse everything out of the format field might be a bit too complicated. Especially since each segment could have its own minimum and maximum, or a set of options like am/pm. An option would be to pass in the parameters for each segment (min, max, step, list of values) and then use the formatter to apply prefixes, suffixes, separators, etc. I.e. '%0:%1 %2' where %0 is restricted as 1-12, %1 is 0-60, and %2 is am or pm. We could add a segment option that takes the restrictions as an array, i.e. [ {min: 1, max: 2, size: 2}, {min: 0, max:60, size: 2}, {items: ['am', 'pm']} ].
    • Another proposed format is PHP's date function: http://php.net/manual/en/function.date.php
    • In the case of 12 hour or 24 hour clocks the formatter used would be different, 12 hour = %(h), 24 hour = %(H). am/pm could be handled by %(a).
    • Each placeholder could assign various criteria in an external formatter script to facilitate localisations of time simply by providing an i18n js file (much like what datepicker) for that language.
    • The big quetsion is how much of this we implement into core leaving the rest to extension?

       

  • Progressive increments.

    To improve support for configurable progressive increments and make it really smooth we could experiment with easing equations. The default easing equation would be "linear", i.e. same as options.incremental=false. We could then specify progressive increments like incremental="easeIn", incremental="easeOut".

     

  • readOnly option

    - How is this different to disabled?

    - Should any methods or interaction work?

    - Should clicking on up/down arrows/keys change the value?

    - Does the spinner need to know about readonly states or is the browser's native interaction good enough?

     

 

Comments (14)

profile picture

Ca-Phun Ung said

at 10:48 am on Sep 5, 2009

<b>Todd Parker</b> - In many other spinners, the text string is selected as you spin, but in our spinner the cursor sits at the end of the value. I don't know what is the more common behavior that is expected, but that might be worth tweaking to be as consistent as possible to standard OS controls. Windows seems to do this highlight behavior: http://msdn.microsoft.com/en-us/library/aa511491.aspx

profile picture

Ca-Phun Ung said

at 10:52 am on Sep 5, 2009

Is this worth looking into?

profile picture

Ca-Phun Ung said

at 10:56 am on Sep 5, 2009

Ca-Phun Ung says:

"destroy/re-attach tests - $('.selector').spinner().spinner('destroy').remove() produces this error "this.parentNode is null". It's related to the wrapper we have around this.element and I'm not sure of the best way to fix this."

Again should I ignore this for now?

profile picture

Scott González said

at 10:47 am on Sep 6, 2009

I fixed the problem in trunk and updated the spinner branch. Now test #3 fails because .replaceWith() doesn't work when disconnected from the DOM. We should probably start a jquery-ui-dev thread about this (should initializing a widget automatically append it to the DOM?) I'm not sure if we have a precedent yet.

profile picture

Ca-Phun Ung said

at 11:31 am on Sep 6, 2009

Just committed a fix for disconnected spinners. Test #3 now passes. What I did was check if uiSpinner.parent() is null and return a clone of the original element if so. It seems to work well in both disconnected and connected spinners but I'm not sure this is the correct permanent fix we're looking for.

profile picture

ajpiano said

at 12:20 pm on Sep 6, 2009

Dialog automatically appends to the DOM upon initialising, so there is at the very least some precedent for it happening.

profile picture

sompylasar said

at 4:01 pm on Sep 6, 2009

I think a widget should not push itself into DOM unless explicitly specified. And it should handle disconnected situations correctly because the only parts I see depending on widget parent are insertion and removal.

profile picture

sompylasar said

at 4:02 pm on Sep 6, 2009

...insertion and removal of the whole widget.

profile picture

Scott González said

at 6:18 pm on Sep 6, 2009

Widgets that are never connected to the DOM are unusable. Also, I've seen various issues crop up because elements aren't forced into the DOM on plugin initialization. Here's an example of a problem I've run into recently:

CSS:
.foo { position: absolute; }

JS:
$('<div></div>')
.addClass('foo')
.draggable()
.appendTo('body');

Expected result: absolutely positioned draggable.
Actual result: relatively positioned draggable.

The problem is that draggable checks the positioning of the element on init and sets it to relative if it's not absolute or fixed. Even though the element has a class that gives it absolute positioning, the CSS doesn't take affect until the element is inserted into the DOM.

profile picture

sompylasar said

at 2:04 pm on Sep 7, 2009

Well, good test case. Then the widget factory should deny widget instantiation if an element is disconnected. This sounds reasonable.

Just to be sure: will the widget be destroyed if I (temporery) remove this.element from the DOM in a widget method code? Never tested, seems to work as expected - destroy is not called - but I start to hesitate after reading this.element 'remove' handler that is automatically attached.

profile picture

Scott González said

at 4:21 pm on Sep 7, 2009

The widget factory definitely should not prevent initialization just because an element is disconnected from the DOM. I was simply making a case for why it should force the element into the DOM.

And yes, removing an element from the DOM will disconnect the plugin instance from the DOM element (though it won't actually delete the plugin instance).

profile picture

sompylasar said

at 4:41 am on Sep 6, 2009

Other glitch (or feature?) in default, donation spinner demos (or in the widget?). Clicking up/down buttons spins two steps, not one.

This should be the problem of handling both 'mousedown' and 'click' events. 'Mousedown' event handler starts continuous spin and does the first increment/decrement, then 'click' event handler does the second step.

profile picture

Ca-Phun Ung said

at 11:51 am on Sep 6, 2009

The spinner doesn't handle click events, only mouse(down/up/wheel) are handled. I think this may be a mouse sensitivity issue. When I click on the buttons the spinner spins 1 step. Only if I hold the mouse down longer than usual it'll do the two or more steps.

profile picture

sompylasar said

at 3:55 pm on Sep 6, 2009

Maybe. But this is still an issue. I'm not able to do one step at a time using the buttons.

You don't have permission to comment on this page.