//>>built // wrapped by build app define("dojox/widget/DataPresentation", ["dijit","dojo","dojox","dojo/require!dojox/grid/DataGrid,dojox/charting/Chart2D,dojox/charting/widget/Legend,dojox/charting/action2d/Tooltip,dojox/charting/action2d/Highlight,dojo/colors,dojo/data/ItemFileWriteStore"], function(dijit,dojo,dojox){ dojo.provide("dojox.widget.DataPresentation"); dojo.experimental("dojox.widget.DataPresentation"); dojo.require("dojox.grid.DataGrid"); dojo.require("dojox.charting.Chart2D"); dojo.require("dojox.charting.widget.Legend"); dojo.require("dojox.charting.action2d.Tooltip"); dojo.require("dojox.charting.action2d.Highlight"); dojo.require("dojo.colors"); dojo.require("dojo.data.ItemFileWriteStore"); (function(){ // sort out the labels for the independent axis of the chart var getLabels = function(range, labelMod, charttype, domNode){ // prepare labels for the independent axis var labels = []; // add empty label, hack labels[0] = {value: 0, text: ''}; var nlabels = range.length; // auto-set labelMod for horizontal charts if the labels will otherwise collide if((charttype !== "ClusteredBars") && (charttype !== "StackedBars")){ var cwid = domNode.offsetWidth; var tmp = ("" + range[0]).length * range.length * 7; // *assume* 7 pixels width per character ( was 9 ) if(labelMod == 1){ for(var z = 1; z < 500; ++z){ if((tmp / z) < cwid){ break; } ++labelMod; } } } // now set the labels for(var i = 0; i < nlabels; i++){ //sparse labels labels.push({ value: i + 1, text: (!labelMod || i % labelMod) ? "" : range[i] }); } // add empty label again, hack labels.push({value: nlabels + 1, text:''}); return labels; }; // get the configuration of an independent axis for the chart var getIndependentAxisArgs = function(charttype, labels){ var args = { vertical: false, labels: labels, min: 0, max: labels.length-1, majorTickStep: 1, minorTickStep: 1 }; // clustered or stacked bars have a vertical independent axis if((charttype === "ClusteredBars") || (charttype === "StackedBars")){ args.vertical = true; } // lines, areas and stacked areas don't need the extra slots at each end if((charttype === "Lines") || (charttype === "Areas") || (charttype === "StackedAreas")){ args.min++; args.max--; } return args; }; // get the configuration of a dependent axis for the chart var getDependentAxisArgs = function(charttype, axistype, minval, maxval){ var args = { vertical: true, fixLower: "major", fixUpper: "major", natural: true }; // secondary dependent axis is not left-bottom if(axistype === "secondary"){ args.leftBottom = false; } // clustered or stacked bars have horizontal dependent axes if((charttype === "ClusteredBars") || (charttype === "StackedBars")){ args.vertical = false; } // ensure axis does not "collapse" for flat series if(minval == maxval){ args.min = minval - 1; args.max = maxval + 1; } return args; }; // get the configuration of a plot for the chart var getPlotArgs = function(charttype, axistype, animate){ var args = { type: charttype, hAxis: "independent", vAxis: "dependent-" + axistype, gap: 4, lines: false, areas: false, markers: false }; // clustered or stacked bars have horizontal dependent axes if((charttype === "ClusteredBars") || (charttype === "StackedBars")){ args.hAxis = args.vAxis; args.vAxis = "independent"; } // turn on lines for Lines, Areas and StackedAreas if((charttype === "Lines") || (charttype === "Hybrid-Lines") || (charttype === "Areas") || (charttype === "StackedAreas")){ args.lines = true; } // turn on areas for Areas and StackedAreas if((charttype === "Areas") || (charttype === "StackedAreas")){ args.areas = true; } // turn on markers and shadow for Lines if(charttype === "Lines"){ args.markers = true; } // turn on shadow for Hybrid-Lines // also, Hybrid-Lines is not a true chart type: use Lines for the actual plot if(charttype === "Hybrid-Lines"){ args.shadows = {dx: 2, dy: 2, dw: 2}; args.type = "Lines"; } // also, Hybrid-ClusteredColumns is not a true chart type: use ClusteredColumns for the actual plot if(charttype === "Hybrid-ClusteredColumns"){ args.type = "ClusteredColumns"; } // enable animation on the plot if animation is requested if(animate){ args.animate = animate; } return args; }; // set up a chart presentation var setupChart = function(/*DomNode*/domNode, /*Object?*/chart, /*String*/type, /*Boolean*/reverse, /*Object*/animate, /*Integer*/labelMod, /*String*/theme, /*String*/tooltip, /*Object?*/store, /*String?*/query, /*String?*/queryOptions){ var _chart = chart; if(!_chart){ domNode.innerHTML = ""; // any other content in the node disrupts the chart rendering _chart = new dojox.charting.Chart2D(domNode); } // set the theme if(theme){ // workaround for a theme bug: its _clone method // does not transfer the markers, so we repair // that omission here // FIXME this should be removed once the theme bug is fixed theme._clone = function(){ var result = new dojox.charting.Theme({ chart: this.chart, plotarea: this.plotarea, axis: this.axis, series: this.series, marker: this.marker, antiAlias: this.antiAlias, assignColors: this.assignColors, assignMarkers: this.assigneMarkers, colors: dojo.delegate(this.colors) }); result.markers = this.markers; result._buildMarkerArray(); return result; }; _chart.setTheme(theme); } var range = store.series_data[0].slice(0); // reverse the labels if requested if(reverse){ range.reverse(); } var labels = getLabels(range, labelMod, type, domNode); // collect details of whether primary and/or secondary axes are required // and what plots we have instantiated using each type of axis var plots = {}; // collect maximum and minimum data values var maxval = null; var minval = null; var seriestoremove = {}; for(var sname in _chart.runs){ seriestoremove[sname] = true; } // set x values & max data value var nseries = store.series_name.length; for(var i = 0; i < nseries; i++){ // only include series with chart=true and with some data values in if(store.series_chart[i] && (store.series_data[i].length > 0)){ var charttype = type; var axistype = store.series_axis[i]; if(charttype == "Hybrid"){ if(store.series_charttype[i] == 'line'){ charttype = "Hybrid-Lines"; }else{ charttype = "Hybrid-ClusteredColumns"; } } // ensure we have recorded that we are using this axis type if(!plots[axistype]){ plots[axistype] = {}; } // ensure we have the correct type of plot for this series if(!plots[axistype][charttype]){ var axisname = axistype + "-" + charttype; // create the plot and enable tooltips _chart.addPlot(axisname, getPlotArgs(charttype, axistype, animate)); var tooltipArgs = {}; if(typeof tooltip == 'string'){ tooltipArgs.text = function(o){ var substitutions = [o.element, o.run.name, range[o.index], ((charttype === "ClusteredBars") || (charttype === "StackedBars")) ? o.x : o.y]; return dojo.replace(tooltip, substitutions); // from Dojo 1.4 onward //return tooltip.replace(/\{([^\}]+)\}/g, function(_, token){ return dojo.getObject(token, false, substitutions); }); // prior to Dojo 1.4 } }else if(typeof tooltip == 'function'){ tooltipArgs.text = tooltip; } new dojox.charting.action2d.Tooltip(_chart, axisname, tooltipArgs); // add highlighting, except for lines if(charttype !== "Lines" && charttype !== "Hybrid-Lines"){ new dojox.charting.action2d.Highlight(_chart, axisname); } // record that this plot type is now created plots[axistype][charttype] = true; } // extract the series values var xvals = []; var valen = store.series_data[i].length; for(var j = 0; j < valen; j++){ var val = store.series_data[i][j]; xvals.push(val); if(maxval === null || val > maxval){ maxval = val; } if(minval === null || val < minval){ minval = val; } } // reverse the values if requested if(reverse){ xvals.reverse(); } var seriesargs = { plot: axistype + "-" + charttype }; if(store.series_linestyle[i]){ seriesargs.stroke = { style: store.series_linestyle[i] }; } _chart.addSeries(store.series_name[i], xvals, seriesargs); delete seriestoremove[store.series_name[i]]; } } // remove any series that are no longer needed for(sname in seriestoremove){ _chart.removeSeries(sname); } // create axes _chart.addAxis("independent", getIndependentAxisArgs(type, labels)); _chart.addAxis("dependent-primary", getDependentAxisArgs(type, "primary", minval, maxval)); _chart.addAxis("dependent-secondary", getDependentAxisArgs(type, "secondary", minval, maxval)); return _chart; }; // set up a legend presentation var setupLegend = function(/*DomNode*/domNode, /*Legend*/legend, /*Chart2D*/chart, /*Boolean*/horizontal){ // destroy any existing legend and recreate var _legend = legend; if(!_legend){ _legend = new dojox.charting.widget.Legend({ chart: chart, horizontal: horizontal }, domNode); }else{ _legend.refresh(); } return _legend; }; // set up a grid presentation var setupGrid = function(/*DomNode*/domNode, /*Object?*/grid, /*Object?*/store, /*String?*/query, /*String?*/queryOptions){ var _grid = grid || new dojox.grid.DataGrid({}, domNode); _grid.startup(); _grid.setStore(store, query, queryOptions); var structure = []; for(var ser = 0; ser < store.series_name.length; ser++){ // only include series with grid=true and with some data values in if(store.series_grid[ser] && (store.series_data[ser].length > 0)){ structure.push({ field: "data." + ser, name: store.series_name[ser], width: "auto", formatter: store.series_gridformatter[ser] }); } } _grid.setStructure(structure); return _grid; }; // set up a title presentation var setupTitle = function(/*DomNode*/domNode, /*object*/store){ if(store.title){ domNode.innerHTML = store.title; } }; // set up a footer presentation var setupFooter = function(/*DomNode*/domNode, /*object*/store){ if(store.footer){ domNode.innerHTML = store.footer; } }; // obtain a subfield from a field specifier which may contain // multiple levels (eg, "child.foo[36].manacle") var getSubfield = function(/*Object*/object, /*String*/field){ var result = object; if(field){ var fragments = field.split(/[.\[\]]+/); for(var frag = 0, l = fragments.length; frag < l; frag++){ if(result){ result = result[fragments[frag]]; } } } return result; }; dojo.declare("dojox.widget.DataPresentation", null, { // summary: // // DataPresentation // // A widget that connects to a data store in a simple manner, // and also provides some additional convenience mechanisms // for connecting to common data sources without needing to // explicitly construct a Dojo data store. The widget can then // present the data in several forms: as a graphical chart, // as a tabular grid, or as display panels presenting meta-data // (title, creation information, etc) from the data. The // widget can also create and manage several of these forms // in one simple construction. // // Note: this is a first experimental draft and any/all details // are subject to substantial change in later drafts. // // example: // // var pres = new dojox.data.DataPresentation("myChartNode", { // type: "chart", // url: "/data/mydata", // gridNode: "myGridNode" // }); // // properties: // // store: Object // Dojo data store used to supply data to be presented. This may // be supplied on construction or created implicitly based on // other construction parameters ('data', 'url'). // // query: String // Query to apply to the Dojo data store used to supply data to // be presented. // // queryOptions: String // Query options to apply to the Dojo data store used to supply // data to be presented. // // data: Object // Data to be presented. If supplied on construction this property // will override any value supplied for the 'store' property. // // url: String // URL to fetch data from in JSON format. If supplied on // construction this property will override any values supplied // for the 'store' and/or 'data' properties. Note that the data // can also be comment-filtered JSON, although this will trigger // a warning message in the console unless djConfig.useCommentedJson // has been set to true. // // urlContent: Object // Content to be passed to the URL when fetching data. If a URL has // not been supplied, this value is ignored. // // urlError: function // A function to be called if an error is encountered when fetching // data from the supplied URL. This function will be supplied with // two parameters exactly as the error function supplied to the // dojo.xhrGet function. This function may be called multiple times // if a refresh interval has been supplied. // // refreshInterval: Number // the time interval in milliseconds after which the data supplied // via the 'data' property or fetched from a URL via the 'url' // property should be regularly refreshed. This property is // ignored if neither the 'data' nor 'url' property has been // supplied. If the refresh interval is zero, no regular refresh is done. // // refreshIntervalPending: // the JavaScript set interval currently in progress, if any // // series: Array // an array of objects describing the data series to be included // in the data presentation. Each object may contain the // following fields: // datapoints: the name of the field from the source data which // contains an array of the data points for this data series. // If not supplied, the source data is assumed to be an array // of data points to be used. // field: the name of the field within each data point which // contains the data for this data series. If not supplied, // each data point is assumed to be the value for the series. // name: a name for the series, used in the legend and grid headings // namefield: the name of the field from the source data which // contains the name the series, used in the legend and grid // headings. If both name and namefield are supplied, name takes // precedence. If neither are supplied, a default name is used. // chart: true if the series should be included in a chart presentation (default: true) // charttype: the type of presentation of the series in the chart, which can be // "range", "line", "bar" (default: "bar") // linestyle: the stroke style for lines (if applicable) (default: "Solid") // axis: the dependant axis to which the series will be attached in the chart, // which can be "primary" or "secondary" // grid: true if the series should be included in a data grid presentation (default: true) // gridformatter: an optional formatter to use for this series in the data grid // // a call-back function may alternatively be supplied. The function takes // a single parameter, which will be the data (from the 'data' field or // loaded from the value in the 'url' field), and should return the array // of objects describing the data series to be included in the data // presentation. This enables the series structures to be built dynamically // after data load, and rebuilt if necessary on data refresh. The call-back // function will be called each time new data is set, loaded or refreshed. // A call-back function cannot be used if the data is supplied directly // from a Dojo data store. // // type: String // the type of presentation to be applied at the DOM attach point. // This can be 'chart', 'legend', 'grid', 'title', 'footer'. The // default type is 'chart'. type: "chart", // // chartType: String // the type of chart to display. This can be 'clusteredbars', // 'areas', 'stackedcolumns', 'stackedbars', 'stackedareas', // 'lines', 'hybrid'. The default type is 'bar'. chartType: "clusteredBars", // // reverse: Boolean // true if the chart independant axis should be reversed. reverse: false, // // animate: Object // if an object is supplied, then the chart bars or columns will animate // into place. If the object contains a field 'duration' then the value // supplied is the duration of the animation in milliseconds, otherwise // a default duration is used. A boolean value true can alternatively be // supplied to enable animation with the default duration. // The default is null (no animation). animate: null, // // labelMod: Integer // the frequency of label annotations to be included on the // independent axis. 1=every label. 0=no labels. The default is 1. labelMod: 1, // // tooltip: String | Function // a string pattern defining the tooltip text to be applied to chart // data points, or a function which takes a single parameter and returns // the tooltip text to be applied to chart data points. The string pattern // will have the following substitutions applied: // {0} - the type of chart element ('bar', 'surface', etc) // {1} - the name of the data series // {2} - the independent axis value at the tooltip data point // {3} - the series value at the tooltip data point point // The function, if supplied, will receive a single parameter exactly // as per the dojox.charting.action2D.Tooltip class. The default value // is to apply the default tooltip as defined by the // dojox.charting.action2D.Tooltip class. // // legendHorizontal: Boolean | Number // true if the legend should be rendered horizontally, or a number if // the legend should be rendered as horizontal rows with that number of // items in each row, or false if the legend should be rendered // vertically (same as specifying 1). The default is true (legend // rendered horizontally). legendHorizontal: true, // // theme: String|Theme // a theme to use for the chart, or the name of a theme. // // chartNode: String|DomNode // an optional DOM node or the id of a DOM node to receive a // chart presentation of the data. Supply only when a chart is // required and the type is not 'chart'; when the type is // 'chart' this property will be set to the widget attach point. // // legendNode: String|DomNode // an optional DOM node or the id of a DOM node to receive a // chart legend for the data. Supply only when a legend is // required and the type is not 'legend'; when the type is // 'legend' this property will be set to the widget attach point. // // gridNode: String|DomNode // an optional DOM node or the id of a DOM node to receive a // grid presentation of the data. Supply only when a grid is // required and the type is not 'grid'; when the type is // 'grid' this property will be set to the widget attach point. // // titleNode: String|DomNode // an optional DOM node or the id of a DOM node to receive a // title for the data. Supply only when a title is // required and the type is not 'title'; when the type is // 'title' this property will be set to the widget attach point. // // footerNode: String|DomNode // an optional DOM node or the id of a DOM node to receive a // footer presentation of the data. Supply only when a footer is // required and the type is not 'footer'; when the type is // 'footer' this property will be set to the widget attach point. // // chartWidget: Object // the chart widget, if any // // legendWidget: Object // the legend widget, if any // // gridWidget: Object // the grid widget, if any constructor: function(node, args){ // summary: // Set up properties and initialize. // // arguments: // node: DomNode // The node to attach the data presentation to. // kwArgs: Object (see above) // apply arguments directly dojo.mixin(this, args); // store our DOM attach point this.domNode = dojo.byId(node); // also apply the DOM attach point as the node for the presentation type this[this.type + "Node"] = this.domNode; // load the theme if provided by name if(typeof this.theme == 'string'){ this.theme = dojo.getObject(this.theme); } // resolve any the nodes that were supplied as ids this.chartNode = dojo.byId(this.chartNode); this.legendNode = dojo.byId(this.legendNode); this.gridNode = dojo.byId(this.gridNode); this.titleNode = dojo.byId(this.titleNode); this.footerNode = dojo.byId(this.footerNode); // we used to support a 'legendVertical' so for now // at least maintain backward compatibility if(this.legendVertical){ this.legendHorizontal = !this.legendVertical; } if(this.url){ this.setURL(null, null, this.refreshInterval); } else{ if(this.data){ this.setData(null, this.refreshInterval); } else{ this.setStore(); } } }, setURL: function(/*String?*/url, /*Object?*/ urlContent, /*Number?*/refreshInterval){ // summary: // Sets the URL to fetch data from, with optional content // supplied with the request, and an optional // refresh interval in milliseconds (0=no refresh) // if a refresh interval is supplied we will start a fresh // refresh after storing the supplied url if(refreshInterval){ this.cancelRefresh(); } this.url = url || this.url; this.urlContent = urlContent || this.urlContent; this.refreshInterval = refreshInterval || this.refreshInterval; var me = this; dojo.xhrGet({ url: this.url, content: this.urlContent, handleAs: 'json-comment-optional', load: function(response, ioArgs){ me.setData(response); }, error: function(xhr, ioArgs){ if(me.urlError && (typeof me.urlError == "function")){ me.urlError(xhr, ioArgs); } } }); if(refreshInterval && (this.refreshInterval > 0)){ this.refreshIntervalPending = setInterval(function(){ me.setURL(); }, this.refreshInterval); } }, setData: function(/*Object?*/data, /*Number?*/refreshInterval){ // summary: // Sets the data to be presented, and an optional // refresh interval in milliseconds (0=no refresh) // if a refresh interval is supplied we will start a fresh // refresh after storing the supplied data reference if(refreshInterval){ this.cancelRefresh(); } this.data = data || this.data; this.refreshInterval = refreshInterval || this.refreshInterval; // TODO if no 'series' property was provided, build one intelligently here // (until that is done, a 'series' property must be supplied) var _series = (typeof this.series == 'function') ? this.series(this.data) : this.series; var datasets = [], series_data = [], series_name = [], series_chart = [], series_charttype = [], series_linestyle = [], series_axis = [], series_grid = [], series_gridformatter = [], maxlen = 0; // identify the dataset arrays in which series values can be found for(var ser = 0; ser < _series.length; ser++){ datasets[ser] = getSubfield(this.data, _series[ser].datapoints); if(datasets[ser] && (datasets[ser].length > maxlen)){ maxlen = datasets[ser].length; } series_data[ser] = []; // name can be specified in series structure, or by field in series structure, otherwise use a default series_name[ser] = _series[ser].name || (_series[ser].namefield ? getSubfield(this.data, _series[ser].namefield) : null) || ("series " + ser); series_chart[ser] = (_series[ser].chart !== false); series_charttype[ser] = _series[ser].charttype || "bar"; series_linestyle[ser] = _series[ser].linestyle; series_axis[ser] = _series[ser].axis || "primary"; series_grid[ser] = (_series[ser].grid !== false); series_gridformatter[ser] = _series[ser].gridformatter; } // create an array of data points by sampling the series // and an array of series arrays by collecting the series // each data point has an 'index' item containing a sequence number // and items named "data.0", "data.1", ... containing the series samples // and the first data point also has items named "name.0", "name.1", ... containing the series names // and items named "series.0", "series.1", ... containing arrays with the complete series in var point, datapoint, datavalue, fdatavalue; var datapoints = []; for(point = 0; point < maxlen; point++){ datapoint = { index: point }; for(ser = 0; ser < _series.length; ser++){ if(datasets[ser] && (datasets[ser].length > point)){ datavalue = getSubfield(datasets[ser][point], _series[ser].field); if(series_chart[ser]){ // convert the data value to a float if possible fdatavalue = parseFloat(datavalue); if(!isNaN(fdatavalue)){ datavalue = fdatavalue; } } datapoint["data." + ser] = datavalue; series_data[ser].push(datavalue); } } datapoints.push(datapoint); } if(maxlen <= 0){ datapoints.push({index: 0}); } // now build a prepared store from the data points we've constructed var store = new dojo.data.ItemFileWriteStore({ data: { identifier: 'index', items: datapoints }}); if(this.data.title){ store.title = this.data.title; } if(this.data.footer){ store.footer = this.data.footer; } store.series_data = series_data; store.series_name = series_name; store.series_chart = series_chart; store.series_charttype = series_charttype; store.series_linestyle = series_linestyle; store.series_axis = series_axis; store.series_grid = series_grid; store.series_gridformatter = series_gridformatter; this.setPreparedStore(store); if(refreshInterval && (this.refreshInterval > 0)){ var me = this; this.refreshIntervalPending = setInterval(function(){ me.setData(); }, this.refreshInterval); } }, refresh: function(){ // summary: // If a URL or data has been supplied, refreshes the // presented data from the URL or data. If a refresh // interval is also set, the periodic refresh is // restarted. If a URL or data was not supplied, this // method has no effect. if(this.url){ this.setURL(this.url, this.urlContent, this.refreshInterval); }else if(this.data){ this.setData(this.data, this.refreshInterval); } }, cancelRefresh: function(){ // summary: // Cancels any and all outstanding data refreshes if(this.refreshIntervalPending){ // cancel existing refresh clearInterval(this.refreshIntervalPending); this.refreshIntervalPending = undefined; } }, setStore: function(/*Object?*/store, /*String?*/query, /*Object?*/queryOptions){ // FIXME build a prepared store properly -- this requires too tight a convention to be followed to be useful this.setPreparedStore(store, query, queryOptions); }, setPreparedStore: function(/*Object?*/store, /*String?*/query, /*Object?*/queryOptions){ // summary: // Sets the store and query. // this.preparedstore = store || this.store; this.query = query || this.query; this.queryOptions = queryOptions || this.queryOptions; if(this.preparedstore){ if(this.chartNode){ this.chartWidget = setupChart(this.chartNode, this.chartWidget, this.chartType, this.reverse, this.animate, this.labelMod, this.theme, this.tooltip, this.preparedstore, this.query, this,queryOptions); this.renderChartWidget(); } if(this.legendNode){ this.legendWidget = setupLegend(this.legendNode, this.legendWidget, this.chartWidget, this.legendHorizontal); } if(this.gridNode){ this.gridWidget = setupGrid(this.gridNode, this.gridWidget, this.preparedstore, this.query, this.queryOptions); this.renderGridWidget(); } if(this.titleNode){ setupTitle(this.titleNode, this.preparedstore); } if(this.footerNode){ setupFooter(this.footerNode, this.preparedstore); } } }, renderChartWidget: function(){ // summary: // Renders the chart widget (if any). This method is // called whenever a chart widget is created or // configured, and may be connected to. if(this.chartWidget){ this.chartWidget.render(); } }, renderGridWidget: function(){ // summary: // Renders the grid widget (if any). This method is // called whenever a grid widget is created or // configured, and may be connected to. if(this.gridWidget){ this.gridWidget.render(); } }, getChartWidget: function(){ // summary: // Returns the chart widget (if any) created if the type // is "chart" or the "chartNode" property was supplied. return this.chartWidget; }, getGridWidget: function(){ // summary: // Returns the grid widget (if any) created if the type // is "grid" or the "gridNode" property was supplied. return this.gridWidget; }, destroy: function(){ // summary: // Destroys the widget and all components and resources. // cancel any outstanding refresh requests this.cancelRefresh(); if(this.chartWidget){ this.chartWidget.destroy(); delete this.chartWidget; } if(this.legendWidget){ // no legend.destroy() delete this.legendWidget; } if(this.gridWidget){ // no grid.destroy() delete this.gridWidget; } if(this.chartNode){ this.chartNode.innerHTML = ""; } if(this.legendNode){ this.legendNode.innerHTML = ""; } if(this.gridNode){ this.gridNode.innerHTML = ""; } if(this.titleNode){ this.titleNode.innerHTML = ""; } if(this.footerNode){ this.footerNode.innerHTML = ""; } } }); })(); });