Moved 'getValues' function to etemplate2 class, added 'et2_surroundingsMgr' class, which is capable of managing DOM-Nodes which are inserted around an widget (see set_label function in et2_input and showMessage function in et2_baseWidget as an example of how to use it), added parsing server side validation errors

This commit is contained in:
Andreas Stöckel 2011-08-23 14:59:49 +00:00
parent 24e34f7927
commit 86414e7daa
11 changed files with 561 additions and 148 deletions

View File

@ -95,6 +95,7 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, {
}; };
this._disabled = false; this._disabled = false;
this._surroundingsMgr = null;
}, },
/** /**
@ -107,6 +108,12 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, {
this.parentNode = null; this.parentNode = null;
this._attachSet = {}; this._attachSet = {};
if (this._surroundingsMgr)
{
this._surroundingsMgr.destroy();
this._surroundingsMgr = null;
}
this._super(); this._super();
}, },
@ -158,6 +165,13 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, {
(node != this._attachSet.node || (node != this._attachSet.node ||
this.parentNode != this._attachSet.parent)) this.parentNode != this._attachSet.parent))
{ {
// If the surroundings manager exists, surround the DOM-Node of this
// widget with the DOM-Nodes inside the surroundings manager.
if (this._surroundingsMgr)
{
node = this._surroundingsMgr.getDOMNode(node);
}
this.parentNode.appendChild(node); this.parentNode.appendChild(node);
// Store the currently attached nodes // Store the currently attached nodes
@ -176,6 +190,15 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, {
return this.parentNode != null; return this.parentNode != null;
}, },
getSurroundings: function() {
if (!this._surroundingsMgr)
{
this._surroundingsMgr = new et2_surroundingsMgr(this);
}
return this._surroundingsMgr;
},
/** /**
* Set the parent DOM node of this element. If another parent node is already * Set the parent DOM node of this element. If another parent node is already
* set, this widget removes itself from the DOM tree * set, this widget removes itself from the DOM tree
@ -283,4 +306,210 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, {
} }
}); });
/**
* The surroundings manager class allows to append or prepend elements around
* an widget node.
*/
var et2_surroundingsMgr = Class.extend({
init: function(_widget) {
this.widget = _widget;
this._widgetContainer = null;
this._widgetSurroundings = [];
this._widgetPlaceholder = null;
this._widgetNode = null;
this._ownPlaceholder = true;
},
destroy: function() {
this._widgetContainer = null;
this._widgetSurroundings = null;
this._widgetPlaceholder = null;
this._widgetNode = null;
},
prependDOMNode: function(_node) {
this._widgetSurroundings.unshift(_node);
this._surroundingsUpdated = true;
},
appendDOMNode: function(_node) {
// Append an placeholder first if none is existing yet
if (this._ownPlaceholder && this._widgetPlaceholder == null)
{
this._widgetPlaceholder = document.createElement("span");
this._widgetSurroundings.push(this._widgetPlaceholder);
}
// Append the given node
this._widgetSurroundings.push(_node);
this._surroundingsUpdated = true;
},
insertDOMNode: function(_node) {
if (!this._ownPlaceholder || this._widgetPlaceholder == null)
{
this.appendDOMNode(_node);
return;
}
// Get the index of the widget placeholder and delete it, insert the
// given node instead
var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder);
this._widgetSurroundings.splice(idx, 1, _node);
// Delete the reference to the own placeholder
this._widgetPlaceholder = null;
this._ownPlaceholder = false;
},
removeDOMNode: function(_node) {
for (var i = 0; i < this._widgetSurroundings.length; i++)
{
if (this._widgetSurroundings[i] == _node)
{
this._widgetSurroundings.splice(i, 1);
this._surroundingsUpdated = true;
break;
}
}
},
setWidgetPlaceholder: function(_node) {
if (_node != this._widgetPlaceholder)
{
if (_node != null && this._ownPlaceholder && this._widgetPlaceholder != null)
{
// Delete the current placeholder which was created by the
// widget itself
var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder);
this._widgetSurroundings.splice(idx, 1);
// Delete any reference to the own placeholder and set the
// _ownPlaceholder flag to false
this._widgetPlaceholder = null;
this._ownPlaceholder = false;
}
this._ownPlaceholder = (_node == null);
this._widgetPlaceholder = _node;
this._surroundingsUpdated = true;
}
},
_rebuildContainer: function() {
// Return if there has been no change in the "surroundings-data"
if (!this._surroundingsUpdated)
{
return false;
}
// Build the widget container
if (this._widgetSurroundings.length > 0)
{
// Check whether the widgetPlaceholder is really inside the DOM-Tree
var hasPlaceholder = et2_hasChild(this._widgetSurroundings,
this._widgetPlaceholder);
// If not, append another widget placeholder
if (!hasPlaceholder)
{
this._widgetPlaceholder = document.createElement("span");
this._widgetSurroundings.push(this._widgetPlaceholder);
this._ownPlaceholder = true;
}
// If the surroundings array only contains one element, set this one
// as the widget container
if (this._widgetSurroundings.length == 1)
{
if (this._widgetSurroundings[0] == this._widgetPlaceholder)
{
this._widgetContainer = null;
}
else
{
this._widgetContainer = this._widgetSurroundings[0];
}
}
else
{
// Create an outer "span" as widgetContainer
this._widgetContainer = document.createElement("span");
// Append the children inside the widgetSurroundings array to
// the widget container
for (var i = 0; i < this._widgetSurroundings.length; i++)
{
this._widgetContainer.appendChild(this._widgetSurroundings[i]);
}
}
}
else
{
this._widgetContainer = null;
this._widgetPlaceholder = null;
}
this._surroundingsUpdated = false;
return true;
},
update: function() {
if (this._surroundingsUpdated)
{
var attached = this.widget ? this.widget.isAttached() : false;
// Reattach the widget - this will call the "getDOMNode" function
// and trigger the _rebuildContainer function.
if (attached && this.widget)
{
this.widget.detatchFromDOM();
this.widget.attachToDOM();
}
}
},
getDOMNode: function(_widgetNode) {
// Update the whole widgetContainer if this is not the first time this
// function has been called but the widget node has changed.
if (this._widgetNode != null && this._widgetNode != _widgetNode)
{
this._surroundingsUpdated = true;
}
// Copy a reference to the given node
this._widgetNode = _widgetNode;
// Build the container if it didn't exist yet.
var updated = this._rebuildContainer(true);
// Return the widget node itself if there are no surroundings arround
// it
if (this._widgetContainer == null)
{
return _widgetNode;
}
// Replace the widgetPlaceholder with the given widget node if the
// widgetContainer has been updated
if (updated)
{
this._widgetPlaceholder.parentNode.replaceChild(_widgetNode,
this._widgetPlaceholder);
if (!this._ownPlaceholder)
{
this._widgetPlaceholder = _widgetNode;
}
}
// Return the widget container
return this._widgetContainer;
}
});

