This JavaScript™ powered multifunctional class allows you to create an end-user-manipulatable HTML form that requires an unknown number of textual-input / file-input form fields. When you don’t want to limit your visitors to a minimal number of input options, but don’t want your page display to be bloated with a large number of empty user-input boxes, this is the utility you need. An end-user can even cut, copy, and paste entire sections of an HTML form to/from multi-clip clipboards, if you set up your Genie and HTML to do so. We use this class in our ►MasterColorPicker package, and you can see useful working integrated examples there (in the “MyPalette” and “Gradientor” sub-applications, and the “Color Filter” extension).
¿What do you use this for? When you have to ask someone to list each of something you request, and they may need one (1) form field, or they may need fifty (50), or anywhere in between or even more! Consider asking about someone’s children and grandchildren. In one family there may be two children, each with one grandchild. What about a large family with 7 children, each with an average of 5 grandchildren? Do you create a form that accommodates the large family, forcing the small family to wade through your web page like trying to navigate swamp water? And what about the family with 12 children and 78 grandchildren? Of course there are functions that say “click this button to create a new form field”. Clicking once or twice may be OK. I've seen that approach used to upload files, and it annoyed me when I had to upload two dozen, simply because it broke the “flow” of selecting each file; and that was all point and click. When you have to actually type in a phrase, then switch to the mouse to click, then back to the keyboard... This is much easier — even when an end-user just needs to simply upload an unknown number of files. And on the back-end, the FormFieldGenie can help you very simply organize complex data sent to the server as the end-user enters it.
The best way to understand this class is to exemplify it, so we created a ►working demo. You should also view the highlighted ►demo source code to understand what is happening, why and how. However, this instruction page provides a basic overall concept and details that make understanding the demo code easy. You can download the JavaScript™ package from our ►download page or Git it from GitHub: ►FormFieldGenie.
Using this class is simple and straight-forward, requiring only a few JavaScript™ event handlers be added to the form field(s) you want duplicated (cloned). But it is no simple class: not only can it clone the form field that the event handlers are attached to, but any associated form fields, their labels, and any other DOM nodes that go along. If the said form-fields are being sent to a server, then the other primary function this class serves is to update the names of the cloned form-fields and keep them in order when new fields are “popped,” “pasted,” “inserted,” or “deleted”. We will attempt to explain its use in terms that a non-programmer can understand, so you can incorporate this class successfully in your site, but a basic amount of HTML understanding and a minimum amount of knowledge on using JavaScript™ in your HTML pages is required. It has a list of options that allow you to fine-tune the way it works making it incredibly versatile. Plus it allows plug-in callback functions that allow it infinite flexibility. It intelligently updates the name associated with each form field cloned based on each individual name; if your given name is incompatible with this class’ capabilities, your custom plugin can handle it.
To create a new FormFieldGenie instance:
myGenie=new SoftMoon.WebWare.FormFieldGenie({…options object…}, HTML_clipMenu, genie_name);
All parameters are optional.
Jumping ahead, the genie_name
genie-tier-index
and update-value-genie
.
To be useful, the name should therefore be valid in an HTML attribute,
so it may not contain line-endings.
It may contain any textual characters except the comma ,
(the comma is used to separate the names within the HTML attributes).
The Genie will throw an Error
on these illegal characters if you try to use them.
Any whitespace at the beginning or end will be trimmed off and ignored,
but whitespace in the middle is OK.
After creating a Genie instance, you can set or change the name like in this example:
myGenie.name='foo'
When adding a new form-field or group of form-fields (using the popNewGroup()
method),
the FormFieldGenie can create (clone) one based on what already exists in the form (more on that below),
or you can explicitly give it a form-field or group of form-fields to clone.
You may pass an explicit DOM node (form-field or group of form-fields) to clone
when creating an instance of the FormFieldGenie; for example:
myGenie=new SoftMoon.WebWare.FormFieldGenie({clone: ……my DOM node to clone…… });
myGenie.popNewGroup(……)
After creating an instance of the FormFieldGenie, you may also
set the instance.config.clone (myGenie.config.clone
in these examples) to the explicit DOM node
(form-field or group of form-fields) you want to clone (if any).
An example of defining an explicit node to clone:
myGenie=new SoftMoon.WebWare.FormFieldGenie;
myGenie.config.clone= ……my DOM node to clone……
myGenie.popNewGroup(………)
Using the FormFieldGenie
The first parameter passed to most of this class’ methods (the “actionable” methods) is the entire
DOM Element Node you want auto-regenerated, deleted, cut, copied, inserted before, or pasted-over.
A quick look at the source code of the demonstration example will help clarify this.
If you only need the form field itself repeated, pass the value of this
to the method.
Remember, in an event handler attached to form fields, the keyword this
refers to the
DOM node of the form-field itself.
If the field has a <label>
Element (or any other Element) around it that you want cloned also,
simply pass this.parentNode
.
If you want to clone a whole group of fields and associated text,
simply repeat parentNode
in a chain as many times as needed to move up the DOM tree.
Alternatively, you could use the .closest()
method of a DOM Element,
perhaps relating to the class-name you assign to your “group
”
(see also groupClass
in the ConfigStack description).
focusIn
& focusOut
events (which do bubble) were not standard.
You had to use focus
& blur
events (which do not bubble)
directly on the <input>
‖ <textarea>
Elements,
requiring an event-handler for each that needs to keep track of parentNode
s and their “anscestors.”
The demo (test!) example was developed then.
These days you can put the event-handlers on parentNode
s and their “ancestors”
(as in the short simple example shown below),
greatly simplifying the application of this class to your HTML.
From there, the class makes one requirement:
there must be an Element enclosing the group
(see below) you want cloned.
The clones are added into this enclosing Element. First, let’s be clear that any
HTML “elements” may be used; it does not matter to the FormFieldGenie.
For the sake of discussion, we will refer to these these as follows:
- The
<input>
,<textarea>
, and<select>
DOM HTML Elements themselves are referred to as the (onlyfieldNode
<input>
types that are found in the array atinstance.config.userDataInputTypes
— see ConfigStack). AfieldNode
needs to have the proper event handlers attached; alternatively, the event-handlers may be attached to thegroup
or thebatch
or even higher up the document hierarchy, if you manage them correctly. - The entire DOM HTML Element node you want cloned
is referred to as the
. It may be as simple as onegroup
fieldNode
; or an Element containing one or morefieldNode
s, descriptive text in a<label>
, any other Elements, …whatever you need or like. - The DOM HTML Element enclosing the cloned DOM nodes
is referred to as the
. It may contain any other HTML (that does not get cloned) if you use thebatch
groupTag
orgroupClass
ConfigStack options.
In the super-simple example shown below,
the <input>
is the fieldNode
,
the <label>
is the group
,
and the <fieldset>
is the batch
.
The <legend>
will be ignored (see more info in the next paragraph).
The number 1
will be updated in the <input>
’s name
attribute
when the group
is cloned and the clone is added to the batch
.
This updated number will be kept in proper sequence (without gaps) with the order of <input>
s in the HTML,
which is generally and accordingly the sequence shown to the end-user.
(Alternatively, with PHP at least, the name could simply be user[file][]
and the FormFieldGenie will do nothing to it, because it doesn’t need to! …more on that below…)
<label>
and we could then use the keyword this
instead of event.target.parentNode
If we keep them on the <fieldset>
they could be attached using addEventListener()
instead of using inline-attributes, as they will not need to be cloned;
the <label>
gets cloned, so it is easier to use inline-attributes there
since they get cloned with the Element, whereas “attached EventListeners” do not get cloned.
<fieldset
onfocusin='myGenie.tabbedOut=false'
onkeydown='myGenie.catchTab(event)'
onfocusout='myGenie.popNewGroup(event.target.parentNode)'>
<legend>select the files you want to upload</legend>
<label>choose a file<input type='file' name='user[file][1]'></label>
</fieldset>
The batch
may contain any other HTML besides the clones;
but the clones will always be added to the batch
directly following the last group
.
See also the config.groupClass
and config.groupTag
options
in the ConfigStack description below.
In this example above, myGenie.config.groupTag='LABEL'
must be set.
Beyond this, you are free to develop your page the way you see fit.
Any time your keyboard cursor is focused on a fieldNode
and then you leave it
by pressing the Tab key or by clicking the mouse button somewhere else,
the class looks at the “current” group
(the one with keyboard focus within) and
all the group
s within the batch
that follow the “current” group
.
If any are empty (except for the last one), by default it deletes them
(you can change this default action with options;
see dumpEmpties()
in the ConfigStack description below).
It then looks within the last group
and
by default (see checkForFilled
and checkField
in the ConfigStack description below)
finds the first text-based <input>
tag
(see userDataInputTypes
in the ConfigStack description below),
or if there are no <input>
s, by default finds the first <textarea>
,
then looks to see if anything was typed in (or similar for type='file'
).
If this fieldNode
has been filled by the user,
the class ‘pops’ a new group
.
If the end-user pressed the Tab key from within a fieldNode
(to exit it)
and a new group
was popped, by default
the cursor is focused within a specified fieldNode
of the new group
.
If nothing was popped, the cursor is controlled by the browser as usual, and typically is passed onto the next form field.
If the end-user (mouse-)“clicked” out of the fieldNode
,
the usual action is taken by the browser depending on where you click,
whether or not a new group
was popped.
That sums up the main framework of the class. Its true power begins to shine when you look at how it updates the names associated with each form field cloned, and how the available options you can pass to the class can modify the default actions of the main framework and the process of updating the field names. This gives it the ability to work with virtually any HTML form of any complexity, and the corresponding supporting server side script.
Updating fieldNode
names
When this class updates a name, it considers several things. Is the name indexed, or not?
(i.e. name[index]
or name[index][index]
etc.)
Remember in PHP, when a name is indexed, a “mapped-array” or nested mapped-arrays is/are created.
If it is indexed, is the last index virtual (“implicit”)? (i.e. name[]
or name[index][]
etc.)
Remember in PHP, when multiple form fields return the same virtual (“implicit”) indexed name
they are automatically placed into a sequentially numbered array.
The individual “indexes” in the name may be numeric, or textual; for example:
name='foo[1][bar][3][baz][7][bing][]'
.
When multiple “indexes” are present, as in the above example,
we are signifying “nested arrays”, and each “level” of nesting we call an name='foo'
or name='foo123'
etc.).
Remember when a form returns multiple fields of the same name without using virtual (“implicit”) indexes,
the last one overwrites all the others when using PHP.
It should be mentioned here that if these built-in processes for updating the name don’t suit your needs,
you may write your own plug-in that can do anything you want!
(See updateName()
in the ConfigStack description below.)
The one other thing to be considered when updating the name is a special case involving the
value of checkboxes and radio-buttons; this involves reasons similar to those involved when
updating virtually indexed names, and we will take a look at this further down… … …
Data organization with the FormFieldGenie
Perhaps the number one (#1) most important consideration when engineering software systems is data organization.
Do everything you can to organize your data to serve your codebase;
don’t organize your codebase to serve your data, unless you don’t have control over the data.
¡Organize your data so that your code can most easily and quickly process it, and that depends on the process!
Typically, data is organized in a flat-list or a hierarchical-tree format (tables are another good example).
When you create an HTML form to send data to a server to be processed,
it is inherently sent by the browser to the server as a flat-list;
each fieldNode
’s name is paired with its value,
and the pairs are sent sequentially.
As mentioned above, the PHP language on a server can interpret the name sent
and create “arrays” … and arrays nested within arrays nested with arrays…
This automatically converts the flat-list data-format to a hierarchical-tree data-format
(and that format can moonlight as a table-format if need be).
Quite often, dealing with complex data is better suited to the hierarchical-tree format,
rather that trying to coordinate a bunch of flat-lists.
The FormFieldGenie was designed with the hierarchical-tree format in mind,
by supporting “indexed names” with nested Genies…
Fully Indexed Names
The class by default looks for the last index in the name that is numeric
and increases it by one for each new field popped.
If no index is found to be numeric, the name is left unchanged.
You can specify which index in the name gets modified with
the opts.indexTier
parameter, or by using an HTML attribute.
The indexTier
is perhaps the most important property of the FormFieldGenie ConfigStack,
and you can find the technical specs behind this option in that section of these instructions.
At times, you may create a form with a complex data structure,
such that having one indexTier
value that applies to
all fieldNode
names in a group
is insufficient;
then you use the custom HTML attribute genie-tier-index
.
This attribute allows you to specify the tierIndex
for one or more Genies (when they are nested).
Check out the example below, noting that you must not place any characters, including whitespace,
between the Genie “names” and the following colon :
but whitespace elsewhere in the attribute data-value is ignored.
// your Genie names: ↓↓↓ ↓↓↓ ↓↓↓
<input name='foo[1][bar][3][baz][7][bing][]' genie-tier-index='myGenie: 0, otherGenie: 1, etcGenie: -2'>
// updates: [1] ↑ [3] ↑ [7] ↑↑
// remember only numeric indexes in the name are “counted”
// the default indxTier value of (-1) points to the numerically-implicit index [] at the end
Virtually Indexed (Numerically Implicit) Names
These are names with an “empty” index-tier at the end: <input name='foo[bars][]'>
As noted, PHP automatically “numbers” them into a sequential array as it receives them.
There is often no need to do anything to these names,
because the server side script (PHP) handles them as they should be.
If your form is returning a “deep tree” of data to PHP
with Genie batch
es nested within Genie batch
es nested within … … …,
and you are using a name with “numeric indices” as well as the “virtual index” at the end,
one or more Genie(s) may need to update a numeric index, while another leaves the virtual index alone.
Because of this need, a virtual index is “counted” as a numeric index when using opts.indxTier
.
Non-indexed Names
Non-indexed names without digits at the end are simply left alone.
This is generally only useful for radio buttons or <button>
Elements.
Names that end in a sequence of digits have that sequence numerically increased by one for each new field popped.
Pay close attention to the favorite cars example and the data it produces when the form is submitted.
Note how only the last sequence of digits is increased, and the rest are left alone.
Special case: checkboxes and radio buttons with “numeric” values
In some cases the need arises for the value of these form fields to be updated, not the name.
This exception occurs sometimes when you use checkboxes and radio buttons.
The favorite pets section in the demonstration exemplifies this.
Take a look at the source code of the demonstration example and also at the test-demo itself.
Notice how the nickname section also uses checkboxes within the group
to be cloned.
If we simply clone these and leave their names unchanged,
the resulting data no longer co-ordinates with its associate data.
You get a sequential array of all the checkboxes checked by the user,
but there is no way to tell which array element corresponds to which nickname.
If an <input>
’s value is a number
it can be updated instead of the name.
You can control which <input>
values are updated
by listing the specific Genie(s) that do so with an HTML Element attribute
(remember, this only works with checkboxes and radio-buttons).
This becomes necessary when group
s are nested within each other.
For example: <input type='checkbox' name='user[1][pets][favorites][]' value='0' update-value-genie='thePetGenie'>
.
The Genie with the name thePetGenie
will update the value of this <input>
;
but when it updates another fieldNode
in the group
it is cloning,
and said fieldNode
has the name user[1][pets][0][name]
,
it will (by default with the indxTier=(-1)
) update the [0]
index in the name;
furthermore, the value='index'
and the [index]
updated in the names
will all have matching index
es within the same cloned group
(provided your seed-clone originally had matching index
es).
In other words, the checkbox/radio-button’s value
will indicate the group
it is in.
The content of this update-value-genie
attribute should be the name
you pass into the
FormFieldGenie constructor (you can also set it directly after you construct your Genie
using something like: myGenie.name='foobar';
).
If you want more than one Genie to update the checkbox / radiobutton value
,
specify their names in your attribute separated by commas.
For this reason, your Genie name may contain any characters except the comma ,
character
(also, whitespace is trimmed off the beginning and end of your name).
Then only the Genie(s) you specify will update the value
;
other Genies that operate on your checkbox or radiobutton will update the <input>
’s name
as usual.
Coping, Cutting, Pasting, & Inserting
You can paste two different ways using three different methods:
- paste over an existing
group
usingpasteGroup(group, {clip: yourClipReference})
- insert a new
group
usingpasteGroup(group, {doso: 'insert', clip: yourClipReference})
- insert a new
group
usingpopNewGroup(group, {doso: 'paste', clip: yourClipReference})
The difference between popNewGroup()
and pasteGroup()
is that pasteGroup()
will return false
if the clip is empty,
while popNewGroup()
will simply pop a new “blank” clone if the clip is empty.
After creating an instance of the FormFieldGenie,
the clipboard Object may be accessed through instance.clipboard
;
each clipboard Object property may contain an individual clip (DOM node).
For more details, see the “options” description below.
The HTML_clipMenu
group
s — when copied/cut we call them “clips”)
into a multi-clip clipboard.
It should be an actual snippet of HTML with a specific format.
The FormFieldGenie will update the contents of this HTML menu by automatically
adding/deleting <li>
Elements that reference copied/cut “clips.”
These “clips” are handled as special by the FormFieldGenie and they have their own
Array in the clipboard Object: myGenie.clipboard.clips
.
In addition, the FormFieldGenie can manage keyboard and mouse interactions with the menu,
if it is “bound” to the Genie instance.
To “bind” the HTML_clipMenu
to your Genie instance you either
• pass it in as the second parameter when creating an instance of the FormFieldGenie and it will be automatically “bound”; or
• “bind” it to the Genie instance using for example myGenie.bind_clipMenu()
.
If you want to customize management of the end-user interaction with the menu,
but still want its contents updated when the end-user cuts/copies/pastes/deletes etc.,
do not bind it, instead set it as the property: myGenie.HTML_clipMenu
of your Genie instance.
When updating the menu, new <li>
Elements that are added
can be automatically given the tabindex
attribute.
“Bound” menus’ <li>
Elements always get a tabindex="-1"
.
“Unbound” menus’ <li>
Elements get a tabindex
if the .HTML_clipMenu_tabindex
of the Genie’s .config
options object is set
(for example myGenie.config.HTML_clipMenu_tabindex
).
That allows you to customize the tabindex
value to your needs,
but typically it would be only "0"
or "-1"
(by default it is set to "-1"
).
Every time the menu’s contents is updated,
a custom ffgeniemenuupdate
Event is dispatched on the menu;
you can listen for this event and further customize the menu if you need to.
Two examples are shown below: the first is typical when binding the menu to the Genie;
the second is a basic suggestion on how you could manage an unbound HTML_clipMenu
,
but you might customize it more to your specific needs.
<menu id='myGenie_bound_popUpMenu'>
<li class='insert' tabindex="-1" controls='myGenie_popUpMenu_insertA myGenie_popUpMenu_insertB'>insert:
<span class='new' id='myGenie_popUpMenu_insertA' tabindex="-1">new group</span>
<ul class='clips' id='myGenie_popUpMenu_insertB'>
<li standard tabindex="-1">all clips</li>
</ul>
</li>
<li class='copy' tabindex="-1" controls='myGenie_popUpMenu_copy'>copy to:<ul class='clips' id='myGenie_popUpMenu_copy'>
<li standard tabindex="-1">new clip</li></ul></li>
<li class='cut' tabindex="-1" controls='myGenie_popUpMenu_cut'>cut to:<ul class='clips' id='myGenie_popUpMenu_cut'>
<li standard tabindex="-1">new clip</li></ul></li>
<li class='paste' tabindex="-1" controls='myGenie_popUpMenu_paste'>paste from:<ul class='clips' id='myGenie_popUpMenu_paste'>
</ul></li>
<li class='delete' title='triple-click' tabindex="-1">delete</li>
<li title='triple-click' tabindex="-1">clear clipboard</li>
</menu>
<script>
myGenie=new SoftMoon.WebWare.FormFieldGenie({…options…}, document.getElementById('myGenie_bound_popUpMenu'));
</script>
<menu id='myGenie_unbound_popUpMenu'>
<li>insert:<span onclick='myGenie.popNewGroup(this.closest("."+myGenie.config.groupClass), {doso:"insert"})'>empty field</span>
<ul class='clips' onclick='if (event.phase===Event.BUBBLING_PHASE) myGenie.pasteGroup(this.closest("."+myGenie.config.groupClass), {doso:"insert", clip:event.target.innerText})'>
<li standard>all clips</li>
</ul></li>
<li>copy to:<ul class='clips' onclick='if (event.phase===Event.BUBBLING_PHASE) myGenie.copyGroup(this.closest("."+myGenie.config.groupClass), {clip:event.target.innerText})'>
<li standard>new clip</li>
</ul></li>
<li>cut to:<ul class='clips' onclick='if (event.phase===Event.BUBBLING_PHASE) myGenie.cutGroup(this.closest("."+myGenie.config.groupClass), {clip:event.target.innerText})'>
<li standard>new clip</li>
</ul></li>
<li>paste from:<ul class='clips' onclick='if (event.phase===Event.BUBBLING_PHASE) myGenie.pasteGroup(this.closest("."+myGenie.config.groupClass), {clip:event.target.innerText})'>
</ul></li>
<li onclick='if (confirm("Do you want to delete this group?")) myGenie.deleteGroup(this.closest("."+myGenie.config.groupClass))'>delete</li>
<li onclick='myGenie.clearClipboard();'>clear clipboard</li>
</menu>
<script>
myGenie=new SoftMoon.WebWare.FormFieldGenie({…options…});
myGenie.HTML_clipMenu=document.getElementById('myGenie_unbound_popUpMenu'));
</script>
Note that the standard
attribute signifies <li>
Elements in the <ul>
s that do not get deleted.
You may modify the TEXT in the lone <span>
without hassles.
For unbound menus, you may modify the TEXT in the <li>
elements that belong to the <menu>
without hassles.
If you need to modify any other text, for clarity in your specific project, or to support another language, etc.,
you must update the .HTML_clipMenu_TEXT
property of your Genie instance
(for example: myGenie.HTML_ClipMenu_TEXT={…your specs…};
).
For instance, with a list of names, your TEXT may become: “insert name”, “copy name”, “cut name”, “paste name”, “delete name”,
and your confirm dialog may become “Do you want to delete this name?”.
See the FormfieldGenie source-code for a description of this Object’s format.
You can have multiple versions of the basic menu in different languages, and change languages on-the-fly:
myGenie.bind_clipMenu(false); //unbind the current menu … perhaps it is English
/* the above step removes the event handlers —
* if you think you might re-instate this menu, you can skip this first step
* and keep it bound, and just re-set myGenie.HTML_clipMenu directly in the future */
myGenie.bind_clipMenu(document.getElementById('myGenie_popUpMenu_French')); //bind the newly selected language menu
// ¡ if you are not using “bound” menus, skip the preceding first two steps ! … instead use:
// myGenie.HTML_clipMenu=document.getElementById('myGenie_popUpMenu_French');
myGenie.HTML_ClipMenu_TEXT=French_TEXT; // our French_TEXT object is predefined elsewhere
myGenie.update_HTML_clipMenu(); //fill the menu with the appropriate “clip” references
You may change/add any id
or class
names to the HTML of the menu for CSS handles;
however, the existing class-names must remain.
The myGenie
variable in the “unbound” menu should of course
be changed to the name of the variable that you use for your specific Genie
(multiple Genies require multiple variables to be assigned to, and multiple HTML_clipMenu
s).
Remember, the first parameter passed into the Genie methods is the “current” group
.
The “unbound” example above assumes you are using the groupClass
option (see ConfigStack below);
maybe you should use the groupTag
option, or perhaps better yet,
just hard-code the .closest()
spec into your script.
The FormFieldGenie will add clip references to the menu that look like these examples (with the default TEXT):
“clip 1”, “clip 2”, “clip 3”, etc.
Using the .HTML_clipMenu_TEXT
Object (see above), you can modify the word “clip” to the word (or language) you desire.
Embedded JavaScript™ event-handler attributes may also be modified and/or expanded as appropriate to your needs.
Since this menu would be typically activated by the end-user using the “context menu” keyboard-key or mouse-button,
and since the “context menu” may need to do more than support the FormFieldGenie in your specific application,
you may (with restrictions):
• add additional <li>
elements to the main <menu>
,
• add any other HTML Elements within your additions,
• add embedded <ul class='clips'>
s to be filled with the current list of “clips” when the menu is updated.
With “bound” menus, and except for <ul class='clips'>
s,
your menu additions are restricted from having the same class-names as any existing class-names on similar elements.
If your menu is “bound” these additions will be ignored, and you must support them.
When the menu is updated, all <li>
Elements that reference copied/cut “clips” are deleted,
and then the menu is rebuilt from scratch.
After the menu is updated, a custom 'ffgeniemenuupdate'
event is fired on the HTML_clipMenu
so you can further customize the update if needed.
If you need event-handlers directly on the auto-generated <li>
Elements that reference copied/cut “clips,”
you will have to use this custom event and then attach your events to these <li>
Elements
using element.attachEventListener()
or by setting their attributes.
As an example of a difference, you may want the end-user to double or even triple-click to delete, and then do it with no confirmation:
onclick='if (event.detail===3) myGenie.deleteGroup(this.closest("."+myGenie.config.groupClass))'
.
The Genie itself will confirm if you want to clear the whole clipboard unless you auto-confirm using .clearClipboard(true)
Note that using the HTML_clipMenu
is not required to cut/copy/paste/delete using the Genie;
but if you do inform the Genie of this menu’s existence, it will update it automatically for you.
This menu interfaces with the Genie’s innate ability to handle a “multi-clip” clipboard;
but you don’t need to integrate the “multi-clip” functionality in your project.
Perhaps you only want to allow your end-users to copy/cut/paste into a “single-clip” clipboard
so they don’t get confused, or for a smaller menu with a smaller footprint on the screen, or for whatever reason.
Perhaps then you need a simple “pop-up” menu like the one shown below.
(¡Note again that we are assuming the use of the groupClass
option like the above example!)
The TEXT in this demo example menu below is meaningless to the Genie and you may change it to anything you like.
Note that we don’t specify the “clip” instead of calculating it like in the above example demo menu;
the Genie will use the _system_
clip spec as defined in the ConfigStack prototype.
Moreover, We tried not to box you in as a developer.
By simplifying the cut/copy/paste menu as shown below, it could be used with may different batch
es
using the same Genie in each batch
, but using a different “clip” spec for each batch
;
perhaps then you would add to the imbedded JavaScript™ something like
{clip: this.closest(".genie-batch").getAttribute("clip-spec")}
<menu id='myGenie_simpleMenu'>
<li onclick='myGenie.copyGroup(this.closest("."+myGenie.config.groupClass))'>copy section</li>
<li onclick='myGenie.cutGroup(this.closest("."+myGenie.config.groupClass))'>cut section</li>
<li onclick='myGenie.pasteGroup(this.closest("."+myGenie.config.groupClass))'>paste copied/cut section</li>
<li onclick='myGenie.pasteGroup(this.closest("."+myGenie.config.groupClass), {doso:"insert"})'>insert copied/cut section</li>
<li onclick='myGenie.popNewGroup(this.closest("."+myGenie.config.groupClass), {doso:"insert"})'>insert new section</li>
<li onclick='myGenie.deleteGroup(this.closest("."+myGenie.config.groupClass))'>delete section</li>
</menu>
The way the embedded JavaScript™ is written in both of these “menu” examples,
your webpage JavaScript™ code should further manage this HTML menu,
typically moving it “into” the group
HTML
when the end-user either •right-clicks (menu-clicks) in said group
or
•presses the “menu” key (looks kinda like this: ≡ when it is present on the keyboard)
and keyboard-focus is “in” a fieldNode
within said group
,
and style it with CSS so it “pops-up” (appears) at the right place at that time.
If your menu is “bound,” you simply use
myGenie.show_clipMenu(event, group, container)
where:
• event
is any event, but a keyboard event
creates the opportunity for the end-user to return to the Element that had keyboard focus;
• group
is the FormFieldGenie group that the menu refers to; and
• container
is the DOM Element that the HTML_clipMenu
is moved into when it “pops-up,” so it appears in the correct position on the screen
(according to your CSS).
“Unbound” menus should also have full keyboard support, by including a tabindex
attribute
on all <li>
Elements, and the .config.HTML_clipMenu_tabindex
option
will automatically add this attribute to the auto-generated <li>
Elements that reference the clips.
Your custom JavaScript™ should support the arrow-keys
haspopup
support, but implementing all that is beyond the scope of these instructions.
If you “bind” your menu, all that is handled for you.
“Bound” menus also get custom "popUp"
and "popDown"
events
at the appropriate time, and the event that triggered the pop-state-change
can be found in the custom-event’s .detail
property.
The publicly accessible properties of a FormFieldGenie instance are:
- .config
- An instance of a FormFieldGenie.ConfigStack.
This holds all the configuration values for a FormFieldGenie instance.
You can
myGenie.config.stack()
a new set of values (of your choice) onto the stack to temporarily change them, thenmyGenie.config.cull()
off those modifications to restore the configuration to the previous values. Any time you pass an opts (options) object to any method, those options are temporarily stacked onto this stack while the method is operating. - .clipboard
- This Object holds all the clipboard data for your Genie instance.
- .HTML_clipMenu
- This points to the HTML that is the “menu” shown to the end-user
with the “multi-clip” clipboard data for your Genie instance.
If you are not implementing a “multi-clip” interface, this property should be
undefined
. - .HTML_clipMenu_TEXT
- This Object references the HTML’s TEXT in the “menu” and can be replaced with references to different customized text or languages.
- .name
- The name of your specific Genie.
It may not contain the comma
,
character or line-endings. Whitespace at the beginning and end will be trimmed off. - .tabbedOut
- this
Boolean
value indicates if the end-user pressed the Tab key to exit a form field.
The publicly accessible user methods of a FormFieldGenie instance are:
- popNewGroup(group, opts)
- Returns
true
if a newgroup
is ‘popped’ orfalse
if not. - deleteGroup(group, opts)
- Returns
true
if thegroup
was deleted,false
if not. - cutGroup(group, opts)
- Returns
true
if thegroup
was deleted,false
if not.
group
will always be copied to the clipboard. - copyGroup(group, opts)
- Returns
null
.
group
will always be copied to the clipboard. - pasteGroup(group, opts)
- Returns
false
if the clipboard clip is empty,true
if it is pasted. - clearClipboard(confirmed)
- Does what it says.
It will manually confirm with the end-user if you don’t pass
true
forconfirmed
. - getClip(clipID, doInit)
- This method is also called internally by other user-methods as a worker-method, but you may utilize it if manually working with the clipboard.
- update_HTML_clipMenu()
- This method is also called internally by other user-methods as a worker-method, but you may utilize it if manually updating the clipboard.
- show_clipMenu(event, group, container)
- If your menu is “bound”,
call this method from your custom event hander that triggers opening the menu (“pop-up”).
Pass along the
event
from your event-handler, and custom-calculate/-specify thegroup
andcontainer
for your HTML. Thegroup
is the one associated with this menu’s operations, and thecontainer
is the DOM Element that the menu will be placed into as it “pops-up.” - bind_clipMenu(Boolean
true ‖ HTML_clipMenu) - Passing a
Boolean
value (true
is default) will either bind or unbind the currentmyGenie.HTML_clipMenu
. Passing an HTML Element will bind that element as the new.HTML_clipMenu
.
The publicly accessible (and user-replaceable) worker-methods of a FormFieldGenie instance are:
- isActiveField(fieldNode, cbParams)
- This is called internally by user methods.
You can replace it using something like:
myGenie.isActiveField=function(fieldNode, cbParams) { /*…your customizing code…*/ }
The standard function also recognizesmyGenie.config.isActiveField
which may be a function or Boolean value. This can replace the standard function to check if a form field is currently active or not; i.e. is it disabled, or is it even displayed at all? You may add/subtract your own rules, perhaps checking the status of another element. Inactive elements will not be considered when deciding to pop a new group or dump an empty one. The cbParams variable is the user’s call-back parameters (seecbParams
below in the ConfigStack description below). Your function should returntrue‖false
. - catchTab(event)
- This is to be utilized (called by) by your “onKeyDown” event handler.
It can also be the “onKeyDown” event handler if you attach it using
Element.addEventListener()
as opposed to the handler being an Element's attribute. - catchKey(event)
- This is not defined natively, but is recognized by the standard
catchTab()
method of an instance. Since the form field that the event-handler (i.e.catchTab()
, which calls this method) is attached to is likely to be cloned, it is easier to attach event-handlers “inline”, i.e. a part of the HTML tag as an “onkeydown” attribute (e.g.<input type='text' onkeydown='myGenie.catchTab(event)'>
), so said attribute that leads to calling this method is inclusively cloned, rather than using “unobtrusive” JavaScript™addEventListener()
methods every time the form field is cloned. This conception, however, limits you to only one (1) “onkeydown” event handler, so the “catchKey” hook is internal to allow you to “use” additional “onkeydown” handlers without having to add them to the element. Alternatively, you may utilize the “myGenie.config.eventRegistrar()
” functional option (see the ConfigStack options below) to add on a handler that calls this method, or any additional handlers, to newly cloned form fields as needed. - clipMenu_controller(event)
- handleEvent(event)
- These two properties are one-in-the-same aliases for the method that handles all end-user interaction with “bound” menus.
- handleKeydownInMenu(event)
- Does what it says and returns
true
if the menu should stay open, orfalse
if the menu should close. - onGenieMenuSelect(event)
- Manages an end-user selection and calls the copy/cut/paste/delete/etc. action.
It returns the value that the action returns.
In addition, the
event
Object has one or two additional properties modified: •.menuSelectReturnStatus
istrue
if the menu should stay open,false
if the menu should close; and •.opStatus
indicates the meta-status of the user-action.
The publicly accessible properties and default values
of a FormFieldGenie.ConfigStack (myGenie.config) instance are:
- indxTier: integer-number
-1 - Number of numeric index “tiers” to ignore counting from the beginning or end of a name;
used to specify the specific tier when updating names.
Positive (+n) and zero (0) values skip over tiers starting from the beginning of the name progressing forwards (i.e. a zero-based count),
while negative (−n) values count tiers starting from the end of the name progressing backwards (i.e. a −1 based count).
Example: name →
myField[4][3][2]
whenindxTier=-3
the FormFieldGenie updates/modifies the index that contains “4” and whenindxTier=1
the FormFieldGenie updates/modifies the index that contains “3”. The Genie looks for the numeric indexes, so note the following example: name →myField[4][subsection][3][2]
whenindxTier=2
the FormFieldGenie updates/modifies the index that contains “2”; the “subsection” tier is ignored. A virtual (“numerically implicit”) index at the end of the name is included in the “count,” so with a name likemyField[4][]
, theindxTier
values of “0
” and “-2
” both point to the[4]
index, while “1
” and “-1
” both point to the virtual[]
index. The FormFieldGenie does nothing to the name when a virtual[]
index is pointed to by theindxTier
. You can override this spec using attributes in yourfieldNode
; see Fully Indexed Names above. - focusField: 0 +number
0 - This applies to
pasteGroup()
andpopNewGroup()
only. Pass the field number (counted from zero) of the text/filename field you want the cursor focused on, if the user pressed the Tab key orconfig.doFocus=true
(see directly below), when popping or when pasting a new group. - doFocus:
true ‖ false ‖
null - This applies to
pasteGroup()
andpopNewGroup()
only. Iftrue
, thefocusField
(see directly above) will receive focus, whether or not the Tab key was pressed. Iffalse
, thefocusField
will not receive focus when the Tab key is pressed. Ifnull
, then the Tab key will cause thefocusField
to receive focus when popping a newgroup
. - isActiveField:
undefined ‖ true ‖ false ‖ function(groupInQuestion, cbParams) { /*…your custom code makes the distinction…*/ } - This works in conjunction with the
isActiveField()
method of a FormFieldGenie instance. Iftrue
, all form fields will be considered active; iffalse
they will not. If this value is a user-defined function, its Boolean return value will be respected. ThecbParams
variable is the user’s options (see below). - dumpEmpties:
dumpEmpties() ‖ true ‖ false ‖ function(empty_groupInQuestion, deleteFlag) { /*…your custom code makes the distinction…*/ } - Remove emptied groups on the fly?
This applies to
deleteGroup()
andpopNewGroup()
only, and not when inserting or pasting. The supplied default function utilizes theminFields
,groupTag
, &groupClass
ConfigStack values (see below) to make the distinction. If a function is supplied, it should returntrue ‖ false ‖ null
and ifnull
is returned, the function should remove the field itself. If you usedeleteGroup()
, thegroup
will be removed even ifdumpEmpties===false
; however, ifdumpEmpties
is a function, it will be called with the value ofdeleteFlag=true
as its second parameter and its return value (true‖false
) will be respected. - minGroups: +number
1 - Min number of “groups” when checking to dump empties
using the standard
dumpEmpties()
function (see directly above). - maxGroups: +number
100 - Maximum number of clones (
group
s) in thebatch
. - groupClass: string ‖ RegExp
"" - This is used to identify specific child-Elements in a
batch
that aregroup
s. If it is undefined, all children of thebatch
aregroup
s if they match theConfigStack.groupTag
. - groupTag:
htmlTagNameString.toUpperCase() ‖
null - This is used to identify specific child-Elements in a
batch
that aregroup
s. If it is undefined, all children of thebatch
aregroup
s if they match theConfigStack.groupClass
. - checkForFilled:
"all" ‖ "some" ‖ "any" ‖
"one" - This applies to
deleteGroup()
andpopNewGroup()
only, and not when inserting or pasting. If set, the corresponding specified input-fields in thegroup
will be checked. By default only the first one is checked. Ifone
orsome
or possiblyany
, thecheckField
option (see below) should be used also.- 'one'
the “specified field” is indexed by checkField
if the one specified field is full, a new group is popped. if the one specified field is empty, the group may be dumped (automatically deleted). - 'any'
the “specified fields” are either all of the fields in the group, or if checkField
is an array, it should contain the numeric indices of the fieldsif any one of the specified fields is full, a new group is popped. if all of the specified fields are empty, the group may be dumped (automatically deleted). - 'some'
each of the -first- "checkField" number of fields specified will be checked, or if checkfield
is an array, it should contain the numeric indices of the fieldsif all of the specified fields are full, a new group is popped. if all of the specified fields are empty, the group may be dumped (automatically deleted). - 'all'
if all of the fields in the group are full, a new group is popped. if all of the fields in the group are empty, the group may be dumped (automatically deleted).
- checkField: 0, +number ‖ Array
0 - This applies to
deleteGroup()
andpopNewGroup()
only, and not when inserting or pasting. Used in conjunction withcheckForFilled
(see above).if checkField is a number (when checkForFilled
= 'one' ‖ 'some')Pass the field number (counted from zero) of the field or fields you want checked for being “empty” when popping. If checkForFilled='some'
then each of the first number of fields will be checked.if checkField is an Array (when checkForFilled
= 'any' ‖ 'some')The array should contain numeric values that are the indices of the fields to check. The fields are indexed (starting with 0) in the order that they appear in the HTML document.
- cbParams:variable
undefined - This is a user-defined object/value of any type.
This property is not defined (e.g.
(cbParams in myGenie.config)===false)
) unless defined by the user. This will be passed through to theupdateName
plugin callback function as the fourth variable (cbParams), and to theisActiveField
†,cloneCustomizer
‡,eventRegistrar
‡ andbatchCustomizer
‡ plugin callback functions as the †second or ‡third. It may be any type as required by your plugin callback functions, but if they share you may want to use an object with separate properties. - updateName:
null ‖ function(field, indxOffset, group, batch, cbParams) { /*your plugin code*/ } - A plugin callback function to handle the process of updating each name.
The function will be passed each individual form DOM object
(
<input>
or<textarea>
or<select>
or<button>
) one at a time in thefield
variable. TheindxOffset
variable contains the numerical positional offset of the newfield
compared to thefield
passed in. ThecbParams
variable is the user’s options (see above). The Function should pass back a string of the new name, ornull
. If a string is returned, the name attribute of the DOM Element will be set to that value; no need for your function to alter the name directly, unless returningnull
. Ifnull
is returned, the usual process of updating the name continues. TheupdateName()
function may do anything it needs from partial updating the name directly (to be continued by the usual process), to updating thevalue
, to updating the parentNode text, or whatever you can imagine… - cloneCustomizer:
null ‖ function(group, pasteOver, cbParams) { /*your customizing code*/ } - If there is something special you want to do to each nodeGroup cloned, you may pass a function to handle that.
All field names will have been updated, but the node will not yet have been added to the document.
The passed variable
pasteOver
will be (true ‖ false ‖ 'paste-over'
) —true
if pasting and inserting,'paste-over'
if pasting over an existinggroup
(the old existing one will be discarded). ThecbParams
variable is the user’s call-back parameters (seecbParams
above). This Function is called only when a newgroup
is being popped or pasted over. - eventRegistrar:
null ‖ function(group, pasteOver, cbParams) { /*your customizing code*/ } - While HTML attributes including event handlers are cloned when a DOM node is cloned,
DOM level 2+ event handlers are not cloned.
If you need event handlers registered for any elements in your cloned group,
you must do them “by hand” through this function.
The function will be passed the
group
after it has been added to the document. SeecloneCustomizer
(above) for info onpasteOver
andparams
. This Function is called only when a newgroup
is being popped or pasted over. - batchCustomizer:
null ‖ function(batch, pasteOver, cbParams) { /*your customizing code*/ } - This is called when a new group is being popped, pasted,
or when a group is deleted or was empty and has been dumped.
It is called from a
setTimeout()
function, so the DOM will be fully updated. Use it to do any final customizing. Note it is passed the wholebatch
node containing allgroup
s including the new one after it has been added to the document, not simply the newly cloned group. SeecloneCustomizer
(above) for info onpasteOver
andparams
. - minPixWidth: 0, +number
4 - for an input to be "active"
- minPixHeight: 0, +number
4 - for an input to be "active"
- clone: HTMLElement
null - This is the Node (including children) that will be cloned when popping a new field/fieldset.
If
null
then the lastgroup
will be cloned instead. - clip: string
"_system_" - Used if you cut/copy/paste w/out specifying the clipboard “clip” name.
See
clip
in the options section below for more information. - clear_clips_whenRetrieved: Boolean
false - ¿Do we remove the clip from the clipboard when pasting it to unclutter the clipboard and/or to free up memory, or do we leave it there so we can paste it multiple times?
- HTML_clipMenu_tabindex: string ‖
null
"-1" - All auto-generated
<li>
items in anHTML_clipMenu
will get atabindex
attribute with this value, unless this is==null
. - only_clips_inAll:
true ‖ false - When using
Genie.getClip('all clips')
, ¿ only return theclipboard.clips
array ? vs. said array with the other properties ofclipboard
appended. This is an advanced feature. - no_system_inAllClips:
true ‖ false - When using
Genie.getClip('all clips')
, ¿ avoid clips inclipboard
with names that contain “_system_
” ? This does not apply to clips in theclipboard.clips
array. This is an advanced feature. - namedElements: string
"input, textarea, select, button" - This string must work with querySelectorAll(); These are elements who’s names will be updated.
¿Maybe you want to use
"*[name]"
instead to update other Elements with non-standard “name” attributes? - userDataInputTypes:
['text', 'search', 'tel', 'url', 'email', 'password', 'datetime', 'date', 'month', 'week', 'time', 'datetime-local', 'number', 'color', 'file'] - An array that specifies which
<input>
“types” the FormFieldGenie recognizes as having values defined by the user.
Passing Options to the FormFieldGenie
Passing options to this class is simple: use an object with properties named accordingly.
Any properties may be included or not, making it simple to control any option without worrying about the others.
If you are not so familiar with JavaScript™, pay close attention to the
demonstration example and how to define an object right in the event handler text.
You can also define your object in a separate <script></script>
,
then simply pass it to the class by name. If you don’t understand how to do this,
please refer to a good book on JavaScript™ programming.
Options may be defined or passed at three levels: global defaults, instance defaults, and when calling a method.
The FormFieldGenie class’ methods, popNewGroup()
, cutGroup()
, copyGroup()
,
pasteGroup()
, and deleteGroup()
, only take two parameters
(the group
and the opts
options object). By setting the option values of the
FormFieldGenie.ConfigStack.prototype
property, you can control the option defaults globally.
When creating an instance (myGenie = new SoftMoon.WebWare.FormFieldGenie( { ...options... } )
)
you can override the global defaults, and when calling instance methods you can override the instance defaults.
However, some options only apply to specific methods as they are being called;
any similar option in the ConfigStack will be ignored.
The FormFieldGenie.ConfigStack
(most others will be ignored).
Some (as noted) will be ignored if passed when creating a new FormFieldGenie instance
and are only valid when calling a method;
those “methodical” properties are noted below:
- doso:
true ‖ 'insert' ‖ 'paste'
- This applies to popNewGroup() and pasteGroup() only.
If you pass (Boolean)
true
when using popNewGroup(), a newgroup
will be popped at the end of thebatch
regardless of whether the lastgroup
is empty; but not exceedingmaxGroups
. Emptygroup
s may be removed as usual. Emptygroup
s will not be automatically removed if"insert"
when using popNewGroup(). If you pass"insert"
or"paste"
when using popNewGroup(), a new field will be popped and inserted before the passedgroup
, regardless of whether the last field is empty; but not exceedingmaxGroups
.
WithpopNewGroup()
, “insert” inserts an empty group.
WithpasteGroup()
, “insert” inserts the selected clip.
WithpopNewGroup()
, “paste” inserts the selected clip. - addTo:
true
- This applies to popNewGroup() and pasteGroup() only.
If you pass
opts.addto=true
, then the value that would be passed intopopNewGroup()
asgroup
will be instead considered thebatch
. This will allow you to add a newgroup
to an emptybatch
but only if •themyGenie.config.clone
is set; •oropts.doso='paste'
while the clipboard has contents. Passingopts.addto=true
acts similar as passingopts.doso=true
in that it will always pop a new field (unless as noted above thebatch
is empty and there is no clone and no paste). Note thatpasteGroup()
withopts.doso='insert'
internally calls callspopNewGroup()
, and this option may then take effect. - clip: number ‖ string
- Object-member/property-identifier — ( a.k.a. yourClipReference )
This is a reference to the member or property of the clipboard object associated with an instance of the FormFieldGenie. Each FormFieldGenie instance has its own clipboard, and each clipboard can hold an “unlimited” number of clips (limited by the machine). You may copy, cut and paste into/from any clip. The.update_HTML_clipMenu()
method, in conjunction with thegetClip()
method and the internal functioning of other methods, recognizes the string pattern"clip num"
(wherenum
is a positive number) and this is the same as passing in a simple number; e.g.{clip:1}
is the same as{clip:"clip 1"}
. These “numbered” clips are stored internally in theinstance.clipboard.clips
array. Every other string value you pass in forclip
refers to a property ofinstance.clipboard
; the clipboard itself is an array, so oddly if you pass the string"1"
like so:{clip:"1"}
you will refer toinstance.clipboard[1]
Most of these options do not need much more explanation, especially if you study the demonstration example. A few do, so we will touch on them here.
By using the config.groupClass
and/or config.groupTag
options,
you may include other DOM Elements besides group
s in your batch
,
including form-inputs or tags with form-inputs as children,
that are not to be duplicated, automatically cleared, or otherwise messed-with by the Genie.
When you want to use a plugin function to update names, you may write one that accepts
a set of parameters, and then pass different parameters to your plugin for different form fields.
There is an included plugin with the demonstration example and the downloadable source code that
shows how this works.
The supplied standard demo plugin can accept two different parameters (passed in the one object).
You may create your own “order” and pass it through to this plugin, should you choose.
If you understand Perl compatible regular expressions, and can understand how this simple plugin functions,
then you could also pass a custom RegExp to the plugin and have even greater control over how it updates the name.
We chose using the first index, because this works well with this plugin’s logic.
Pay attention to the fact that the logic requires matching a single character,
then the “incremental word” follows. Another example of a naming style that would work
with this function’s logic is name_first
name_second
using the RegExp /_([a-z]+)$/
or name_first[]
using /_([a-z]+)\[/