From ecb972ca66ce5cbcd6e053b999f2750ecd61545d Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 18 Aug 2015 17:47:40 +0000 Subject: [PATCH] Etemplate now uses JSON instead of XML for templates on the client side. XML files are parsed into JSON objects on the server, then sent as JSON. Etemplate parses the JSON object on the client side instead of the XML file directly. This is supposed to be faster for IE. --- etemplate/inc/class.etemplate_new.inc.php | 4 +- .../class.etemplate_widget_template.inc.php | 7 + etemplate/js/et2_core_widget.js | 215 +++++++++--------- etemplate/js/et2_core_xml.js | 27 ++- etemplate/js/et2_extension_customfields.js | 4 +- etemplate/js/et2_widget_box.js | 26 +-- etemplate/js/et2_widget_entry.js | 2 +- etemplate/js/et2_widget_grid.js | 38 ++-- etemplate/js/et2_widget_hbox.js | 6 +- etemplate/js/et2_widget_selectbox.js | 4 +- etemplate/js/et2_widget_split.js | 2 +- etemplate/js/et2_widget_tabs.js | 16 +- etemplate/js/et2_widget_template.js | 62 ++--- etemplate/js/etemplate2.js | 36 +-- etemplate/template.php | 126 ++++++++++ 15 files changed, 366 insertions(+), 209 deletions(-) create mode 100644 etemplate/template.php diff --git a/etemplate/inc/class.etemplate_new.inc.php b/etemplate/inc/class.etemplate_new.inc.php index 9fa896933f..54ca498bfe 100644 --- a/etemplate/inc/class.etemplate_new.inc.php +++ b/etemplate/inc/class.etemplate_new.inc.php @@ -166,9 +166,11 @@ class etemplate_new extends etemplate_widget_template // Info required to load the etemplate client-side $dom_id = str_replace('.','-',$this->dom_id); + $filename = etemplate_widget_template::rel2path(etemplate_widget_template::relPath($name)); $load_array = array( 'name' => $this->name, - 'url' => etemplate_widget_template::rel2url($this->rel_path), + 'url' => egw_framework::link('/etemplate/template.php', array('name' => $this->name, 'download' => filemtime($filename))), + // etemplate_widget_template::rel2url($this->rel_path), 'data' => $data, 'DOMNodeID' => $dom_id, ); diff --git a/etemplate/inc/class.etemplate_widget_template.inc.php b/etemplate/inc/class.etemplate_widget_template.inc.php index 47965567b9..a796f8850b 100644 --- a/etemplate/inc/class.etemplate_widget_template.inc.php +++ b/etemplate/inc/class.etemplate_widget_template.inc.php @@ -35,6 +35,13 @@ class etemplate_widget_template extends etemplate_widget */ protected static $cache = array(); + /** + * Tell egw framework it's ok to call this + */ + public $public_functions = array( + 'ajaxtoJSON' => true + ); + /** * Get instance of template specified by name, template(-set) and version * diff --git a/etemplate/js/et2_core_widget.js b/etemplate/js/et2_core_widget.js index f3b3a78636..2f7d0ceb9d 100644 --- a/etemplate/js/et2_core_widget.js +++ b/etemplate/js/et2_core_widget.js @@ -471,16 +471,15 @@ var et2_widget = ClassWithAttributes.extend( }, /** - * The parseXMLAttrs function takes an XML DOM attributes object + * The parseJSONAttrs function takes a JSON object * and adds the given attributes to the _target associative array. This * function also parses the legacyOptions. * - * @param _attrsObj is the XML DOM attributes object + * @param _attrsObj is the JSON object * @param {object} _target is the object to which the attributes should be written. * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute */ - parseXMLAttrs: function(_attrsObj, _target, _proto) { - + parseJSONAttrs: function(_attrsObj, _target, _proto) { // Check whether the attributes object is really existing, if not abort if (typeof _attrsObj == "undefined") { @@ -488,71 +487,62 @@ var et2_widget = ClassWithAttributes.extend( } // Iterate over the given attributes and parse them - var mgr = this.getArrayMgr("content"); - for (var i = 0; i < _attrsObj.length; i++) + for (var name in _attrsObj) { - var attrName = _attrsObj[i].name; - var attrValue = _attrsObj[i].value; + this._parseAttr(name, _attrsObj[name], _target, _proto); + } + }, - // Special handling for the legacy options - if (attrName == "options" && _proto.legacyOptions.length > 0) + /** + * Parse a single attribute + * + * @param {string} attrName Attribute name + * @param {object} attrValue Attribute value + * @param {object} _target is the object to which the attributes should be written. + * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute + */ + _parseAttr: function(attrName, attrValue, _target, _proto) + { + var mgr = this.getArrayMgr("content"); + // Special handling for the legacy options + if (attrName == "options" && _proto.legacyOptions.length > 0) + { + // Check for modifications on legacy options here. Normal modifications + // are handled in widget constructor, but it's too late for legacy options then + if(_target.id && this.getArrayMgr("modifications").getEntry(_target.id)) { - // Check for modifications on legacy options here. Normal modifications - // are handled in widget constructor, but it's too late for legacy options then - if(_target.id && this.getArrayMgr("modifications").getEntry(_target.id)) - { - var mod = this.getArrayMgr("modifications").getEntry(_target.id); - if(typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options; - } - // expand legacyOptions with content - if(attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) - { - attrValue = mgr.expandName(attrValue); - } - - // Parse the legacy options (as a string, other types not allowed) - var splitted = et2_csvSplit(attrValue+""); - - for (var j = 0; j < splitted.length && j < _proto.legacyOptions.length; j++) - { - // Blank = not set - if(splitted[j].trim().length == 0) continue; - - // Check to make sure we don't overwrite a current option with a legacy option - if(typeof _target[_proto.legacyOptions[j]] === "undefined") - { - attrValue = splitted[j]; - - /** - If more legacy options than expected, stuff them all in the last legacy option - Some legacy options take a comma separated list. - */ - if(j == _proto.legacyOptions.length - 1 && splitted.length > _proto.legacyOptions.length) - { - attrValue = splitted.slice(j); - } - - var attr = _proto.attributes[_proto.legacyOptions[j]]; - - // If the attribute is marked as boolean, parse the - // expression as bool expression. - if (attr.type == "boolean") - { - attrValue = mgr.parseBoolExpression(attrValue); - } - else if (typeof attrValue != "object") - { - attrValue = mgr.expandName(attrValue); - } - _target[_proto.legacyOptions[j]] = attrValue; - } - } + var mod = this.getArrayMgr("modifications").getEntry(_target.id); + if(typeof mod.options != "undefined") attrValue = attrValue = mod.options; } - else + // expand legacyOptions with content + if(attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) { - if (mgr != null && typeof _proto.attributes[attrName] != "undefined") + attrValue = mgr.expandName(attrValue); + } + + // Parse the legacy options (as a string, other types not allowed) + var splitted = et2_csvSplit(attrValue+""); + + for (var j = 0; j < splitted.length && j < _proto.legacyOptions.length; j++) + { + // Blank = not set + if(splitted[j].trim().length == 0) continue; + + // Check to make sure we don't overwrite a current option with a legacy option + if(typeof _target[_proto.legacyOptions[j]] === "undefined") { - var attr = _proto.attributes[attrName]; + attrValue = splitted[j]; + + /** + If more legacy options than expected, stuff them all in the last legacy option + Some legacy options take a comma separated list. + */ + if(j == _proto.legacyOptions.length - 1 && splitted.length > _proto.legacyOptions.length) + { + attrValue = splitted.slice(j); + } + + var attr = _proto.attributes[_proto.legacyOptions[j]]; // If the attribute is marked as boolean, parse the // expression as bool expression. @@ -560,16 +550,35 @@ var et2_widget = ClassWithAttributes.extend( { attrValue = mgr.parseBoolExpression(attrValue); } - else + else if (typeof attrValue != "object") { attrValue = mgr.expandName(attrValue); } + _target[_proto.legacyOptions[j]] = attrValue; } - - // Set the attribute - _target[attrName] = attrValue; } } + else + { + if (mgr != null && typeof _proto.attributes[attrName] != "undefined") + { + var attr = _proto.attributes[attrName]; + + // If the attribute is marked as boolean, parse the + // expression as bool expression. + if (attr.type == "boolean") + { + attrValue = mgr.parseBoolExpression(attrValue); + } + else + { + attrValue = mgr.expandName(attrValue); + } + } + + // Set the attribute + _target[attrName] = attrValue; + } }, /** @@ -622,47 +631,51 @@ var et2_widget = ClassWithAttributes.extend( }, /** - * Create a et2_widget from an XML node. + * Create a et2_widget from a JSON object. * * First the type and attributes are read from the node. Then the readonly & modifications * arrays are checked for changes specific to the loaded data. Then the appropriate * constructor is called. After the constructor returns, the widget has a chance to - * further initialize itself from the XML node when the widget's loadFromXML() method - * is called with the node. + * further initialize itself from the object when the widget's loadFromObject() method + * is called with the object. * - * @param _node XML node to read + * @param _node Object to read * * @return et2_widget */ - createElementFromNode: function(_node) { + createElementFromObject: function(_node) { var attributes = {}; + if(typeof _node.attributes === 'undefined') + { + _node.attributes = {}; + } // Parse the "readonly" and "type" flag for this element here, as they // determine which constructor is used - var _nodeName = attributes["type"] = _node.getAttribute("type") ? - _node.getAttribute("type") : _node.nodeName.toLowerCase(); + var _nodeName = attributes["type"] = _node.attributes.type ? + _node.attributes.type : _node.tag; var readonly = attributes["readonly"] = this.getArrayMgr("readonlys").isReadOnly( - _node.getAttribute("id"), _node.getAttribute("readonly"), + _node.attributes.id, _node.attributes.readonly, typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly ); // Check to see if modifications change type var modifications = this.getArrayMgr("modifications"); - if(modifications && _node.getAttribute("id")) { - var entry = modifications.getEntry(_node.getAttribute("id")); + if(modifications && _node.attributes.id) { + var entry = modifications.getEntry(_node.attributes.id); if(entry == null) { // Try again, but skip the fancy stuff // TODO: Figure out why the getEntry() call doesn't always work - var entry = modifications.data[_node.getAttribute("id")]; + var entry = modifications.data[_node.attributes.id]; if(entry) { - this.egw().debug("warn", "getEntry("+_node.getAttribute("id")+") failed, but the data is there.", modifications, entry); + this.egw().debug("warn", "getEntry("+_node.attributes.id+") failed, but the data is there.", modifications, entry); } else { // Try the root, in case a namespace got missed - var entry = modifications.getRoot().getEntry(_node.getAttribute("id")); + var entry = modifications.getRoot().getEntry(_node.attributes.id); } } if(entry && entry.type) @@ -689,7 +702,7 @@ var et2_widget = ClassWithAttributes.extend( } // Parse the attributes from the given XML attributes object - this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype); + this.parseJSONAttrs(_node.attributes, attributes, constructor.prototype); // Do an sanity check for the attributes constructor.prototype.generateAttributeSet(attributes); @@ -699,39 +712,31 @@ var et2_widget = ClassWithAttributes.extend( var widget = new constructor(this, attributes); // Load the widget itself from XML - widget.loadFromXML(_node); + widget.loadFromJSON(_node); return widget; }, /** - * Loads the widget tree from an XML node + * Loads the widget tree from JSON object * - * @param _node xml node + * @param {Object} _content */ - loadFromXML: function(_node) { - // Load the child nodes. - for (var i = 0; i < _node.childNodes.length; i++) + loadFromJSON: function(object) + { + if(object.content) { - var node = _node.childNodes[i]; - var widgetType = node.nodeName.toLowerCase(); - - if (widgetType == "#comment") - { - continue; - } - - if (widgetType == "#text") - { - if (node.data.replace(/^\s+|\s+$/g, '')) - { - this.loadContent(node.data); - } - continue; - } - + this.loadContent(object.content); + } + if(!object.children) return; + // Load the child nodes. + for (var i = 0; i < object.children.length; i++) + { + var node = object.children[i]; + node.parentNode = object; + // Create the new element - this.createElementFromNode(node); + this.createElementFromObject(node); } }, diff --git a/etemplate/js/et2_core_xml.js b/etemplate/js/et2_core_xml.js index ffbdd727ae..8616b9b484 100644 --- a/etemplate/js/et2_core_xml.js +++ b/etemplate/js/et2_core_xml.js @@ -64,11 +64,14 @@ function et2_directChildrenByTagName(_node, _tagName) _tagName = _tagName.toLowerCase(); var result = []; - for (var i = 0; i < _node.childNodes.length; i++) + var children = _node.childNodes || _node.children || []; + for (var i = 0; i < children.length; i++) { - if (_tagName == _node.childNodes[i].nodeName.toLowerCase()) + var child = children[i]; + child.parentNode = _node; + if (child.nodeName && _tagName === child.nodeName.toLowerCase() || child.tag && _tagName === child.tag) { - result.push(_node.childNodes[i]); + result.push(child); } } @@ -77,10 +80,12 @@ function et2_directChildrenByTagName(_node, _tagName) function et2_filteredNodeIterator(_node, _callback, _context) { - for (var i = 0; i < _node.childNodes.length; i++) + if(!_node.children) return; + for (var i = 0; i < _node.children.length; i++) { - var node = _node.childNodes[i]; - var nodeName = node.nodeName.toLowerCase(); + var node = _node.children[i]; + node.parentNode = _node; + var nodeName = node.tag; if (nodeName.charAt(0) != "#") { _callback.call(_context, node, nodeName); @@ -90,9 +95,15 @@ function et2_filteredNodeIterator(_node, _callback, _context) function et2_readAttrWithDefault(_node, _name, _default) { + if( _node.getAttribute) + { var val = _node.getAttribute(_name); - - return (val === null) ? _default : val; + } + else if (_node.attributes) + { + var val = _node.attributes[_name]; + } + return (val === null || typeof val === 'undefined') ? _default : val; } diff --git a/etemplate/js/et2_extension_customfields.js b/etemplate/js/et2_extension_customfields.js index dcbae50553..49d960cd3a 100644 --- a/etemplate/js/et2_extension_customfields.js +++ b/etemplate/js/et2_extension_customfields.js @@ -328,13 +328,13 @@ var et2_customfields_list = et2_valueWidget.extend([et2_IDetachedDOM, et2_IInput } }, - loadFromXML: function(_node) { + loadFromJSON: function(_node) { this.loadFields(); // Load the nodes as usual this._super.apply(this, arguments); }, - + set_value: function(_value) { if(!this.options.customfields) return; for(var field_name in this.options.customfields) diff --git a/etemplate/js/et2_widget_box.js b/etemplate/js/et2_widget_box.js index 7aa45b1363..6ee5fa278f 100644 --- a/etemplate/js/et2_widget_box.js +++ b/etemplate/js/et2_widget_box.js @@ -53,7 +53,7 @@ var et2_box = et2_baseWidget.extend([et2_IDetachedDOM], * * @param {object} _node */ - loadFromXML: function(_node) { + loadFromJSON: function(_node) { if(this._type != "box") { return this._super.apply(this, arguments); @@ -61,30 +61,16 @@ var et2_box = et2_baseWidget.extend([et2_IDetachedDOM], // Load the child nodes. var childIndex = 0; var repeatNode = null; - for (var i=0; i < _node.childNodes.length; i++) + for (var i=0; i < _node.children.length; i++) { - var node = _node.childNodes[i]; - var widgetType = node.nodeName.toLowerCase(); - - if (widgetType == "#comment") - { - continue; - } - - if (widgetType == "#text") - { - if (node.data.replace(/^\s+|\s+$/g, '')) - { - this.loadContent(node.data); - } - continue; - } + var node = _node.children[i]; + var widgetType = node.tag; // Create the new element, if no expansion needed var id = et2_readAttrWithDefault(node, "id", ""); if(id.indexOf('$') < 0 || widgetType != 'box') { - this.createElementFromNode(node); + this.createElementFromObject(node); childIndex++; } else @@ -109,7 +95,7 @@ var et2_box = et2_baseWidget.extend([et2_IDetachedDOM], } } - this.createElementFromNode(repeatNode); + this.createElementFromObject(repeatNode); } // Reset diff --git a/etemplate/js/et2_widget_entry.js b/etemplate/js/et2_widget_entry.js index 26cfbdbecf..2aa2e088d8 100644 --- a/etemplate/js/et2_widget_entry.js +++ b/etemplate/js/et2_widget_entry.js @@ -86,7 +86,7 @@ var et2_entry = et2_valueWidget.extend( this.setDOMNode(document.createElement('span')); }, - loadFromXML: function(_node) { + loadFromJSON: function(_node) { // Load the nodes as usual this._super.apply(this, arguments); diff --git a/etemplate/js/et2_widget_grid.js b/etemplate/js/et2_widget_grid.js index 487ec4352e..553cb754ee 100644 --- a/etemplate/js/et2_widget_grid.js +++ b/etemplate/js/et2_widget_grid.js @@ -352,9 +352,9 @@ var et2_grid = et2_DOMWidget.extend([et2_IDetachedDOM, et2_IAligned, et2_IResize var cell = this._getCell(cells, x, y); // Read the span value of the element - if (node.getAttribute("span")) + if (node.attributes.span) { - cell.rowSpan = node.getAttribute("span"); + cell.rowSpan = node.attributes.span; } else { @@ -423,9 +423,9 @@ var et2_grid = et2_DOMWidget.extend([et2_IDetachedDOM, et2_IAligned, et2_IResize var cell = this._getCell(cells, x, y); // Read the span value of the element - if (node.getAttribute("span")) + if (node.attributes && node.attributes.span) { - cell.colSpan = node.getAttribute("span"); + cell.colSpan = node.attributes.span; } else { @@ -441,20 +441,20 @@ var et2_grid = et2_DOMWidget.extend([et2_IDetachedDOM, et2_IAligned, et2_IResize var span = cell.colSpan = this._forceNumber(cell.colSpan); // Read the align value of the element - if (node.getAttribute("align")) + if (node.attributes && node.attributes.align) { - cell.align = node.getAttribute("align"); + cell.align = node.attributes.align; } // store id of nextmatch-*headers, so it is available for disabled widgets, which get not instanciated if (nodeName.substr(0, 10) == 'nextmatch-') { - cell.nm_id = node.getAttribute('id'); + cell.nm_id = node.attributes.id; } // Apply widget's class to td, for backward compatability - if(node.getAttribute("class")) + if(node.attributes && node.attributes.class) { - cell.class += (cell.class ? " " : "") + node.getAttribute("class"); + cell.class += (cell.class ? " " : "") + node.attributes.class; } // Create the element @@ -480,7 +480,7 @@ var et2_grid = et2_DOMWidget.extend([et2_IDetachedDOM, et2_IAligned, et2_IResize } - var widget = this.createElementFromNode(node, nodeName); + var widget = this.createElementFromObject(node, nodeName); } // Fill all cells the widget is spanning @@ -520,7 +520,7 @@ var et2_grid = et2_DOMWidget.extend([et2_IDetachedDOM, et2_IAligned, et2_IResize } // If row disabled, just skip it var disabled = false; - if(node.getAttribute("disabled") == "1") + if(et2_readAttrWithDefault(node, "disabled", false) == "1") { disabled = true; } @@ -595,23 +595,23 @@ var et2_grid = et2_DOMWidget.extend([et2_IDetachedDOM, et2_IAligned, et2_IResize /** * As the does not fit very well into the default widget structure, we're - * overwriting the loadFromXML function and doing a two-pass reading - + * overwriting the loadFromJSON function and doing a two-pass reading - * in the first step the * * @param {object} _node xml node to process */ - loadFromXML: function(_node) { + loadFromJSON: function(_node) { // Keep the node for later changing / reloading this.template_node = _node; // Get the columns and rows tag - var rowsElems = et2_directChildrenByTagName(_node, "rows"); - var columnsElems = et2_directChildrenByTagName(_node, "columns"); + var rowsElems = _node.children[1]; + var columnsElems = _node.children[0]; - if (rowsElems.length == 1 && columnsElems.length == 1) + if (rowsElems && columnsElems) { - var columns = columnsElems[0]; - var rows = rowsElems[0]; + var columns = columnsElems; + var rows = rowsElems; var colData = []; var rowData = []; @@ -870,7 +870,7 @@ var et2_grid = et2_DOMWidget.extend([et2_IDetachedDOM, et2_IAligned, et2_IResize } // Rebuild grid - this.loadFromXML(this.template_node); + this.loadFromJSON(this.template_node); // New widgets need to finish this.loadingFinished(); diff --git a/etemplate/js/et2_widget_hbox.js b/etemplate/js/et2_widget_hbox.js index 53f56bbe8f..a30c255d31 100644 --- a/etemplate/js/et2_widget_hbox.js +++ b/etemplate/js/et2_widget_hbox.js @@ -96,15 +96,15 @@ var et2_hbox = et2_baseWidget.extend( }, /** - * The overwritten loadFromXML function checks whether any child element has + * The overwritten function checks whether any child element has * a special align value. * * @param {object} _node */ - loadFromXML: function(_node) { + loadFromJSON: function(_node) { // Check whether any child node has an alignment tag et2_filteredNodeIterator(_node, function(_node) { - var align = _node.getAttribute("align"); + var align = et2_readAttrWithDefault(_node, 'align',''); if (!align) { diff --git a/etemplate/js/et2_widget_selectbox.js b/etemplate/js/et2_widget_selectbox.js index 6cce32cfbc..87ac181274 100644 --- a/etemplate/js/et2_widget_selectbox.js +++ b/etemplate/js/et2_widget_selectbox.js @@ -398,9 +398,9 @@ var et2_selectbox = et2_inputWidget.extend( return true; }, - loadFromXML: function(_node) { + loadFromJSON: function(_node) { // Handle special case where legacy option for empty label is used (conflicts with rows), and rows is set as an attribute - var legacy = _node.getAttribute("options"); + var legacy = _node.attributes.options || false; if(legacy) { var legacy = legacy.split(","); diff --git a/etemplate/js/et2_widget_split.js b/etemplate/js/et2_widget_split.js index f0c09e8a61..15e780fe46 100644 --- a/etemplate/js/et2_widget_split.js +++ b/etemplate/js/et2_widget_split.js @@ -114,7 +114,7 @@ var et2_split = et2_DOMWidget.extend([et2_IResizeable,et2_IPrint], * Tap in here to check if we have real children, because all children should be created * by this point. If there are, replace the placeholders. */ - loadFromXML: function() { + loadFromJSON: function() { this._super.apply(this, arguments); if(this._children.length > 0) { diff --git a/etemplate/js/et2_widget_tabs.js b/etemplate/js/et2_widget_tabs.js index e947d8d79f..f1b71ac416 100644 --- a/etemplate/js/et2_widget_tabs.js +++ b/etemplate/js/et2_widget_tabs.js @@ -132,7 +132,7 @@ var et2_tabbox = et2_valueWidget.extend([et2_IInput,et2_IResizeable], "contentDiv": null, "flagDiv": null, "hidden": hide, - "XMLNode": null, + "JSON": null, "promise": null }); } @@ -157,7 +157,7 @@ var et2_tabbox = et2_valueWidget.extend([et2_IInput,et2_IResizeable], if (i < tabData.length) { // Store node for later evaluation - tabData[i].XMLNode = node; + tabData[i].JSON = node; } else { @@ -167,7 +167,7 @@ var et2_tabbox = et2_valueWidget.extend([et2_IInput,et2_IResizeable], }, this); }, - loadFromXML: function(_node) { + loadFromJSON: function(_node) { // Get the tabs and tabpanels tags var tabsElems = et2_directChildrenByTagName(_node, "tabs"); var tabpanelsElems = et2_directChildrenByTagName(_node, "tabpanels"); @@ -219,7 +219,7 @@ var et2_tabbox = et2_valueWidget.extend([et2_IInput,et2_IResizeable], "contentDiv": null, "flagDiv": null, "hidden": typeof tab.hidden != "undefined" ? tab.hidden : readonly[tab_id] || false, - "XMLNode": null, + "JSON": null, "promise": null }); } @@ -274,12 +274,12 @@ var et2_tabbox = et2_valueWidget.extend([et2_IInput,et2_IResizeable], _loadTab: function(index,promises) { var tabData = this.tabData[index]; if(!tabData || tabData.loaded) return; - if(tabData.XMLNode != null) + if(tabData.JSON != null) { - tabData.widget = this.createElementFromNode(tabData.XMLNode,tabData.XMLNode.nodeName.toLowerCase()); + tabData.widget = this.createElementFromObject(tabData.JSON,tabData.JSON.tag); - // Release the XML node - tabData.XMLNode = null; + // Release the JSON object + tabData.JSON = null; } else if (tabData.widget_options) { diff --git a/etemplate/js/et2_widget_template.js b/etemplate/js/et2_widget_template.js index a4f21a375c..a95ca5ddda 100644 --- a/etemplate/js/et2_widget_template.js +++ b/etemplate/js/et2_widget_template.js @@ -86,10 +86,10 @@ var et2_template = et2_DOMWidget.extend( var cache_buster = parts.length > 1 ? parts.pop() : null; var template_name = parts.pop(); - // Check to see if XML is known - var xml = null; + // Check to see if the template is known + var template = null; var templates = etemplate2.prototype.templates; // use global eTemplate cache - if(!(xml = templates[template_name])) + if(!(template = templates[template_name])) { // Check to see if ID is short form --> prepend parent/top-level name if(template_name.indexOf('.') < 0) @@ -98,42 +98,52 @@ var et2_template = et2_DOMWidget.extend( var top_name = root && root._inst ? root._inst.name : null; if (top_name && template_name.indexOf('.') < 0) template_name = top_name+'.'+template_name; } - xml = templates[template_name]; - if(!xml) + template = templates[template_name]; + if(!template) { // Ask server var splitted = template_name.split('.'); // use template base url from initial template, to continue using webdav, if that was loaded via webdav - var path = this.getRoot()._inst.template_base_url + splitted.shift() + "/templates/default/" + - splitted.join('.')+ ".xet" + (cache_buster ? '?download='+cache_buster : + var path = this.getRoot()._inst.template_base_url + + splitted.join('.') + (cache_buster ? '&download='+cache_buster : // if server did not give a cache-buster, fall back to current time - '?download='+(new Date).valueOf()); + '&download='+(new Date).valueOf()); if(splitted.length) { - et2_loadXMLFromURL(path, function(_xmldoc) { - // Scan for templates and store them - for(var i = 0; i < _xmldoc.childNodes.length; i++) { - var template = _xmldoc.childNodes[i]; - if(template.nodeName.toLowerCase() != "template") continue; - templates[template.getAttribute("id")] = template; + jQuery.ajax({ + url: path, + context: this, + type: 'GET', + dataType: 'json', + success: function(_data, _status, _xmlhttp){ + for(var i = 0; i < _data.children.length; i++) + { + var template = _data.children[i]; + if(template.tag !== "template") continue; + templates[template.attributes.id] = template; + }// Read the structure of the requested template + if (typeof templates[template_name] != 'undefined') this.loadFromJSON(templates[template_name]); + + // Update flag + this.loading.resolve(); + }, + error: function(_xmlhttp, _err) { + egw().debug('error', 'Loading eTemplate from '+_url+' failed! '+_xmlhttp.status+' '+_xmlhttp.statusText); } - - // Read the XML structure of the requested template - if (typeof templates[template_name] != 'undefined') this.loadFromXML(templates[template_name]); - - // Update flag - this.loading.resolve(); - - }, this); + }); } return; } } - if(xml !== null && typeof xml !== "undefined") + if(template !== null && typeof template !== "undefined") { - this.egw().debug("log", "Loading template from XML: ", template_name); - this.loadFromXML(xml); + this.egw().debug("log", "Loading template: ", template_name); + if(template.tag) + { + this.loadFromJSON(template); + } + // Don't call this here - done by caller, or on whole widget tree //this.loadingFinished(); @@ -142,7 +152,7 @@ var et2_template = et2_DOMWidget.extend( } else { - this.egw().debug("warn", "Unable to find XML for ", template_name); + this.egw().debug("warn", "Unable to find ", template_name); this.loading.reject(); } } diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index d7832f4193..01ff8c4a73 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -446,8 +446,11 @@ etemplate2.prototype.load = function(_name, _url, _data, _callback) } etemplate2._byTemplate[_name].push(this); - // Read the XML structure of the requested template - this.widgetContainer.loadFromXML(this.templates[this.name]); + // Read the structure of the requested template + if (this.templates[this.name].children) + { + this.widgetContainer.loadFromJSON(this.templates[this.name]); + } // List of Promises from widgets that are not quite fully loaded var deferred = []; @@ -543,18 +546,25 @@ etemplate2.prototype.load = function(_name, _url, _data, _callback) // Load & process if(!this.templates[_name]) { - // Asynchronously load the XET file - et2_loadXMLFromURL(_url, function(_xmldoc) { - - // Scan for templates and store them - for(var i = 0; i < _xmldoc.childNodes.length; i++) { - var template = _xmldoc.childNodes[i]; - if(template.nodeName.toLowerCase() != "template") continue; - this.templates[template.getAttribute("id")] = template; - if(!_name) this.name = template.getAttribute("id"); + jQuery.ajax({ + url: _url, + context: this, + type: 'GET', + dataType: 'json', + success: function(_data, _status, _xmlhttp){ + for(var i = 0; i < _data.children.length; i++) + { + var template = _data.children[i]; + if(template.tag !== "template") continue; + this.templates[template.attributes.id] = template; + if(!_name) this.name = template.attributes.id; + } + _load.apply(this,[]); + }, + error: function(_xmlhttp, _err) { + egw().debug('error', 'Loading eTemplate from '+_url+' failed! '+_xmlhttp.status+' '+_xmlhttp.statusText); } - _load.apply(this,[]); - }, this); + }); // Split the given data into array manager objects and pass those to the // widget container - do this here because file is loaded async diff --git a/etemplate/template.php b/etemplate/template.php new file mode 100644 index 0000000000..7c816e4207 --- /dev/null +++ b/etemplate/template.php @@ -0,0 +1,126 @@ + 'etemplate', + 'noheader' => true, + 'nonavbar' => true +); +include ('../header.inc.php'); + +if (!ajaxtoJSON($_GET['name'])) +{ + header('404 Not found'); + http_response_code(404); +} + +/** +* Gets the specified template XML file converted to JSON representation +* +* @param String $name +* @return JSON +*/ +function ajaxtoJSON($name) +{ + if(!$name) + { + $name = get_var('name'); + } + $filename = etemplate_widget_template::rel2path(etemplate_widget_template::relPath($name)); + // Bad template name + if(trim($filename) == '') + { + return false; + } + error_log("Filename: $filename"); + + $mtime = filemtime($filename); + + // First, check cache + $cached = egw_cache::getInstance('etemplate', $name); + + // Not found, or modified + if(!$cached || !is_array($cached) || is_array($cached) && $cached['mtime'] != $mtime) + { + // Load XML & parse into JSON + $reader = simplexml_load_file($filename); + $template = json_encode(nodeToArray($reader)); + $cached = array( + 'template' => $template, + 'mtime' => $mtime + ); + } + else if ($cached); + { + $template = $cached['template']; + } + if($cached) + { + // Keep in instance cache so we don't have to regenerate it + egw_cache::setInstance('etemplate', $name, $cached, CACHE_TIME); + } + else + { + return false; + } + + // Should set some headers so the browser can cache it too + header('Cache-Control: public, max-age='.CACHE_TIME); + header('Expires: ' . gmdate('D, d M Y H:i:s', time()+CACHE_TIME) . ' GMT'); + header('Content-type: application/json'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime)); + header('Content-Length: ' . mb_strlen($template)); + + echo $template; + return true; +} + +function nodeToArray($xmlnode, &$jsnode = false) +{ + if(!$xmlnode) return; + + if(!($xmlnode instanceof SimpleXMLElement) && trim($xmlnode)) + { + $jsnode['content'] = $xmlnode; + return ''; + } + $nodename = $xmlnode->getName(); + $node =& $jsnode ? $jsnode : array(); + $node['tag'] = strtolower($nodename); + $node['attributes'] = array(); + + if (count($xmlnode->attributes()) > 0) + { + $node["attributes"] = array(); + foreach($xmlnode->attributes() as $key => $value) + { + $node["attributes"][$key] = (string)$value; + } + } + + if(trim($xmlnode->__toString()) != '') + { + $node['content'] = $xmlnode->__toString(); + } + + // Load children + $child_index = 0; + foreach ($xmlnode->children() as $childxmlnode) + { + $node['children'][$child_index] = array('tag' => $childxmlnode->getName()); + nodeToArray($childxmlnode, $node['children'][$child_index++]); + } + return $node; +}