From 86414e7daa9cc734b868de604e6706ebf3babfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20St=C3=B6ckel?= Date: Tue, 23 Aug 2011 14:59:49 +0000 Subject: [PATCH] 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 --- etemplate/js/et2_DOMWidget.js | 229 ++++++++++++++++++ etemplate/js/et2_baseWidget.js | 130 +++++++++- etemplate/js/et2_common.js | 26 ++ etemplate/js/et2_inputWidget.js | 121 ++++----- etemplate/js/et2_widget.js | 71 ------ etemplate/js/etemplate2.js | 74 +++++- .../js/test/et2_test_timesheet_edit.json | 5 +- etemplate/js/test/gfx/error.png | Bin 0 -> 701 bytes etemplate/js/test/gfx/hint.png | Bin 0 -> 778 bytes etemplate/js/test/gfx/tick.png | Bin 0 -> 537 bytes etemplate/js/test/test.css | 53 +++- 11 files changed, 561 insertions(+), 148 deletions(-) create mode 100644 etemplate/js/test/gfx/error.png create mode 100644 etemplate/js/test/gfx/hint.png create mode 100644 etemplate/js/test/gfx/tick.png diff --git a/etemplate/js/et2_DOMWidget.js b/etemplate/js/et2_DOMWidget.js index eb8877a806..6e1e5ff273 100644 --- a/etemplate/js/et2_DOMWidget.js +++ b/etemplate/js/et2_DOMWidget.js @@ -95,6 +95,7 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { }; this._disabled = false; + this._surroundingsMgr = null; }, /** @@ -107,6 +108,12 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { this.parentNode = null; this._attachSet = {}; + if (this._surroundingsMgr) + { + this._surroundingsMgr.destroy(); + this._surroundingsMgr = null; + } + this._super(); }, @@ -158,6 +165,13 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { (node != this._attachSet.node || 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); // Store the currently attached nodes @@ -176,6 +190,15 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { 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, 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; + } + +}); + diff --git a/etemplate/js/et2_baseWidget.js b/etemplate/js/et2_baseWidget.js index 27b5c99604..e5529b3f48 100644 --- a/etemplate/js/et2_baseWidget.js +++ b/etemplate/js/et2_baseWidget.js @@ -58,10 +58,7 @@ var et2_baseWidget = et2_DOMWidget.extend(et2_IAligned, { this.node = null; this.statustext = ""; - - this._labelContainer = null; - this._widgetPlaceholder = null; - + this._messageDiv = null; this._tooltipElem = null; }, @@ -69,6 +66,113 @@ var et2_baseWidget = et2_DOMWidget.extend(et2_IAligned, { this._super.apply(this, arguments); 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() { @@ -127,6 +231,15 @@ var et2_baseWidget = et2_DOMWidget.extend(et2_IAligned, { return this.getDOMNode(this); }, + click: function(_node) { + if (this.onclick) + { + return this.onclick.call(_node); + } + + return true; + }, + set_statustext: function(_value) { // Don't execute the code below, if no tooltip will be attached/detached 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) { this.align = _value; }, diff --git a/etemplate/js/et2_common.js b/etemplate/js/et2_common.js index caca8dea39..023993a6c5 100644 --- a/etemplate/js/et2_common.js +++ b/etemplate/js/et2_common.js @@ -566,3 +566,29 @@ function et2_cloneObject(_obj) 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; +} + diff --git a/etemplate/js/et2_inputWidget.js b/etemplate/js/et2_inputWidget.js index a227077659..23170cf53d 100644 --- a/etemplate/js/et2_inputWidget.js +++ b/etemplate/js/et2_inputWidget.js @@ -62,6 +62,12 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, { "name": "onchange", "type": "js", "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._labelContainer = null; - this._widgetPlaceholder = null; }, destroy: function() { @@ -83,34 +88,23 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, { this._super.apply(this, arguments); 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() { - 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(); if (node) { @@ -121,14 +115,14 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, { this._super.apply(this,arguments); - $j(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved - $j(this.getInputNode()).validator(); +// $j(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved +// $j(this.getInputNode()).validator(); }, detatchFromDOM: function() { - if(this.getInputNode()) { - $j(this.getInputNode()).data("validator").destroy(); - } +// if(this.getInputNode()) { +// $j(this.getInputNode()).data("validator").destroy(); +// } this._super.apply(this,arguments); }, @@ -171,14 +165,10 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, { }, set_label: function(_value) { - // Copy the given value - this.label = _value; - - // Detach the current element from the DOM - var attached = this.isAttached(); - if (attached) + // Abort if ther was no change in the label + if (_value == this.label) { - this.detatchFromDOM(); + return; } if (_value) @@ -186,46 +176,49 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, { // Create the label container if it didn't exist yet if (this._labelContainer == null) { - this._labelContainer = document.createElement("label"); - this._labelContainer.setAttribute("class", "et2_label"); + this._labelContainer = $j(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); } // Clear the label container. - for (;this._labelContainer.childNodes.length > 0;) - { - this._labelContainer.removeChild(this._labelContainer.childNodes[0]); - } + this._labelContainer.empty(); - // Create the placeholder element - this._widgetPlaceholder = document.createElement("span"); + // Create the placeholder element and set it + var ph = document.createElement("span"); + this.getSurroundings().setWidgetPlaceholder(ph); // Split the label at the "%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++) { if (parts[i]) { - this._labelContainer.appendChild(document.createTextNode(parts[i])); + this._labelContainer.append(document.createTextNode(parts[i])); } if (i == 0) { - this._labelContainer.appendChild(this._widgetPlaceholder); + this._labelContainer.append(ph); } } } else { + // Delete the labelContainer from the surroundings object + if (this._labelContainer) + { + this.getSurroundings().deleteDOMNode(this._labelContainer[0]); + } this._labelContainer = null; - this._widgetPlaceholder = null; } - // Attach the current element to the DOM again - if (attached) - { - this.attachToDOM(); - } + // Update the surroundings in order to reflect the change in the label + this.getSurroundings().update(); + + // Copy the given value + this.label = _value; }, set_required: function(_value) { @@ -241,13 +234,21 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, { }, - getDOMNode: function(_sender) { - if (_sender == this && this._labelContainer) + set_validation_error: function(_value) { + 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() { diff --git a/etemplate/js/et2_widget.js b/etemplate/js/et2_widget.js index b4a71aed32..3d74e71285 100644 --- a/etemplate/js/et2_widget.js +++ b/etemplate/js/et2_widget.js @@ -585,77 +585,6 @@ var et2_widget = Class.extend({ 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 * root array managers of the container object. diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index 16d43690bd..2a5cfb97b1 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -166,7 +166,7 @@ etemplate2.prototype.submit = function() if(!valid) return false;*/ // Get the form values - var values = this.widgetContainer.getValues(); + var values = this.getValues(this.widgetContainer); // Trigger the submit event 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 * diff --git a/etemplate/js/test/et2_test_timesheet_edit.json b/etemplate/js/test/et2_test_timesheet_edit.json index c19ea96e45..51c6d9a925 100644 --- a/etemplate/js/test/et2_test_timesheet_edit.json +++ b/etemplate/js/test/et2_test_timesheet_edit.json @@ -74,6 +74,9 @@ var timesheet_data = { "tabs":{ "customfields":true } - } + }, + "validation_errors": { + "ts_title": "Please enter some meaningful title here!" + } } diff --git a/etemplate/js/test/gfx/error.png b/etemplate/js/test/gfx/error.png new file mode 100644 index 0000000000000000000000000000000000000000..c37bd062e60c3b38fc82e4d1f236a8ac2fae9d8c GIT binary patch literal 701 zcmV;u0z&N#0$9Ug7g~-`rQ^qx~m@y2OU8A z#zh~=7n#Z$Z*fx-GOtDf07cgx0suCz_W(2~Y(0tf@FX@P6EPuM_dgn$vj9LucO)%W zw%HgMW>=#oL>nZ>M&NEf08>)#)k<{$fCT_r>rPi=BV=hFh6WS^qqze>C6Ek}o{M5% za|@JGowu0t{&hgNzySHZxy@LTNh);YzZ2zSp_ zl$^T&Dnc|NLb&RD_!4>pt@VHdP)ZGER%5ZmWEe$lryR&y;2u^3cOkO4#6c%-(EY6a{600000NkvXXu0mjfxS2AI literal 0 HcmV?d00001 diff --git a/etemplate/js/test/gfx/hint.png b/etemplate/js/test/gfx/hint.png new file mode 100644 index 0000000000000000000000000000000000000000..12cd1aef900803abba99b26920337ec01ad5c267 GIT binary patch literal 778 zcmV+l1NHogP)BVme|mWaqy4$_pJm?y9KM{-*hp?1+Ey3e-CEDooTa!B;e(Q>TSF?bj>5At13y1p zriN3w3x~5SfZj{@J4M{kp{?=M_Lh2bV+5LH)Q)5W!-ePA$RgE1@5f1cyHki0Y}JyVEYZF(LD$xXlt$7A5CgE@ zpV-&l%vf;=5kZ2-2gi@Y6J&=cuwt>!vJ^#(&n|LcZyUzi6Duj$$hJ1s*HD-#;k-w@ zpdrwAuoDG_N2bvb07G$Zk*?Hc)JLtW4yqOnic_$zO7NZ#l>Fm){;fE?b$IbOaX2fe z0la4g0Dfw2xk7Wi7NapVD8YMPCZu?A1QCK*67dgsvRKBLFtrM>?$%&_lD1882mzdO zWPdw5KWw6IT`m1b_8=lS5jt8D3=RDa=&jWzR-)S@56WMslZ~mKu1)-wpXB>rNBQ>N zU#K`#1B&v|_AQK;7I~B}OdGiUT9LX>f0xm6<;LeP!=vFjPsUQF*wCJ*dO)4YBypgdiuF!=i@6Zyi7F|q#K zz?tlSZULa@t1D?$e;f@b36&N!V2mjOHw|*Hs{AQG2a)rMyf zFQK~pm1x3+7!nu%-M`k}``c>^00{o_1pjWJUTfl8mg=3qGEl8H@}^@w`VUx0_$uy4 z2FhRqKX}xI*?Tv1DJd8z#F#0c%*~rM30HE1@2o5m~}ZyoWhqv>ql{V z1ZGE0lgcoK^lx+eqc*rAX1Ky;Xx3U%u#zG!m-;eD1Qsn@kf3|F9qz~|95=&g3(7!X zB}JAT>RU;a%vaNOGnJ%e1=K6eAh43c(QN8RQ6~GP%O}Jju$~Ld*%`mO1p