define([ "dojo/_base/kernel", // kernel.deprecated "dojo/_base/lang", // lang.mixin lang.delegate lang.hitch lang.isFunction lang.isObject "../_Widget", "../_Container", "./_ContentPaneResizeMixin", "dojo/string", // string.substitute "dojo/html", // html._ContentSetter "dojo/i18n!../nls/loading", "dojo/_base/array", // array.forEach "dojo/_base/declare", // declare "dojo/_base/Deferred", // Deferred "dojo/dom", // dom.byId "dojo/dom-attr", // domAttr.attr "dojo/dom-construct", // empty() "dojo/_base/xhr", // xhr.get "dojo/i18n", // i18n.getLocalization "dojo/when" ], function(kernel, lang, _Widget, _Container, _ContentPaneResizeMixin, string, html, nlsLoading, array, declare, Deferred, dom, domAttr, domConstruct, xhr, i18n, when){ // module: // dijit/layout/ContentPane return declare("dijit.layout.ContentPane", [_Widget, _Container, _ContentPaneResizeMixin], { // summary: // A widget containing an HTML fragment, specified inline // or by uri. Fragment may include widgets. // // description: // This widget embeds a document fragment in the page, specified // either by uri, javascript generated markup or DOM reference. // Any widgets within this content are instantiated and managed, // but laid out according to the HTML structure. Unlike IFRAME, // ContentPane embeds a document fragment as would be found // inside the BODY tag of a full HTML document. It should not // contain the HTML, HEAD, or BODY tags. // For more advanced functionality with scripts and // stylesheets, see dojox/layout/ContentPane. This widget may be // used stand alone or as a base class for other widgets. // ContentPane is useful as a child of other layout containers // such as BorderContainer or TabContainer, but note that those // widgets can contain any widget as a child. // // example: // Some quick samples: // To change the innerHTML: // | cp.set('content', 'new content')` // Or you can send it a NodeList: // | cp.set('content', dojo.query('div [class=selected]', userSelection)) // To do an ajax update: // | cp.set('href', url) // href: String // The href of the content that displays now. // Set this at construction if you want to load data externally when the // pane is shown. (Set preload=true to load it immediately.) // Changing href after creation doesn't have any effect; Use set('href', ...); href: "", // content: String|DomNode|NodeList|dijit/_Widget // The innerHTML of the ContentPane. // Note that the initialization parameter / argument to set("content", ...) // can be a String, DomNode, Nodelist, or _Widget. content: "", // extractContent: Boolean // Extract visible content from inside of ` .... `. // I.e., strip `` and `` (and it's contents) from the href extractContent: false, // parseOnLoad: Boolean // Parse content and create the widgets, if any. parseOnLoad: true, // parserScope: String // Flag passed to parser. Root for attribute names to search for. If scopeName is dojo, // will search for data-dojo-type (or dojoType). For backwards compatibility // reasons defaults to dojo._scopeName (which is "dojo" except when // multi-version support is used, when it will be something like dojo16, dojo20, etc.) parserScope: kernel._scopeName, // preventCache: Boolean // Prevent caching of data from href's by appending a timestamp to the href. preventCache: false, // preload: Boolean // Force load of data on initialization even if pane is hidden. preload: false, // refreshOnShow: Boolean // Refresh (re-download) content when pane goes from hidden to shown refreshOnShow: false, // loadingMessage: String // Message that shows while downloading loadingMessage: "${loadingState}", // errorMessage: String // Message that shows if an error occurs errorMessage: "${errorState}", // isLoaded: [readonly] Boolean // True if the ContentPane has data in it, either specified // during initialization (via href or inline content), or set // via set('content', ...) / set('href', ...) // // False if it doesn't have any content, or if ContentPane is // still in the process of downloading href. isLoaded: false, baseClass: "dijitContentPane", /*====== // ioMethod: dojo/_base/xhr.get|dojo._base/xhr.post // Function that should grab the content specified via href. ioMethod: dojo.xhrGet, ======*/ // ioArgs: Object // Parameters to pass to xhrGet() request, for example: // |
ioArgs: {}, // onLoadDeferred: [readonly] dojo.Deferred // This is the `dojo.Deferred` returned by set('href', ...) and refresh(). // Calling onLoadDeferred.then() registers your // callback to be called only once, when the prior set('href', ...) call or // the initial href parameter to the constructor finishes loading. // // This is different than an onLoad() handler which gets called any time any href // or content is loaded. onLoadDeferred: null, // Cancel _WidgetBase's _setTitleAttr because we don't want the title attribute (used to specify // tab labels) to be copied to ContentPane.domNode... otherwise a tooltip shows up over the // entire pane. _setTitleAttr: null, // Flag to parser that I'll parse my contents, so it shouldn't. stopParser: true, // template: [private] Boolean // Flag from the parser that this ContentPane is inside a template // so the contents are pre-parsed. // TODO: this declaration can be commented out in 2.0 template: false, markupFactory: function(params, node, ctor){ var self = new ctor(params, node); // If a parse has started but is waiting for modules to load, then return a Promise for when the parser // finishes. Don't return a promise though for the case when content hasn't started loading because the // ContentPane is hidden and it has an href (ex: hidden pane of a TabContainer). In that case we consider // that initialization has already finished. return !self.href && self._contentSetter && self._contentSetter.parseDeferred && !self._contentSetter.parseDeferred.isFulfilled() ? self._contentSetter.parseDeferred.then(function(){ return self; }) : self; }, create: function(params, srcNodeRef){ // Convert a srcNodeRef argument into a content parameter, so that the original contents are // processed in the same way as contents set via set("content", ...), calling the parser etc. // Avoid modifying original params object since that breaks NodeList instantiation, see #11906. if((!params || !params.template) && srcNodeRef && !("href" in params) && !("content" in params)){ srcNodeRef = dom.byId(srcNodeRef); var df = srcNodeRef.ownerDocument.createDocumentFragment(); while(srcNodeRef.firstChild){ df.appendChild(srcNodeRef.firstChild); } params = lang.delegate(params, {content: df}); } this.inherited(arguments, [params, srcNodeRef]); }, postMixInProperties: function(){ this.inherited(arguments); var messages = i18n.getLocalization("dijit", "loading", this.lang); this.loadingMessage = string.substitute(this.loadingMessage, messages); this.errorMessage = string.substitute(this.errorMessage, messages); }, buildRendering: function(){ this.inherited(arguments); // Since we have no template we need to set this.containerNode ourselves, to make getChildren() work. // For subclasses of ContentPane that do have a template, does nothing. if(!this.containerNode){ this.containerNode = this.domNode; } // remove the title attribute so it doesn't show up when hovering // over a node (TODO: remove in 2.0, no longer needed after #11490) this.domNode.removeAttribute("title"); }, startup: function(){ // summary: // Call startup() on all children including non _Widget ones like dojo/dnd/Source objects // This starts all the widgets this.inherited(arguments); // And this catches stuff like dojo/dnd/Source if(this._contentSetter){ array.forEach(this._contentSetter.parseResults, function(obj){ if(!obj._started && !obj._destroyed && lang.isFunction(obj.startup)){ obj.startup(); obj._started = true; } }, this); } }, _startChildren: function(){ // summary: // Called when content is loaded. Calls startup on each child widget. Similar to ContentPane.startup() // itself, but avoids marking the ContentPane itself as "restarted" (see #15581). // This starts all the widgets array.forEach(this.getChildren(), function(obj){ if(!obj._started && !obj._destroyed && lang.isFunction(obj.startup)){ obj.startup(); obj._started = true; } }); // And this catches stuff like dojo/dnd/Source if(this._contentSetter){ array.forEach(this._contentSetter.parseResults, function(obj){ if(!obj._started && !obj._destroyed && lang.isFunction(obj.startup)){ obj.startup(); obj._started = true; } }, this); } }, setHref: function(/*String|Uri*/ href){ // summary: // Deprecated. Use set('href', ...) instead. kernel.deprecated("dijit.layout.ContentPane.setHref() is deprecated. Use set('href', ...) instead.", "", "2.0"); return this.set("href", href); }, _setHrefAttr: function(/*String|Uri*/ href){ // summary: // Hook so set("href", ...) works. // description: // Reset the (external defined) content of this pane and replace with new url // Note: It delays the download until widget is shown if preload is false. // href: // url to the page you want to get, must be within the same domain as your mainpage // Cancel any in-flight requests (a set('href', ...) will cancel any in-flight set('href', ...)) this.cancel(); this.onLoadDeferred = new Deferred(lang.hitch(this, "cancel")); this.onLoadDeferred.then(lang.hitch(this, "onLoad")); this._set("href", href); // _setHrefAttr() is called during creation and by the user, after creation. // Assuming preload == false, only in the second case do we actually load the URL; // otherwise it's done in startup(), and only if this widget is shown. if(this.preload || (this._created && this._isShown())){ this._load(); }else{ // Set flag to indicate that href needs to be loaded the next time the // ContentPane is made visible this._hrefChanged = true; } return this.onLoadDeferred; // Deferred }, setContent: function(/*String|DomNode|Nodelist*/data){ // summary: // Deprecated. Use set('content', ...) instead. kernel.deprecated("dijit.layout.ContentPane.setContent() is deprecated. Use set('content', ...) instead.", "", "2.0"); this.set("content", data); }, _setContentAttr: function(/*String|DomNode|Nodelist*/data){ // summary: // Hook to make set("content", ...) work. // Replaces old content with data content, include style classes from old content // data: // the new Content may be String, DomNode or NodeList // // if data is a NodeList (or an array of nodes) nodes are copied // so you can import nodes from another document implicitly // clear href so we can't run refresh and clear content // refresh should only work if we downloaded the content this._set("href", ""); // Cancel any in-flight requests (a set('content', ...) will cancel any in-flight set('href', ...)) this.cancel(); // Even though user is just setting content directly, still need to define an onLoadDeferred // because the _onLoadHandler() handler is still getting called from setContent() this.onLoadDeferred = new Deferred(lang.hitch(this, "cancel")); if(this._created){ // For back-compat reasons, call onLoad() for set('content', ...) // calls but not for content specified in srcNodeRef (ie:
...
) // or as initialization parameter (ie: new ContentPane({content: ...}) this.onLoadDeferred.then(lang.hitch(this, "onLoad")); } this._setContent(data || ""); this._isDownloaded = false; // mark that content is from a set('content') not a set('href') return this.onLoadDeferred; // Deferred }, _getContentAttr: function(){ // summary: // Hook to make get("content") work return this.containerNode.innerHTML; }, cancel: function(){ // summary: // Cancels an in-flight download of content if(this._xhrDfd && (this._xhrDfd.fired == -1)){ this._xhrDfd.cancel(); } delete this._xhrDfd; // garbage collect this.onLoadDeferred = null; }, destroy: function(){ this.cancel(); this.inherited(arguments); }, destroyRecursive: function(/*Boolean*/ preserveDom){ // summary: // Destroy the ContentPane and its contents // if we have multiple controllers destroying us, bail after the first if(this._beingDestroyed){ return; } this.inherited(arguments); }, _onShow: function(){ // summary: // Called when the ContentPane is made visible // description: // For a plain ContentPane, this is called on initialization, from startup(). // If the ContentPane is a hidden pane of a TabContainer etc., then it's // called whenever the pane is made visible. // // Does necessary processing, including href download and layout/resize of // child widget(s) this.inherited(arguments); if(this.href){ if(!this._xhrDfd && // if there's an href that isn't already being loaded (!this.isLoaded || this._hrefChanged || this.refreshOnShow) ){ return this.refresh(); // If child has an href, promise that fires when the load is complete } } }, refresh: function(){ // summary: // [Re]download contents of href and display // description: // 1. cancels any currently in-flight requests // 2. posts "loading..." message // 3. sends XHR to download new data // Cancel possible prior in-flight request this.cancel(); this.onLoadDeferred = new Deferred(lang.hitch(this, "cancel")); this.onLoadDeferred.then(lang.hitch(this, "onLoad")); this._load(); return this.onLoadDeferred; // If child has an href, promise that fires when refresh is complete }, _load: function(){ // summary: // Load/reload the href specified in this.href // display loading message this._setContent(this.onDownloadStart(), true); var self = this; var getArgs = { preventCache: (this.preventCache || this.refreshOnShow), url: this.href, handleAs: "text" }; if(lang.isObject(this.ioArgs)){ lang.mixin(getArgs, this.ioArgs); } var hand = (this._xhrDfd = (this.ioMethod || xhr.get)(getArgs)), returnedHtml; hand.then( function(html){ returnedHtml = html; try{ self._isDownloaded = true; return self._setContent(html, false); }catch(err){ self._onError('Content', err); // onContentError } }, function(err){ if(!hand.canceled){ // show error message in the pane self._onError('Download', err); // onDownloadError } delete self._xhrDfd; return err; } ).then(function(){ self.onDownloadEnd(); delete self._xhrDfd; return returnedHtml; }); // Remove flag saying that a load is needed delete this._hrefChanged; }, _onLoadHandler: function(data){ // summary: // This is called whenever new content is being loaded this._set("isLoaded", true); try{ this.onLoadDeferred.resolve(data); }catch(e){ console.error('Error ' + this.widgetId + ' running custom onLoad code: ' + e.message); } }, _onUnloadHandler: function(){ // summary: // This is called whenever the content is being unloaded this._set("isLoaded", false); try{ this.onUnload(); }catch(e){ console.error('Error ' + this.widgetId + ' running custom onUnload code: ' + e.message); } }, destroyDescendants: function(/*Boolean*/ preserveDom){ // summary: // Destroy all the widgets inside the ContentPane and empty containerNode // Make sure we call onUnload (but only when the ContentPane has real content) if(this.isLoaded){ this._onUnloadHandler(); } // Even if this.isLoaded == false there might still be a "Loading..." message // to erase, so continue... // For historical reasons we need to delete all widgets under this.containerNode, // even ones that the user has created manually. var setter = this._contentSetter; array.forEach(this.getChildren(), function(widget){ if(widget.destroyRecursive){ // All widgets will hit this branch widget.destroyRecursive(preserveDom); }else if(widget.destroy){ // Things like dojo/dnd/Source have destroy(), not destroyRecursive() widget.destroy(preserveDom); } widget._destroyed = true; }); if(setter){ // Most of the widgets in setter.parseResults have already been destroyed, but // things like Menu that have been moved to haven't yet array.forEach(setter.parseResults, function(widget){ if(!widget._destroyed){ if(widget.destroyRecursive){ // All widgets will hit this branch widget.destroyRecursive(preserveDom); }else if(widget.destroy){ // Things like dojo/dnd/Source have destroy(), not destroyRecursive() widget.destroy(preserveDom); } widget._destroyed = true; } }); delete setter.parseResults; } // And then clear away all the DOM nodes if(!preserveDom){ domConstruct.empty(this.containerNode); } // Delete any state information we have about current contents delete this._singleChild; }, _setContent: function(/*String|DocumentFragment*/ cont, /*Boolean*/ isFakeContent){ // summary: // Insert the content into the container node // returns: // Returns a Deferred promise that is resolved when the content is parsed. // first get rid of child widgets this.destroyDescendants(); // html.set will take care of the rest of the details // we provide an override for the error handling to ensure the widget gets the errors // configure the setter instance with only the relevant widget instance properties // NOTE: unless we hook into attr, or provide property setters for each property, // we need to re-configure the ContentSetter with each use var setter = this._contentSetter; if(!(setter && setter instanceof html._ContentSetter)){ setter = this._contentSetter = new html._ContentSetter({ node: this.containerNode, _onError: lang.hitch(this, this._onError), onContentError: lang.hitch(this, function(e){ // fires if a domfault occurs when we are appending this.errorMessage // like for instance if domNode is a UL and we try append a DIV var errMess = this.onContentError(e); try{ this.containerNode.innerHTML = errMess; }catch(e){ console.error('Fatal ' + this.id + ' could not change content due to ' + e.message, e); } })/*, _onError */ }); } var setterParams = lang.mixin({ cleanContent: this.cleanContent, extractContent: this.extractContent, parseContent: !cont.domNode && this.parseOnLoad, parserScope: this.parserScope, startup: false, dir: this.dir, lang: this.lang, textDir: this.textDir }, this._contentSetterParams || {}); var p = setter.set((lang.isObject(cont) && cont.domNode) ? cont.domNode : cont, setterParams); // dojox/layout/html/_base::_ContentSetter.set() returns a Promise that indicates when everything is completed. // dojo/html::_ContentSetter.set() currently returns the DOMNode, but that will be changed for 2.0. // So, if set() returns a promise then use it, otherwise fallback to waiting on setter.parseDeferred var self = this; return when(p && p.then ? p : setter.parseDeferred, function(){ // setter params must be pulled afresh from the ContentPane each time delete self._contentSetterParams; if(!isFakeContent){ if(self._started){ // Startup each top level child widget (and they will start their children, recursively) self._startChildren(); // Call resize() on each of my child layout widgets, // or resize() on my single child layout widget... // either now (if I'm currently visible) or when I become visible self._scheduleLayout(); } self._onLoadHandler(cont); } }); }, _onError: function(type, err, consoleText){ this.onLoadDeferred.reject(err); // shows user the string that is returned by on[type]Error // override on[type]Error and return your own string to customize var errText = this['on' + type + 'Error'].call(this, err); if(consoleText){ console.error(consoleText, err); }else if(errText){// a empty string won't change current content this._setContent(errText, true); } }, // EVENT's, should be overide-able onLoad: function(/*===== data =====*/){ // summary: // Event hook, is called after everything is loaded and widgetified // tags: // callback }, onUnload: function(){ // summary: // Event hook, is called before old content is cleared // tags: // callback }, onDownloadStart: function(){ // summary: // Called before download starts. // description: // The string returned by this function will be the html // that tells the user we are loading something. // Override with your own function if you want to change text. // tags: // extension return this.loadingMessage; }, onContentError: function(/*Error*/ /*===== error =====*/){ // summary: // Called on DOM faults, require faults etc. in content. // // In order to display an error message in the pane, return // the error message from this method, as an HTML string. // // By default (if this method is not overriden), it returns // nothing, so the error message is just printed to the console. // tags: // extension }, onDownloadError: function(/*Error*/ /*===== error =====*/){ // summary: // Called when download error occurs. // // In order to display an error message in the pane, return // the error message from this method, as an HTML string. // // Default behavior (if this method is not overriden) is to display // the error message inside the pane. // tags: // extension return this.errorMessage; }, onDownloadEnd: function(){ // summary: // Called when download is finished. // tags: // callback } }); });