View File

@ -58,10 +58,7 @@ var et2_baseWidget = et2_DOMWidget.extend(et2_IAligned, {
this.node = null; this.node = null;
this.statustext = ""; this.statustext = "";
this._messageDiv = null;
this._labelContainer = null;
this._widgetPlaceholder = null;
this._tooltipElem = null; this._tooltipElem = null;
}, },
@ -69,6 +66,113 @@ var et2_baseWidget = et2_DOMWidget.extend(et2_IAligned, {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.node = null; this.node = null;
this._messageDiv = null;
},
/**
* The setMessage function can be used to attach a small message box to the
* widget. This is e.g. used to display validation errors or success messages
*
* @param _text is the text which should be displayed as a message
* @param _type is an css class which is attached to the message box.
* Currently available are "hint", "success" and "validation_error", defaults
* to "hint"
* @param _floating if true, the object will be in one row with the element,
* defaults to true
* @param _prepend if set, the message is displayed behind the widget node
* instead of before. Defaults to false.
*/
showMessage: function(_text, _type, _floating, _prepend) {
// Preset the parameters
if (typeof _type == "undefined")
{
_type = "hint"
}
if (typeof _floating == "undefined")
{
_floating = true;
}
if (typeof _prepend == "undefined")
{
_prepend = false;
}
var surr = this.getSurroundings();
// Remove the message div from the surroundings before creating a new
// one
this.hideMessgae(false, true);
// Create the message div and add it to the "surroundings" manager
this._messageDiv = $j(document.createElement("div"))
.addClass("message")
.addClass(_type)
.addClass(_floating ? "floating" : "")
.text(_text);
// Decide whether to prepend or append the div
if (_prepend)
{
surr.prependDOMNode(this._messageDiv[0]);
}
else
{
surr.appendDOMNode(this._messageDiv[0]);
}
surr.update();
},
/**
* The hideMessgae function can be used to hide a previously shown message.
*
* @param _fade if true, the message div will fade out, otherwise the message
* div is removed immediately. Defaults to true.
* @param _noUpdate is used internally to prevent an update of the surroundings
* manager.
*/
hideMessgae: function(_fade, _noUpdate) {
if (typeof _fade == "undefined")
{
_fade = true;
}
if (typeof _noUpdate == "undefined")
{
_noUpdate = false;
}
// Remove the message from the surroundings manager and remove the
// reference to it
if (this._messageDiv != null)
{
var surr = this.getSurroundings();
var self = this;
var _done = function() {
surr.removeDOMNode(self._messageDiv[0]);
self._messageDiv = null;
// Update the surroundings manager
if (!_noUpdate)
{
surr.update();
}
}
// Either fade out or directly call the function which removes the div
if (_fade)
{
this._messageDiv.fadeOut("fast", _done);
}
else
{
_done();
}
}
}, },
detatchFromDOM: function() { detatchFromDOM: function() {
@ -127,6 +231,15 @@ var et2_baseWidget = et2_DOMWidget.extend(et2_IAligned, {
return this.getDOMNode(this); return this.getDOMNode(this);
}, },
click: function(_node) {
if (this.onclick)
{
return this.onclick.call(_node);
}
return true;
},
set_statustext: function(_value) { set_statustext: function(_value) {
// Don't execute the code below, if no tooltip will be attached/detached // Don't execute the code below, if no tooltip will be attached/detached
if (_value == "" && !this._tooltipElem) if (_value == "" && !this._tooltipElem)
@ -156,15 +269,6 @@ var et2_baseWidget = et2_DOMWidget.extend(et2_IAligned, {
} }
}, },
click: function(_node) {
if (this.onclick)
{
return this.onclick.call(_node);
}
return true;
},
set_align: function(_value) { set_align: function(_value) {
this.align = _value; this.align = _value;
}, },

View File

@ -566,3 +566,29 @@ function et2_cloneObject(_obj)
return result; return result;
} }
/**
* Returns true if the given array of nodes or their children contains the given
* child node.
*/
function et2_hasChild(_nodes, _child)
{
for (var i = 0; i < _nodes.length; i++)
{
if (_nodes[i] == _child)
{
return true;
}
else if (_nodes[i].childNodes)
{
var res = et2_hasChild(_nodes[i].childNodes, _child);
if (res)
{
return true;
}
}
}
return false;
}

