HTML forms are an essential part of interacting with web-page viewers,
and the <input>
tag is the most common interface tool used.
However, it gives little help to users that need to enter complex and/or specific data.
HTML5 introduces the <datalist>
element which can turn an
<input>
tag into a hybrid with a <select>
tag,
giving the user a pull-down menu to choose from without limiting the input data to only the menu items.
But sometimes that’s not enough…you want to present to your users
a complex table of data to choose from, or maybe some other source needs to generate the data on-the-fly,
such as a color-picker (like the MasterColorPicker project, for which this project was developed).
So here I present the <input type="picker">
project.
If an <input type="text">
tag is like a car,
and a <textarea>
tag is like a pickup truck,
and a <select>
tag is like a bus,
and an <input type="text">
with a <datalist>
is like a train,
then an <input>
with a Picker
interface is like an multi-city intergalactic spacecraft.
Using the Picker
Class with an <input type="text">
is simple.
Just as you would do using the <datalist>
tag, the “picker” HTML
is separate from the <input>
tag itself.
However, <datalist>
s are simple “lists”, and you may only associate one <datalist>
with each <input>
.
With the Picker
Class you may associate any number of “panels” (HTML elements),
and each panel may have any number of “pickers” along with any other HTML markup
and auxiliary displays and data, as well as “interface” elements − form controls
(such as <input>
s and <select>
s)
that modify the “pickers” themselves and/or their behaviors.
Furthermore, not only may you associate multiple “panels” and “pickers” with an <input>
,
you may associate multiple <input>
s with the same “panel”(s)/“picker”(s).
For instance, the MasterColorPicker project uses
8 “panels” and 14 “pickers” by default (at this time of writing),
and the Rainbow-Maker 2 project
associates the MasterColorPicker with many different <input>
s.
“Panels” and their “pickers” receive applied CSS class-names when “active”,
allowing the developer to pop-up and pop-out the picker HTML if desired,
or style it any way needed for complete flexibility.
“Panels” may be styled any way you choose using your custom CSS,
and if you have more than one panel, they may visually overlap on the display;
the Picker
Class manages an additional set of class-names which allows you to manage the
Z-index level of each panel, so the last one clicked on may be “brought to the top.”
The target <input>
(or <textarea>
but for simplicity we will just refer to inputs)
also needs to be kept in focus when you click on the Picker Interface “panel(s).”
Additionally, “panels” and “pickers” may be assigned user-defined event-handlers which are triggered
when they become “active” or “inactive” (onPickerStateChange
and onInterfaceStateChange
).
All this is managed by the Picker Class by a complex interaction − a dance − of event-handlers.
This is its primary function in life.
It would be easy to have a JavaScript function that
simply adds/replaces an <input>
’s value
with anything when you click on whatever…
And it’s easy to keep up with which <input>
is the one of many
of which to replace the old value with a new value.
The trick is keeping up with when an <input>
is “active”, especially when Picker-interfaces
need to scroll or if they need to use another HTML element that requires keyboard “focus.”
Generally, when an <input>
is associated with a Picker-interface,
the <input>
and Picker are considered “active” when the <input>
has “focus.”
If your interface itself needs to have an HTML element that requires keyboard “focus”,
including other <input type='text'>
s,
<input type='file'>
s, <input type='number'>
s,
<textarea>
s, <select>
s, etc.,
then they need to be “registered.”
The Picker
Class gives you all you need to manage all the event-handlers required to maintain the “focus”
of the target <input>
associated with a Picker-interface when necessary, and to keep track of
when the Picker-interface is “active”, and when it is not.
The comments in the code file explain all this.
But wait!
You don’t even need to use an <input type='text'>
with the Picker
Class.
HTML <textarea>
s, <select>
s,
<input type='text' list='datalistID'>
s,
and even document text-nodes are automatically recognized by the Picker_instance.pick()
method.
By default, the Picker_instance.dataTarget
receives the data-value picked by the end user.
If no dataTarget
is set then the Picker_instance.masterTarget
is used, if any.
The dataTarget
and masterTarget
each may be any
JavaScript™ Object.
Or they may be <input type='hidden'>
if you don't care to show the user the picked value,
but want to send the value back to the server with a form-submittal.
To supplement this functionality, add a JavaScript™ function to the
Array of Picker_instance.pickFilters
.
To replace this functionality, replace the Picker_instance.pick
method with your own function.
There is a plethora of comments embedded in the Picker
JavaScript™ code
which you should read if you want to use this package.
Beyond that, as usual a simple example does best to explain the usage of the Picker
Class Object.
Below you will see the code for a simple HTML Picker
example and the working demo
of this example code.
The MasterColorPicker™ project is a more complex example of
using the Picker
package.
If you study the MasterColorPicker2.js file, you will see the
x_ColorPicker
Class (found near the beginning of that file)
acts as an intermediary between the color-pickers and the Picker
Class.
At the end of each color-picker’s code section you will see the event-handlers attached to each color-picker,
and how they interact with the x_ColorPicker
class to “pick” colors;
and the YinYang NíHóng™ Color Picker code shows an advanced example of
using the onPickerStateChange
event.
At the end of the MasterColorPicker2.js file you will see how the
MasterColorPicker
Object (which is used by the x-ColorPicker
Class)
is created from an instance of the Picker
Class,
an example of how the pickFilter
works, and another example of using the onPickerStateChange
event.
<style>
label {
display: inline-block;
position: relative;
margin-bottom: 4em; }
label .pickerPanel {
position: absolute;
top: 1.1618em;
right: -10.618em; }
.pickerPanel {display: none;}
.activePicker {display: table;}
</style>
A very simple example:
<label>Pick a color <input type='text' id='getColor' />
<table id='colorPicker'>
<caption>Colors</caption>
<tr>
<td style='background-color:red'>red</td>
<td style='background-color:orange'>orange</td>
<td style='background-color:yellow'>yellow</td>
<td style='background-color:green'>green</td>
<td style='background-color:cyan'>cyan</td>
<td style='background-color:blue'>blue</td>
<td style='background-color:violet'>violet</td>
<td style='background-color:indigo'>indigo</td>
</tr>
</table></label>
<script type='text/javascript'>
// note how you can use the required/included “UniDOM” package to add event handlers, or add them any other way.
UniDOM.addEventHandler(window, 'onload', function() {
var itybitHTML=document.getElementById('colorPicker'),
itybitColorPicker=new SoftMoon.WebWare.Picker(itybitHTML), // no options needed
itybitColorInput=document.getElementById('getColor'),
itybitColors=itybitHTML.getElementsByTagName('td'),
selector=function() {itybitColorPicker.pick(this.firstChild.data);};
for (var i=0; i<itybitColors.length; i++) {
UniDOM.addEventHandler(itybitColors[i], 'onclick', selector); }
itybitColorPicker.registerTargetElement(itybitColorInput); });
</script>
Basically the same, but with two different target-inputs, and a relocating Picker. Note the difference in the way the
dataTarget <input>
s are registered:
<div id='relocatableColorPicker'>
<label>Foreground Color:<input type='text' /></label>
<label>Background Color:<input type='text' /></label>
</div>
<script type='text/javascript'>
UniDOM.addEventHandler(window, 'onload', function() {
var itybitHTML=document.getElementById('colorPicker').cloneNode(true), // we might instead use removeNode but we keep the original because it is used in the previous example.
itybitColorPicker=new SoftMoon.WebWare.Picker(itybitHTML), // no options needed
cInputs=document.getElementById('relocatableColorPicker').getElementsByTagName('input'),
itybitColors=itybitHTML.getElementsByTagName('td'),
selector=function() {itybitColorPicker.pick(this.firstChild.data);};
for (var i=0; i<itybitColors.length; i++) {
UniDOM.addEventHandler(itybitColors[i], 'onclick', selector); }
for (var i=0; i<cInputs.length; i++) { // ↓↓note we pass in the <label> for each input as the containing element for the picker.
itybitColorPicker.registerTargetElement(cInputs[i], cInputs[i].parentNode); } });
</script>
As mentioned above, the Picker
Class manages more complex situations
where the Picker HTML Interface needs to scroll (using a scroll-bar).
The idea of the Picker
Class is that when you click anywhere on the Picker HTML Interface,
“focus” is retained by the target <input>
, even if nothing is “picked.”
Click on the caption of the itybitColorPicker above (where it says “Colors”),
or anywhere on the yellow background of the song-picker below, as a demonstration of this principle.
This was the trick in older browsers, because often times clicking on a scroll-bar did not
generate an onclick
event.
Focus is taken away from the target <input>
, and it needs to be “refocused.”
The Picker
Class needs to keep track of where the mouse is,
so when a scroll-bar is clicked on this situation can be remedied.
Sometimes with older browsers the “onclick” event for the Picker HTML comes before the
“onblur” event for the <input>
, sometimes the other way around.
If the “blur” occurs first, depending on your CSS rules of course, the Picker HTML
may be hidden before the “click” happens;
then the click does not register on the Picker, but rather on whatever is underneath.
The older version of Picker
Class remedies this by keeping track of when the mouse is hovering over a “Picker panel,”
and also when it is hovering over the first-of-kin children.
If it is over the “panel,” but not over any of the children, then it must be over the scroll-bar.
If a registered target <input>
looses focus,
it will automatically refocus when the mouse is hovering over said scroll-bars.
At this point it might be thought “why not simply refocus if the mouse is over the Picker panel in general?”
That then voids the “onclick” event; it simply then never occurs and nothing gets “picked” —
at least in some browsers under some conditions;
I pulled hair for a while when the original prototypes for the Picker
code would work and then wouldn’t,
so I would modify the way the handlers interacted, and that would work and then not…
Anyway, the “onclick” event on a picker-panel then refocuses the dataTarget <input>
.
That was all a mess that still sometimes failed in complex situations.
With newer HTML5 browsers, multiple events and event-handlers have more of a “defined” order of occurance, based (at least) on the order that handlers are registered. The “dance of event-handlers” is by definition random in the old DOM standard. The latest version of the Picker Class works only with the newer browsers, and without needing to keep track of the mouse.
If you want to use more that one “panel”
(i.e. more than the mainPanel
,
which is passed in as the first (required) argument when creating a new Picker
instance),
you need to “register” the additional panels,
the same as you need to “register” interface control-elements
(see above, and also the comments in the Picker
code file).
Registering an additional panel (besides the mainPanel
) also automatically
registers any “interface control-elements” within that panel.
If you want to use more than one “picker,” the Picker
Class can manage that also,
provided all the “pickers” reside within these “registered”
PickerInstance.panels
(including the mainPanel
).
The options
Object that can be passed in as a
second argument when creating a new Picker
instance
may hold a pickerSelect
property that references either a <select>
tag
or an array of <input type='checkbox‖radio'>
buttons that can control which “Pickers”
are considered “active.”
You should also probably “register” your pickerSelect
if it is a <select>
tag,
assuming you want to keep the “focus” of the target <input>
while selecting different pickers.
<style>
#songs .picker {
display: none; }
#songs .activePicker {
display: table; }
#songs {
padding: 2em 0 .618em 0;
margin: .618em 12.618em;
position: relative; }
#songPickerControls.activePicker {
position: absolute;
top: 1.1618em;
left: 0;
display: block;
width: 100%;
text-align: center;
background-color: yellow; }
#songs label {
margin: 0 .618em; }
#songPickerHTML {
background-color: yellow;
border: 2px solid blue; }
</style>
<fieldset id='songs'><legend>Pick 3 songs</legend>
<label>1. <input type='text' /></label>
<label>2. <input type='text' /></label>
<label>3. <input type='text' /></label>
<div id='songPickerHTML'><!-- this is the picker’s mainPanel -->
<table id='PinkFloyd' class='picker rock'><caption>Pink Floyd</caption>
<tr><th>song</th><th>from album</th></tr>
<tr><td>Set the Controls for the Heart of the Sun</td><td>A Saucerful Of Secrets</td></tr>
<tr><td>Main Theme</td><td>More</td></tr>
<tr><td>Echoes</td><td>Meddle</td></tr>
</table>
<table id='GratefulDead' class='picker rock'><caption>Grateful Dead</caption>
<tr><th>song</th><th>from album</th></tr>
<tr><td>Bertha</td><td>Grateful Dead</td></tr>
<tr><td>China Cat Sunflower</td><td>Aoxomoxoa</td></tr>
<tr><td>Eyes Of The World</td><td>Wake Of The Flood</td></tr>
</table>
<table id='LedZeppelin' class='picker rock'><caption>Led Zeppelin</caption>
<tr><th>song</th><th>from album</th></tr>
<tr><td>Whole Lotta Love</td><td>Led Zeppelin II</td></tr>
<tr><td>Stairway to Heaven</td><td>Led Zeppelin IV</td></tr>
<tr><td>In The Light</td><td>Physical Graffiti</td></tr>
</table>
<table id='MilesDavis' class='picker jazz'><caption>Miles Davis</caption>
<tr><th>song</th><th>from album</th></tr>
<tr><td>Flamenco Sketches</td><td>Kind Of Blue</td></tr>
<tr><td>Shhh/Peaceful</td><td>In A Silent Way</td></tr>
<tr><td>Pharaohs Dance</td><td>Bitches Brew</td></tr>
</table>
<table id='TheloniusMonk' class='picker jazz'><caption>Thelonius Monk</caption>
<tr><th>song</th><th>from album</th></tr>
<tr><td>Bright Mississippi</td><td>Monks Dream</td></tr>
<tr><td>Im Confessin (That I Love You)</td><td>Solo Monk</td></tr>
<tr><td>Ugly Beauty</td><td>Underground</td></tr>
</table>
<table id='TedeschiTrucksBand' class='picker blues'><caption>Tedeschi Trucks Band</caption>
<tr><th>song</th><th>from album</th></tr>
<tr><td>Dont Let Me Slide</td><td>Revelator</td></tr>
<tr><td>Love Has Something Else To Say</td><td>Revelator</td></tr>
<tr><td>Learn How To Love</td><td>Everybodys Talkin</td></tr>
</table>
<table id='PinkAnderson' class='picker blues'><caption>Pink Anderson</caption>
<tr><th>song</th><th>from album</th></tr>
<tr><td>Papas About To Get Mad / Gonna Tip Out Tonight</td><td>(single)</td></tr>
<tr><td>Every Day In The Week Blues / C.C And O. Blues</td><td>(single)</td></tr>
<tr><td>Greasy Greens</td><td>American Street Songs</td></tr>
</table>
<table id='FloydCouncil' class='picker blues'><caption>Floyd Council</caption>
<tr><th>song</th><th>from album</th></tr>
<tr><td>I Dont Want No Hungry Woman</td><td>Carolina Blues</td></tr>
<tr><td>Lookin For My Baby</td><td>Carolina Blues</td></tr>
<tr><td>Workin Man Blues</td><td>Carolina Blues</td></tr>
</table>
</div><!-- close songPickerHTML -->
<div id='songPickerControls'>
<label>Style: <select id='musicStyle'>
<option selected='selected'>rock</option>
<option>jazz</option>
<option>blues</option>
</select></label>
<label>Artist: <select id='artists'>
<option class='rock' selected='selected'>Pink Floyd</option>
<option class='rock'>Grateful Dead</option>
<option class='rock'>Led Zeppelin</option>
<option class='jazz'>Miles Davis</option>
<option class='jazz'>Thelonius Monk</option>
<option class='blues'>Tedeschi Trucks Band</option>
<option class='blues'>Pink Anderson</option>
<option class='blues'>Floyd Council</option>
</select></label>
</div>
</fieldset>
<script type='text/javascript'>
UniDOM.addEventHandler(window, 'onload', function() {
var songsHTML=document.getElementById('songPickerHTML'),
musicStyle=document.getElementById('musicStyle'),
artistSelect=document.getElementById('artists'),
sInputs=document.getElementById('songs').getElementsByTagName('input'),
songs=songsHTML.getElementsByTagName('tr'),
SongPicker=new SoftMoon.WebWare.Picker(songsHTML, {picker_select: artistSelect),
selector=function() {SongPicker.pick(this.firstChild.data);},
setStyle=function(onBoot) {
for (var i=0, flag=false; i<artistSelect.options.length; i++) {
artistSelect.options[i].disabled=!(this.value===artistSelect.options[i].className);
if (!flag) artistSelect.options[i].selected=(flag=(this.value===artistSelect.options[i].className)); }
if (onBoot!==true) SongPicker.choosePicker(); };
for (var i=0; i<songs.length; i++) { //attach event handlers to each song in every table
if (songs[i].firstChild.nodeName==='TD') UniDOM.addEventHandler(songs[i].firstChild, 'onclick', selector); }
for (i=0; i<sInputs.length; i++) { // ↓↓note we pass in the <label> for each input as the containing element for the picker.
SongPicker.registerTargetElement(sInputs[i], sInputs[i].parentNode); }
SongPicker.registerInterfacePanel(document.getElementById('songPickerControls'));
UniDOM.addEventHandler(musicStyle, 'onchange', setStyle);
setStyle.call(musicStyle, true); });
</script>
The above example need not use two panels, and realistically complicates a simple interface. But if you were to include items such as dates, publishers, playtimes, etc. into the tables in this example, you may want to filter the possible song selections using any number of factors. In a real-world shopping-cart project, having these filters in a separate panel from the “pickers,” maybe even one that is always shown to the end-user, can be an optimal way of organizing your HTML page.
The version 2+ release of the Picker Class adds even better support for “interface Elements” in the
pickers you develop. Tab-key support within interface Elements has been expanded,
and a new concept of “interface Targets” is now available for use.
With version 2+ of the Picker Class, the non-standard HTML attributes
“tabToTarget” (same as Version 1 release) and
“backTabToTarget” may have values of "true"
or "false"
.
When registering an “interface panel” the first and last “interface Elements” found on the panel
by default are registered to backtab-to-target and tab-to-target, respectively.
You can override this with the HTML attributes markup,
or with JavaScript™ by “registering” them seperately
with the desired options before registering the panel they are on (see the comments in the code-file).
For more complex tab-control needs, HTML of course gives you the tabIndex
attribute, but this can be confusing at best sometimes. If you are creating a plugin that should work within
any web-page, how can you be sure that your tab-indexes are not going to conflict with what the develper wants,
or with any other plugin?
Since registering an “interface Element” interferes with the tab-key control by nessesity,
I thought it best to fully integrate tab-key direction within this project for Version 2.
This allows so much more flexibility and dynamicy in developing your Picker,
with multiple panels that may be added/removed according to needs (perhaps loaded via Ajax on demand).
So two more additional HTML attributes are now recognized by the Picker Class
for “registered interface Elements”: “tabTo” and “backTabTo”.
Their values may be either an HTML “id” of the Element to tab to,
or a JavaScript™ expression that evaluates to the Element to tab to.
Users may also want to manually tab from one panel to another.
By default they could now do this using CTRL+Tab
and SHIFT+CTRL+Tab
except for that most browsers block these key-combos from JavaScript™.
So by default the Picker class also recognizes CTRL+<
and CTRL+>
(on QUERTY keyboards this would actually be
SHIFT+CTRL+,< and
SHIFT+CTRL+.>)
to serve this need.
On non-QUERTY keyboards, the < and > keys
may normally generate characters when pressed in combo with the CTRL key,
and the Picker’s default panel-tab control may then conflict (and not work!).
You may alter the values found in:
pickerInstance.panelTabKey
and pickerInstance.panelBacktabKey
which are UniDOM.KeySniffer
objects.
A value of true
or false
for a modifier-key means that key
must be in the indicated state.
A value of undefined
for a modifier-key means the KeySniffer doesn’t care.
You may want your “interface Element” to remain focused when it has focus
when the user presses the ↵Enter key;
or maybe you want keyboard focus to return to your data-target (<input>
).
The Boolean value found at pickerInstance.doKeepInterfaceFocus
controls that default action;
but your “interface Element” itself may have an attribute keep-focus
and this may have a Boolean value that overrides the pickerInstance
default.
For HTML <button>
“interface Elements”
the Picker also adds a customized buttonpress
event
when the <button>
has keyboard focus
and the user presses the ↵Enter key.
“Interface Targets” are special “interface Elements” of your Picker that can receive a value “picked” from another part of your Picker, where normally a user “picks” something for the “data Target”. For instance, the MasterColorPicker Version 2 project has a “color filter” that filters the color a user “picks” from one of the palettes, modifying the “picked” color by mixing in the “filter-color”. The user must first choose this filter-color, by picking a color from one of the palettes. So the color-filter panel has an “interface Target” to recieve the filter-color picked. As usual, the Picker Class makes sure keyboard focus is directed to the proper Element at all times, as well as properly managing the “top panel” under these shifting conditions.