define(function(require, exports, module) {
main.consumes = ["ui", "Plugin", "settings"];
main.provides = ["Form"];
return main;
function main(options, imports, register) {
var Plugin = imports.Plugin;
var settings = imports.settings;
var ui = imports.ui;
/***** Initialization *****/
function Form(options) {
var plugin = new Plugin("Ajax.org", main.consumes);
var emit = plugin.getEmitter();
var model = options.model;
var edge = options.edge || "10 10 10 10";
var rowheight = options.rowheight || 40;
var width = options.colwidth || 300;
var maxwidth = options.colmaxwidth || 300;
var widths = options.widths || {};
var skins = options.skins || {};
var container, meta = {};
var debug = location.href.indexOf('menus=1') > -1;
var headings = {};
if (!Form.proxy) {
Form.proxy = new apf.Class().$init();
apf.nameserver.register("all", "Form", Form);
}
var loaded;
function load(){
if (loaded) return;
loaded = true;
if (options.html)
draw(options.html);
if (options.form)
add(options.form);
}
var drawn = false;
function draw(htmlNode) {
if (drawn) return;
drawn = true;
// Create UI elements
container = new ui.bar({
htmlNode: htmlNode || document.body,
"class" : options.className,
style: options.style
});
plugin.addElement(container);
if (model)
container.setAttribute("model", model);
emit("draw");
}
/***** Method *****/
function add(state, foreign) {
if (!drawn) {
plugin.on("draw", function wait(){
add(state, foreign);
plugin.off("draw", wait);
});
return;
}
// An array of items
if (state instanceof Array) {
state.forEach(function(item) {
createItem({ container: container }, item.title, item, foreign);
});
return;
}
// A simple object as items
for (var caption in state) {
if (caption == "position") continue;
var first = state[caption];
var heading = createHeading(caption,
// basegroup +
first.position,
Object.keys(first).length < 1, foreign);
if (first instanceof Array) {
first.forEach(function(item) {
createItem(heading, item.title, item, foreign);
});
}
else {
for (var title in first) {
if (title == "position") {
continue;
}
var second = first[title];
createItem(heading, title, second, foreign);
}
}
}
}
function createHeading(name, position, hack, foreign) {
if (!foreign) foreign = plugin;
var heading = headings[name];
if (!heading) {
if (!hack) {
var aml = container.appendChild(new apf.bar());
aml.$int.innerHTML = '
';
}
heading = headings[name] = {
container: aml
};
foreign.addOther(function(){
heading.container.destroy(true, true);
// heading.nav.parentNode.removeChild(ns.nav);
delete headings[name];
});
}
if (position && position != heading.container.$position) {
!hack && ui.insertByIndex(container, heading.container, position, foreign);
}
return heading;
}
function getName(path) {
var parts = path.split("/");
var name = "";
parts.forEach(function(part) {
name += part.uCaseFirst().replace(/[-\.]/g, "_");
});
if (~name.indexOf("@"))
name = name.replace(/@(.*)/, "$1Attribute");
return name;
}
function createBind(path) {
var name = getName(path);
Form.proxy.setProperty(name, settings.get(path));
Form.proxy.on("prop." + name, function(e) {
settings.set(path, e.value);
});
settings.on(path, function(value) {
if (Form.proxy[name] != value)
Form.proxy.setProperty(name, value);
}, plugin);
return "{Form.proxy." + name + "}";
}
function createItem(heading, name, options, foreign) {
if (!foreign) foreign = plugin;
var position = options.position;
var node, childNodes;
if (options.setting && !options.path)
options.path = options.setting;
if (debug)
name = "\\[" + (position || "") + "\\] " + name;
switch (options.type) {
case "checkbox":
childNodes = [
new ui.label({ width : width, maxwidth: maxwidth, caption: name + ":" }),
new ui.checkbox({
value: options.path
? createBind(options.path)
: (options.defaultValue || ""),
values: options.values,
skin: "cboffline"
// width: "55"
})
];
break;
case "dropdown":
var model = options.model || new ui.model();
var data = options.items && options.items.map(function(item) {
return " ";
}).join("");
if (data) model.load("" + data + "");
childNodes = [
new ui.label({ width : width, maxwidth: maxwidth, caption: name + ":" }),
new ui.dropdown({
model: model,
width: options.width || widths.dropdown,
skin: "black_dropdown",
margin: "-1 0 0 0",
zindex: 100,
onafterchange: options.onchange && function(e) {
options.onchange({ value: e.value || e.args[2] });
},
value: options.path
? createBind(options.path) //{settings.model}::
: (options.defaultValue || ""),
each: options.each || "[item]",
caption: options.caption || "[text()]",
eachvalue: options.eachvalue || "[@value]",
"empty-message" : options["empty-message"]
})
];
break;
case "spinner":
childNodes = [
new ui.label({ width : width, maxwidth: maxwidth, caption: name + ":" }),
new ui.spinner({
width: options.width || widths.spinner,
value: options.path
? createBind(options.path)
: (options.defaultValue || ""),
min: options.min,
max: options.max,
realtime: typeof options.realtime !== "undefined" ? options.realtime : 1
})
];
break;
case "checked-spinner":
childNodes = [
new ui.checkbox({
value: options.checkboxPath
? createBind(options.checkboxPath)
: (options.defaultCheckboxValue || ""),
width: width, maxwidth: maxwidth,
label: name + ":",
skin: "checkbox_black"
}),
new ui.spinner({
width: options.width || widths["checked-spinner"],
value: options.path
? createBind(options.path)
: (options.defaultValue || ""),
min: options.min,
max: options.max,
realtime: typeof options.realtime !== "undefined" ? options.realtime : 1
})
];
break;
case "checkbox-single":
childNodes = [
new ui.checkbox({
value: options.path
? createBind(options.path)
: (options.defaultValue || ""),
width: options.width || widths["checked-single"],
label: name,
skin: "checkbox_black"
})
];
break;
case "textbox":
childNodes = [
new ui.label({ width : width, maxwidth: maxwidth, caption: name + ":" }),
new ui.textbox({
skin: skins.textbox || "searchbox",
margin: "-3 0 0 0",
"initial-message": options.message || "",
width: options.width || widths.textbox,
value: options.path
? createBind(options.path)
: (options.defaultValue || ""),
realtime: typeof options.realtime !== "undefined" ? options.realtime : 1
})
];
break;
case "password":
childNodes = [
new ui.label({ width : width, maxwidth: maxwidth, caption: name + ":" }),
new ui.password({
//skin : "codebox",
width: options.width || widths.password,
value: options.path
? createBind(options.path)
: (options.defaultValue || ""),
realtime: typeof options.realtime !== "undefined" ? options.realtime : 1
})
];
break;
case "colorbox":
childNodes = [
new ui.label({ width : width, maxwidth: maxwidth, caption: name + ":" }),
new ui.colorbox({
width: options.width || widths.colorbox,
value: options.path
? createBind(options.path)
: (options.defaultValue || ""),
realtime: typeof options.realtime !== "undefined" ? options.realtime : 1
})
];
break;
case "button":
childNodes = [
new ui.label({ width : width, maxwidth: maxwidth, caption: name + ":" }),
new ui.button({
skin: "blackbutton",
height: 24,
margin: "-2 0 -2 0",
style: "line-height:22px",
caption: options.caption,
width: options.width || widths.button,
onclick: options.onclick
})
];
break;
case "submit":
node = new ui.button({
skin: "btn-default-css3",
margin: options.margin,
caption: options.caption || "",
submenu: options.submenu && options.submenu.aml
|| options.submenu || "",
width: options.width || "",
onclick: options.onclick,
"default" : options["default"] ? "1" : ""
});
break;
case "label":
node = new ui.label({
caption: options.caption,
style: options.style,
width: options.width || ""
});
break;
case "image":
node = new ui.img({
skin: "simpleimg",
src: options.src,
margin: options.margin,
width: options.width,
height: options.height
});
break;
case "divider":
node = new ui.divider({
skin: "c9-divider-hor",
margin: "5 0 5 0"
});
break;
case "textarea":
childNodes = [
new ui.label({ width : width, maxwidth: maxwidth, caption: name + ":" }),
new ui.textarea({
width: options.width || widths.textarea,
height: options.height || 200,
value: options.path
? createBind(options.path)
: (options.defaultValue || ""),
realtime: typeof options.realtime !== "undefined" ? options.realtime : 1
})
];
break;
case "custom":
node = options.node;
break;
default:
throw new Error("Unknown form element type: "
+ options.type);
}
if (!node) {
node = new ui.hsplitbox({
options: options,
height: options.rowheight || rowheight,
edge: options.edge || edge,
type: options.type,
childNodes: childNodes
});
}
if (options.name)
node.setAttribute("id", options.name);
ui.insertByIndex(heading.container, node, position, foreign);
foreign.addElement(node);
return node;
}
function update(items) {
items.forEach(function(item) {
var el = plugin.getElement(item.id);
switch(el.type) {
case "dropdown":
var dropdown = el.lastChild;
var data = item.items.map(function(item) {
return " ";
}).join("");
if (data)
dropdown.$model.load("" + data + "");
if (item.value)
dropdown.setAttribute("value", item.value);
break;
default:
el.lastChild.setAttribute('value', item.value);
break;
}
})
}
function attachTo(htmlNode, beforeNode) {
draw();
if (htmlNode.$amlLoaded) {
container.parentNode = htmlNode;
htmlNode = htmlNode.$int;
}
htmlNode.insertBefore(container.$ext, beforeNode || null);
emit("show");
}
function detach(){
container.$ext.parentNode.removeChild(container.$ext);
emit("hide");
}
function toJson(amlNode, json) {
if (!json)
json = {};
(amlNode || container).childNodes.forEach(function(row) {
if (row.localName == 'bar')
return toJson(row, json);
var name = row.name;
if (!name) return;
var value = row.childNodes.filter(function(node) {
return node.localName != "label";
}).map(function(node) {
return node.value;
});
if (value.length == 1) value = value[0];
json[name] = value;
});
var result = emit("serialize", { json: json });
return result || json;
}
function reset(){
container.childNodes.forEach(function(row) {
var options = row.options;
if (!row.childNodes.length) return;
if (row.firstChild.localName == "checkbox")
row.firstChild.setValue(options.defaultCheckboxValue || "");
row.lastChild.setValue(options.defaultValue || "");
});
}
function validate(){
return true;
}
function getRect(){
return container.$ext.getBoundingClientRect();
}
function show(){ container.show(); emit("show"); }
function hide(){ container.hide(); emit("hide"); }
/***** Lifecycle *****/
plugin.on("load", function(){
load();
});
plugin.on("enable", function(){
});
plugin.on("disable", function(){
});
plugin.on("unload", function(){
loaded = false;
drawn = false;
});
/***** Register and define API *****/
/**
* Form Builder for Cloud9. Use this class to create forms
* that requests user input.
*
* This is what the {@link preferences.Preferences Preferences} plugin uses to draw the
* settings and what the deploy plugins use to create forms that
* allow the user to choose the deploy instances.
*
* Example:
*
* var authform = new Form({
* rowheight : 27,
* colwidth : 70,
* form : [
* {
* type : "image",
* src : "/logo.png",
* width : 180,
* height : 58
* },
* {
* title : "Username",
* name : "username",
* type : "textbox",
* },
* {
* title : "Password",
* name : "password",
* type : "password",
* },
* {
* name : "loginfail",
* type : "label",
* caption : "Could not login. Please try again.",
* style : "color:rgb(255, 143, 0);margin-left:5px;"
* },
* {
* type : "submit",
* caption : "Login",
* "default" : true,
* margin : "10 20 5 20",
* onclick : function(){
* if (authform.validate())
* login(authform.toJson(), function(){});
* }
* }
* ]
* });
*/
/**
* @constructor
* Creates a new Form instance based on an abstract definition of
* the form elements.
*
* The form elements are described by simple objects that determine
* the properties of each element. The following properties are the
* same for all form element definitions:
*
*
* Property | Possible Values |
* title | The text of the label in the first column |
* name | This value is used when creating a json object using {@link #toJson} |
* width | A different width for the element in the 2nd column |
* rowheight | A different height for this row |
* edge | The edges on the top, right, bottom, left side of the form element in pixels, space separated (e.g. "5 5 10 5"). |
* position | Integer specifying the position in the form. |
* type | The type of form element to create. The following values are supported:
*
*
* Type | Description |
* checkbox | A simple two-state checkbox |
* dropdown | A dropdown containing items that a user can select |
* spinner | A widget that lets a users select integers |
* checked-spinner | A combination of a checkbox and a spinner |
* textbox | A simple textbox |
* password | A textbox that doesn't show the content while the user types into it |
* colorbox | A widget that allows a user to select a color |
* button | A simple button |
* submit | A button without a label |
* label | A label that spans both columns |
* image | An image that spans both columns |
* divider | A divider that spans both columns |
* textarea | A multi line input widget |
* custom | A custom APF element. This can be anything |
*
* |
*
*
* Each element type also has properties that are unique for it's
* type.
*
* ## checkbox ##
*
* Property | Possible Value |
* defaultValue | The default value of the checkbox. This is the value that is set initially and after calling {@link #reset} |
* values | Array defining the values that correspond with the checked and unchecked state. (i.e. ["on", "off"]) |
*
* ## dropdown ##
*
* Property | Possible Value |
* items | Array of objects with a value and caption property. |
* defaultValue | The value of the default selected item |
* empty-message | The message displayed when no items have been selected |
*
* ## spinner ##
*
* Property | Possible Value |
* defaultValue | The default value of the spinner. This is the value that is set initially and after calling {@link #reset} |
* min | The smallest value that a number can have in the spinner |
* max | The largest value that a number can have in the spinner |
* realtime | Whether the values are updated while typing, or only when the form element has lost focus |
*
* ## checked-spinner ##
*
* Property | Possible Value |
* defaultCheckboxValue | The default value of the checkbox. This is the value that is set initially and after calling {@link #reset} |
* defaultValue | The default value of the spinner. This is the value that is set initially and after calling {@link #reset} |
* min | The smallest value that a number can have in the spinner |
* max | The largest value that a number can have in the spinner |
* realtime | Whether the values are updated while typing, or only when the form element has lost focus |
*
* ## textbox ##
*
* Property | Possible Value |
* message | The message displayed while the textbox has no value |
* defaultValue | The default value of the textbox. This is the value that is set initially and after calling {@link #reset} |
* realtime | Whether the values are updated while typing, or only when the form element has lost focus |
*
* ## password ##
*
* Property | Possible Value |
* defaultValue | The default value of the password element. This is the value that is set initially and after calling {@link #reset} |
* realtime | Whether the values are updated while typing, or only when the form element has lost focus |
*
* ## textarea ##
*
* Property | Possible Value |
* height | The height of the textarea in pixels. |
* defaultValue | The default value of the password element. This is the value that is set initially and after calling {@link #reset} |
* realtime | Whether the values are updated while typing, or only when the form element has lost focus |
*
* ## colorbox ##
*
* Property | Possible Value |
* defaultValue | The default value of the colorbox. This is the value that is set initially and after calling {@link #reset} |
* realtime | Whether the values are updated while typing, or only when the form element has lost focus |
*
* ## button ##
*
* Property | Possible Value |
* caption | The text displayed on the button |
* onclick | The function that is executed when a user clicks on the button |
*
* ## submit ##
*
* Property | Possible Value |
* margin | The edges on the top, right, bottom, left side of the submit element in pixels, space separated (e.g. "5 5 10 5"). |
* caption | The text displayed on the button |
* submenu | A reference to a menu that is shown when the button is clicked |
* onclick | The function that is executed when a user clicks on the button |
* default | Whether this is the action that is executed when the user presses Enter in one of the form elements |
*
* ## label ##
*
* Property | Possible Value |
* caption | The text displayed on the button |
* style | A css string that is applied to the label |
*
* ## image ##
*
* Property | Possible Value |
* src | The URL to load the image from |
* margin | The edges on the top, right, bottom, left side of the submit element in pixels, space separated (e.g. "5 5 10 5"). |
* height | The height of the image in pixels |
*
* ## divider ##
* *None*
*
* @param {Object} [options] The definition of the form.
* @param {String} [options.edge] The edges on the top, right, bottom, left side of the form in pixels, space separated (e.g. "5 5 10 5").
* @param {Number} [options.rowheight] The default height of each row in pixels.
* @param {Number} [options.colwidth] The width of the left column in pixels.
* @param {String} [options.className] This className is applied to the form container.
* @param {Object} [options.widths] A hash of the default widths of the form elements. The key of the hash is the type name of the form element.
* @param {HTMLElement} [options.html] The html element to attacht the form to. You can also attach the form later using {@link #attachTo}.
* @param {Object[]} [options.form] A list of form element definitions as described above.
*/
plugin.freezePublicAPI({
/**
* The HTML Element that contains the form.
* @property {HTMLElement} container
* @readonly
*/
get container(){ return container.$ext; },
/**
* The APF element that is the parent to all form elements.
* @property {AMLElement} aml
* @private
* @readonly
*/
get aml(){ return container; },
/**
* A hash of all the headings in this form, indexed by name.
* @property {Object} headings
* @private
* @readonly
*/
get headings(){ return headings; },
/**
* A meta data object that allows you to store whatever you want
* in relation to this form.
* @property {Object} meta
*/
get meta(){ return meta; },
set meta(v){ meta = v; },
_events: [
/**
* Fires when the form is drawn.
* @event draw
*/
"draw",
/**
* Fires when the form becomes visible. This happens when
* it's attached to an HTML element using the {@link #attachTo}
* method, or by calling the {@link #method-show} method.
* @event show
*/
"show",
/**
* Fires when the form becomes hidden. This happens when
* it's detached from an HTML element using the {@link #detach}
* method, or by calling the {@link #method-hide} method.
* @event hide
*/
"hide",
/**
* Fires when toJson is called. The event object contains
* a JSON object containing key value pairs. Each
* key is the value of the name property of each form
* element and the value is the current value of the form
* element at the time of calling serialize()
*
* This event allows plugins to alter the data, prior to
* it being returned to the caller of the {@link #toJson}
* method.
*
* @event serialize
* @param {Object} e
* @param {Object} e.json The JSON object representing the
* serialized state of the form.
*/
"serialize"
],
/**
* Retrieves the geometrical properties of the form
* @return {Object}
* @return {Number} return.left The left position of the form, relative to the browser window.
* @return {Number} return.top The top position of the form, relative to the browser window.
* @return {Number} return.width The width of the form.
* @return {Number} return.height The height of the form.
*/
getRect: getRect,
/**
* Returns a JSON object containing key value pairs. Each
* key is the value of the name property of each form
* element and the value is the current value of the form
* element at the time of calling serialize()
* @return {Object}
* @fires serialize
*/
toJson: toJson,
/**
* Attaches this form to an HTML element.
* @param {HTMLElement} htmlNode The HTML element to attach the form to.
* @param {HTMLElement} beforeNode The HTML element before
* which the form will be insert. This element should
* always be a child of the `htmlNode`.
* @fires show
*/
attachTo: attachTo,
/**
* Detaches this form from the HTML element it is attached to.
* @fires hide
*/
detach: detach,
/**
* Renders all the form elements, defined during the creation of
* the form instance.
* @fires draw
*/
draw: draw,
/**
* Clear all values from the form elements or return values to
* their default values, if defined.
*/
reset: reset,
/**
* Add additional form elements to the form.
* @param {Object} state The form definition. See {@link Form}.
* @param {Plugin} foreign The plugin responsible for adding these elements (needed for cleanup).
*/
add: add,
/**
* Updates form elements with new values. This method currently
* only supports updating the items of a dropdown element.
* @param {Array} items
*/
update: update,
/**
* Show the form. This requires the form to be
* {@link #attachTo attached} to an HTML element.
* @fires show
*/
show: show,
/**
* Hide the form.
* @fires hide
*/
hide: hide,
/**
* Not implemented yet. Validate the form elements.
*/
validate: validate
});
plugin.load(null, "form");
return plugin;
}
register(null, {
Form: Form
});
}
});