View File

@ -62,6 +62,12 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, {
"name": "onchange", "name": "onchange",
"type": "js", "type": "js",
"description": "JS code which is executed when the value changes." "description": "JS code which is executed when the value changes."
},
"validation_error": {
"name": "Validation Error",
"type": "string",
"default": et2_no_init,
"description": "Used internally to store the validation error that came from the server."
} }
}, },
@ -70,7 +76,6 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, {
this._oldValue = ""; this._oldValue = "";
this._labelContainer = null; this._labelContainer = null;
this._widgetPlaceholder = null;
}, },
destroy: function() { destroy: function() {
@ -83,34 +88,23 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this._labelContainer = null; this._labelContainer = null;
this._widgetPlaceholder = null; },
/**
* Load the validation errors from the server
*/
transformAttributes: function(_attrs) {
this._super.apply(this, arguments);
// Check whether an validation error entry exists
var val = this.getArrayMgr("validation_errors").getValueForID(this.id);
if (val)
{
_attrs["validation_error"] = val;
}
}, },
attachToDOM: function() { attachToDOM: function() {
if (this._labelContainer)
{
// Get the DOM Node without the labelContainer - that's why null is
// passed here.
var node = this.getDOMNode(null);
if (node)
{
// Recreate the widget placeholder and return, as the set_label
// function will call attachToDOM again
if (!this._widgetPlaceholder)
{
this.set_label(this.label);
return;
}
// Insert the widget at the position of the placeholder
this._labelContainer.replaceChild(node, this._widgetPlaceholder);
// Set the widgetPlaceholder to null
this._widgetPlaceholder = null;
}
}
var node = this.getInputNode(); var node = this.getInputNode();
if (node) if (node)
{ {
@ -121,14 +115,14 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, {
this._super.apply(this,arguments); this._super.apply(this,arguments);
$j(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved // $j(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved
$j(this.getInputNode()).validator(); // $j(this.getInputNode()).validator();
}, },
detatchFromDOM: function() { detatchFromDOM: function() {
if(this.getInputNode()) { // if(this.getInputNode()) {
$j(this.getInputNode()).data("validator").destroy(); // $j(this.getInputNode()).data("validator").destroy();
} // }
this._super.apply(this,arguments); this._super.apply(this,arguments);
}, },
@ -171,14 +165,10 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, {
}, },
set_label: function(_value) { set_label: function(_value) {
// Copy the given value // Abort if ther was no change in the label
this.label = _value; if (_value == this.label)
// Detach the current element from the DOM
var attached = this.isAttached();
if (attached)
{ {
this.detatchFromDOM(); return;
} }
if (_value) if (_value)
@ -186,46 +176,49 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, {
// Create the label container if it didn't exist yet // Create the label container if it didn't exist yet
if (this._labelContainer == null) if (this._labelContainer == null)
{ {
this._labelContainer = document.createElement("label"); this._labelContainer = $j(document.createElement("label"))
this._labelContainer.setAttribute("class", "et2_label"); .addClass("et2_label");
this.getSurroundings().insertDOMNode(this._labelContainer[0]);
} }
// Clear the label container. // Clear the label container.
for (;this._labelContainer.childNodes.length > 0;) this._labelContainer.empty();
{
this._labelContainer.removeChild(this._labelContainer.childNodes[0]);
}
// Create the placeholder element // Create the placeholder element and set it
this._widgetPlaceholder = document.createElement("span"); var ph = document.createElement("span");
this.getSurroundings().setWidgetPlaceholder(ph);
// Split the label at the "%s" // Split the label at the "%s"
var parts = et2_csvSplit(_value, 2, "%s"); var parts = et2_csvSplit(_value, 2, "%s");
// Create the content of the label container // Update the content of the label container
for (var i = 0; i < parts.length; i++) for (var i = 0; i < parts.length; i++)
{ {
if (parts[i]) if (parts[i])
{ {
this._labelContainer.appendChild(document.createTextNode(parts[i])); this._labelContainer.append(document.createTextNode(parts[i]));
} }
if (i == 0) if (i == 0)
{ {
this._labelContainer.appendChild(this._widgetPlaceholder); this._labelContainer.append(ph);
} }
} }
} }
else else
{ {
// Delete the labelContainer from the surroundings object
if (this._labelContainer)
{
this.getSurroundings().deleteDOMNode(this._labelContainer[0]);
}
this._labelContainer = null; this._labelContainer = null;
this._widgetPlaceholder = null;
} }
// Attach the current element to the DOM again // Update the surroundings in order to reflect the change in the label
if (attached) this.getSurroundings().update();
{
this.attachToDOM(); // Copy the given value
} this.label = _value;
}, },
set_required: function(_value) { set_required: function(_value) {
@ -241,13 +234,21 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, {
}, },
getDOMNode: function(_sender) { set_validation_error: function(_value) {
if (_sender == this && this._labelContainer) var node = this.getInputNode();
if (node)
{ {
return this._labelContainer; if (_value === false)
{
this.hideMessage();
$j(node).removeClass("invalid");
}
else
{
this.showMessage(_value, "validation_error");
$j(node).addClass("invalid");
}
} }
return this._super.apply(this, arguments);
}, },
getInputNode: function() { getInputNode: function() {

View File

@ -585,77 +585,6 @@ var et2_widget = Class.extend({
return true; return true;
}, },
/**
* Fetches all input element values and returns them in an associative
* array. Widgets which introduce namespacing can use the internal _target
* parameter to add another layer.
*/
getValues: function() {
var result = {};
// Iterate over the widget tree
this.iterateOver(function(_widget) {
// The widget must have an id to be included in the values array
if (_widget.id == "")
{
return;
}
// Get the path to the node we have to store the value at
var path = _widget.getArrayMgr("content").getPath();
// check if id contains a hierachical name, eg. "button[save]"
var id = _widget.id;
if (_widget.id.indexOf('[') != -1)
{
var parts = _widget.id.replace(/]/g,'').split('[');
id = parts.pop();
path = path.concat(parts);
}
// Set the _target variable to that node
var _target = result;
for (var i = 0; i < path.length; i++)
{
// Create a new object for not-existing path nodes
if (typeof _target[path[i]] == "undefined")
{
_target[path[i]] = {};
}
// Check whether the path node is really an object
if (_target[path[i]] instanceof Object)
{
_target = _target[path[i]];
}
else
{
et2_debug("error", "ID collision while writing at path " +
"node '" + path[i] + "'");
}
}
// Check whether the entry is really undefined
if (typeof _target[id] != "undefined")
{
et2_debug("error", _widget, "Overwriting value of '" + _widget.id +
"', id exists twice!");
}
// Store the value of the widget and reset its dirty flag
var value = _widget.getValue();
if (value !== null)
{
_target[id] = value;
}
_widget.resetDirty();
}, this, et2_IInput);
return result;
},
/** /**
* Sets all array manager objects - this function can be used to set the * Sets all array manager objects - this function can be used to set the
* root array managers of the container object. * root array managers of the container object.

View File

@ -166,7 +166,7 @@ etemplate2.prototype.submit = function()
if(!valid) return false;*/ if(!valid) return false;*/
// Get the form values // Get the form values
var values = this.widgetContainer.getValues(); var values = this.getValues(this.widgetContainer);
// Trigger the submit event // Trigger the submit event
if (this.fireEvent("submit", [values])) if (this.fireEvent("submit", [values]))
@ -184,6 +184,78 @@ etemplate2.prototype.submit = function()
} }
} }
/**
* Fetches all input element values and returns them in an associative
* array. Widgets which introduce namespacing can use the internal _target
* parameter to add another layer.
*/
etemplate2.prototype.getValues = function(_root)
{
var result = {};
// Iterate over the widget tree
_root.iterateOver(function(_widget) {
// The widget must have an id to be included in the values array
if (_widget.id == "")
{
return;
}
// Get the path to the node we have to store the value at
var path = _widget.getArrayMgr("content").getPath();
// check if id contains a hierachical name, eg. "button[save]"
var id = _widget.id;
if (_widget.id.indexOf('[') != -1)
{
var parts = _widget.id.replace(/]/g,'').split('[');
id = parts.pop();
path = path.concat(parts);
}
// Set the _target variable to that node
var _target = result;
for (var i = 0; i < path.length; i++)
{
// Create a new object for not-existing path nodes
if (typeof _target[path[i]] == "undefined")
{
_target[path[i]] = {};
}
// Check whether the path node is really an object
if (_target[path[i]] instanceof Object)
{
_target = _target[path[i]];
}
else
{
et2_debug("error", "ID collision while writing at path " +
"node '" + path[i] + "'");
}
}
// Check whether the entry is really undefined
if (typeof _target[id] != "undefined")
{
et2_debug("error", _widget, "Overwriting value of '" + _widget.id +
"', id exists twice!");
}
// Store the value of the widget and reset its dirty flag
var value = _widget.getValue();
if (value !== null)
{
_target[id] = value;
}
_widget.resetDirty();
}, this, et2_IInput);
return result;
}
/** /**
* Adds an callback function to the given event slot * Adds an callback function to the given event slot
* *

View File

@ -74,6 +74,9 @@ var timesheet_data = {
"tabs":{ "tabs":{
"customfields":true "customfields":true
} }
} },
"validation_errors": {
"ts_title": "Please enter some meaningful title here!"
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

View File

@ -219,8 +219,8 @@ input[required] {
background-color: #ffffd0; background-color: #ffffd0;
} }
input.invalid { input.invalid {
border-style: dotted; border: 1px solid #a6261d;
border-color: red; background-color: #faecec;
} }
.error { .error {
-moz-border-radius: 0 4px 4px 0; -moz-border-radius: 0 4px 4px 0;
@ -268,3 +268,52 @@ label input, label span, label div, label select, label textarea {
margin-right: 1ex; margin-right: 1ex;
} }
/**
* Message styles
*/
/* Style used for a generic message (such as success messages or validation errors) */
.message {
display: block;
border: 1px solid gray;
padding: 3px 3px 3px 22px;
margin: 5px 0px 5px 0px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
background-repeat: no-repeat;
background-position: 3px center;
clear: left;
max-width: 500px;
}
.message.floating {
display: inline;
margin: 0px 5px 0px 5px;
}
.message.validation_error {
color: #a93030;
font-style: italic;
background-color: #f3d4d0;
border-color: #a93030;
background-image:url(gfx/error.png);
}
.message.success {
font-style: normal;
background-color: #e5f3d0;
color: #98a930;
border-color: #9ea930;
background-image:url(gfx/tick.png);
}
.message.hint {
font-style: normal;
background-color: #d9e2ed;
border-color: #56729a;
color: #56729a;
background-image:url(gfx/hint.png);
}