blob: 9870f0150506b55d4d7296fa2872af4a795826e9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview This manages rendering of gadgets in a place holder, within an
* HTML element in the container. The API for this is low-level. Use the
* container APIs to work with gadget sites.
*/
/**
* @param {osapi.container.Container} container The container that hosts this gadget site.
* @param {osapi.container.Service} service The container's service.
* @param {Object} args containing:
* {osapi.container.Service} service to fetch gadgets metadata, token.
* {string} navigateCallback name of callback function on navigateTo().
* {Element} gadgetEl Element into which to render the gadget.
* {Element} bufferEl Optional element for double buffering.
* @constructor
* @extends {osapi.container.Site}
*/
osapi.container.GadgetSite = function(container, service, args) {
var undef;
osapi.container.Site.call(this, container, service, args['gadgetEl']); // call super
/**
* @type {string}
* @private
*/
this.navigateCallback_ = args['navigateCallback'];
/**
* @type {Element}
* @private
*/
this.loadingGadgetEl_ = args['bufferEl'];
/**
* @type {string}
* @private
*/
this.gadgetOnLoad_ = args['gadgetOnLoad'];
/**
* Unique numeric module ID for this gadget instance. A module id is used to
* identify persisted instances of gadgets.
*
* @type {number}
* @private
*/
this.moduleId_ = 0;
/**
* Information about the currently visible gadget.
* @type {osapi.container.GadgetHolder?}
* @private
*/
this.currentGadgetHolder_ = undef;
/**
* Information about the currently loading gadget.
* @type {osapi.container.GadgetHolder?}
* @private
*/
this.loadingGadgetHolder_ = undef;
this.onConstructed();
};
osapi.container.GadgetSite.prototype = new osapi.container.Site;
/**
* @return {undefined|null|number} The numerical moduleId of this gadget, if
* set. May return null or undefined if not set.
*/
osapi.container.GadgetSite.prototype.getModuleId = function() {
return this.moduleId_;
};
/**
* If you want to change the moduleId after a gadget has rendered, re-navigate the site.
*
* @param {string} url This gadget's url (may not yet be accessible in all cases from the holder).
* @param {number} mid The numerical moduleId for this gadget to use.
* @param {function} opt_callback Optional callback to run when the moduleId is set.
* @private
*/
osapi.container.GadgetSite.prototype.setModuleId_ = function(url, mid, opt_callback) {
if (mid && this.moduleId_ != mid) {
var self = this,
url = osapi.container.util.buildTokenRequestUrl(url, mid);
if (!self.service_.getCachedGadgetToken(url)) {
// We need to request a security token for this gadget instance.
var request = osapi.container.util.newTokenRequest([url]);
self.service_.getGadgetToken(request, function(response) {
var ttl, mid;
if (response && response[url]) {
if (ttl = response[url][osapi.container.TokenResponse.TOKEN_TTL]) {
self.container_.scheduleRefreshTokens_(ttl);
}
var mid = response[url][osapi.container.TokenResponse.MODULE_ID];
if (mid || mid == 0) {
self.moduleId_ = mid;
}
}
if (opt_callback) {
opt_callback();
}
});
return;
}
}
if (opt_callback) {
opt_callback();
}
};
/**
* @inheritDoc
*/
osapi.container.GadgetSite.prototype.getActiveSiteHolder = function() {
return this.loadingGadgetHolder_ || this.currentGadgetHolder_;
};
/**
* @inheritDoc
*/
osapi.container.GadgetSite.prototype.setTitle = function(title) {
osapi.container.Site.prototype.setTitle.call(this, title);
// sometimes there are 2 holders
if (this.loadingGadgetHolder_ && this.currentGadgetHolder_) {
// loadingGadgetHolder_ was set by super call
this.currentGadgetHolder_.setTitle(title); // set my other one.
}
return this;
};
/**
* Returns configuration of a feature with a given name. Defaults to current
* loading or visible gadget if no metadata is passed in.
* @param {string} name Name of the feature.
* @param {Object=} opt_gadgetInfo Optional gadget info.
* @return {Object} JSON representing the feature.
*/
osapi.container.GadgetSite.prototype.getFeature = function(name, opt_gadgetInfo) {
var gadgetInfo = opt_gadgetInfo || this.getActiveSiteHolder().getGadgetInfo();
return gadgetInfo[osapi.container.MetadataResponse.FEATURES] &&
gadgetInfo[osapi.container.MetadataResponse.FEATURES][name];
};
/**
* Render a gadget in the site, by URI of the gadget XML.
* @param {string} gadgetUrl The absolute URL to gadget.
* @param {Object} viewParams Look at osapi.container.ViewParam.
* @param {Object} renderParams Look at osapi.container.RenderParam.
* @param {function(Object)=} opt_callback Function called with gadget info
* after navigation has occurred.
*/
osapi.container.GadgetSite.prototype.navigateTo = function(
gadgetUrl, viewParams, renderParams, opt_callback) {
var start = osapi.container.util.getCurrentTimeMs();
var cached = this.service_.getCachedGadgetMetadata(gadgetUrl);
var callback = opt_callback || function() {};
var request = osapi.container.util.newMetadataRequest([gadgetUrl]);
var self = this;
this.service_.getGadgetMetadata(request, function(response) {
var xrt = (!cached) ? (osapi.container.util.getCurrentTimeMs() - start) : 0;
var gadgetInfo = response[gadgetUrl];
if (gadgetInfo.error) {
var message = ['Failed to navigate for gadget ', gadgetUrl, '.'].join('');
gadgets.warn(message);
message = ['Detailed error: ', gadgetInfo.error.code || '', ' ', gadgetInfo.error.message || ''].join('');
gadgets.log(message);
callback(gadgetInfo);
} else {
var moduleId = renderParams[osapi.container.RenderParam.MODULE_ID] || 0;
self.setModuleId_(gadgetUrl, moduleId, function() {
self.container_.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_BEFORE_RENDER,
gadgetInfo);
self.render(gadgetInfo, viewParams, renderParams);
callback(gadgetInfo);
});
}
// Return metadata server response time.
var timingInfo = {};
timingInfo[osapi.container.NavigateTiming.URL] = gadgetUrl;
timingInfo[osapi.container.NavigateTiming.ID] = self.id_;
timingInfo[osapi.container.NavigateTiming.START] = start;
timingInfo[osapi.container.NavigateTiming.XRT] = xrt;
self.onNavigateTo(timingInfo);
});
};
/**
* Provide overridable callback invoked when navigateTo is completed.
* @param {Object} data the statistic/timing information to return.
*/
osapi.container.GadgetSite.prototype.onNavigateTo = function(data) {
if (this.navigateCallback_) {
var func = window[this.navigateCallback_];
if (typeof func === 'function') {
func(data);
}
}
};
/**
* Render a gadget in this site, using a JSON gadget description.
*
* Note: A view provided in either renderParams or viewParams is subject to aliasing if the gadget
* does not support the view specified.
*
* @param {Object} gadgetInfo the JSON gadget description.
* @param {Object} viewParams Look at osapi.container.ViewParam.
* @param {Object} renderParams Look at osapi.container.RenderParam.
* @override
*/
osapi.container.GadgetSite.prototype.render = function(
gadgetInfo, viewParams, renderParams) {
var curUrl = this.currentGadgetHolder_ ? this.currentGadgetHolder_.getUrl() : null;
var previousView = null;
if (curUrl == gadgetInfo['url']) {
previousView = this.currentGadgetHolder_.getView();
}
// Simple function to find a suitable alias
var findAliasInfo = function(viewConf) {
if (typeof viewConf !== 'undefined' && viewConf != null) {
var aliases = viewConf['aliases'] || [];
for (var i = 0; i < aliases.length; i++) {
if (gadgetInfo[osapi.container.MetadataResponse.VIEWS][aliases[i]]) {
return {'view':aliases[i],
'viewInfo':gadgetInfo[osapi.container.MetadataResponse.VIEWS][aliases[i]]};
}
}
}
return null;
};
// Find requested view.
var view = renderParams[osapi.container.RenderParam.VIEW] ||
viewParams[osapi.container.ViewParam.VIEW] ||
previousView;
var viewInfo = gadgetInfo[osapi.container.MetadataResponse.VIEWS][view];
if (view && !viewInfo) {
var aliasInfo = findAliasInfo(gadgets.config.get('views')[view]);
if (aliasInfo) {
view = aliasInfo['view'];
viewInfo = aliasInfo['viewInfo'];
}
}
// Allow default view if requested view is not found. No sense doing this if the view is already "default".
if (!viewInfo &&
renderParams[osapi.container.RenderParam.ALLOW_DEFAULT_VIEW] &&
view != osapi.container.GadgetSite.DEFAULT_VIEW_) {
view = osapi.container.GadgetSite.DEFAULT_VIEW_;
viewInfo = gadgetInfo[osapi.container.MetadataResponse.VIEWS][view];
if (!viewInfo) {
var aliasInfo = findAliasInfo(gadgets.config.get('views')[view]);
if (aliasInfo) {
view = aliasInfo['view'];
viewInfo = aliasInfo['viewInfo'];
}
}
}
// Check if view exists.
if (!viewInfo) {
gadgets.warn(['Unsupported view ', view, ' for gadget ', gadgetInfo['url'], '.'].join(''));
return;
}
// Set the loading gadget holder:
// 1. If the gadget site already has currentGadgetHolder_ set and no loading element passed,
// simply set the current gadget holder as the loading gadget holder.
// 2. Else, check if caller pass the loading gadget element. If it does then use it to create new
// instance of osapi.container.GadgetHolder as the loading gadget holder.
if (this.currentGadgetHolder_ && !this.loadingGadgetEl_) {
this.loadingGadgetHolder_ = this.currentGadgetHolder_;
this.currentGadgetHolder_ = null;
}
else {
// Check if we are passed the loading gadget element
var el = this.loadingGadgetEl_ || this.el_;
this.loadingGadgetHolder_ = new osapi.container.GadgetHolder(this, el, this.gadgetOnLoad_);
}
var localRenderParams = {};
for (var key in renderParams) {
localRenderParams[key] = renderParams[key];
}
localRenderParams[osapi.container.RenderParam.VIEW] = view;
localRenderParams[osapi.container.RenderParam.HEIGHT] =
renderParams[osapi.container.RenderParam.HEIGHT] ||
viewInfo[osapi.container.MetadataResponse.PREFERRED_HEIGHT] ||
gadgetInfo[osapi.container.MetadataResponse.MODULE_PREFS][osapi.container.MetadataResponse.HEIGHT] ||
String(osapi.container.GadgetSite.DEFAULT_HEIGHT_);
localRenderParams[osapi.container.RenderParam.WIDTH] =
renderParams[osapi.container.RenderParam.WIDTH] ||
viewInfo[osapi.container.MetadataResponse.PREFERRED_WIDTH] ||
gadgetInfo[osapi.container.MetadataResponse.MODULE_PREFS][osapi.container.MetadataResponse.WIDTH] ||
String(osapi.container.GadgetSite.DEFAULT_WIDTH_);
this.updateSecurityToken_(gadgetInfo, localRenderParams);
this.loadingGadgetHolder_.render(gadgetInfo, viewParams, localRenderParams);
};
/**
* Called when a gadget loads in the site. Uses double buffer, if present.
*/
osapi.container.GadgetSite.prototype.onRender = function() {
this.swapBuffers_();
// Only dispose the current holder set it to the loading holder if a loading holder exists.
// This protects this method from re-entrant code that can cause the current holder to be
// removed without a loading holder to take its place
if (this.loadingGadgetHolder_) {
if (this.currentGadgetHolder_) {
this.currentGadgetHolder_.dispose();
}
this.currentGadgetHolder_ = this.loadingGadgetHolder_;
this.loadingGadgetHolder_ = null;
}
};
/**
* Sends RPC call to the current/visible gadget.
* @param {string} serviceName RPC service name to call.
* @param {function(Object)} callback Function to call upon RPC completion.
* @param {...number} var_args payload to pass to the recipient.
*/
osapi.container.GadgetSite.prototype.rpcCall = function(
serviceName, callback, var_args) {
if (this.currentGadgetHolder_) {
gadgets.rpc.call(this.currentGadgetHolder_.getIframeId(),
serviceName, callback, var_args);
}
};
/**
* If token has been fetched at least once, set the token to the most recent
* one. Otherwise, leave it.
* @param {Object} gadgetInfo The gadgetInfo used to update security token.
* @param {Object} renderParams Look at osapi.container.RenderParam.
* @private
*/
osapi.container.GadgetSite.prototype.updateSecurityToken_ = function(gadgetInfo, renderParams) {
var url = osapi.container.util.buildTokenRequestUrl(gadgetInfo['url'], this.moduleId_),
tokenInfo = this.service_.getCachedGadgetToken(url);
if (tokenInfo) {
var token = tokenInfo[osapi.container.TokenResponse.TOKEN];
this.loadingGadgetHolder_.setSecurityToken(token);
}
};
/**
* @inheritDoc
*/
osapi.container.GadgetSite.prototype.close = function() {
if (this.loadingGadgetHolder_) {
this.loadingGadgetHolder_.dispose();
}
if (this.currentGadgetHolder_) {
this.currentGadgetHolder_.dispose();
}
};
/**
* Swap the double buffer elements, if there is a double buffer.
* @private
*/
osapi.container.GadgetSite.prototype.swapBuffers_ = function() {
// Only process double buffering if loading gadget exists
if (this.loadingGadgetEl_) {
this.loadingGadgetEl_.style.left = '';
this.loadingGadgetEl_.style.position = '';
this.el_.style.position = 'absolute';
this.el_.style.left = '-2000px';
// Swap references; cur_ will now again be what's visible
var oldCur = this.el_;
this.el_ = this.loadingGadgetEl_;
this.loadingGadgetEl_ = oldCur;
}
};
/**
* Key to identify the calling gadget site.
* @type {string}
*/
osapi.container.GadgetSite.RPC_ARG_KEY = 'gs';
/**
* Default height of gadget. Refer to --
* http://code.google.com/apis/gadgets/docs/legacy/reference.html.
* @type {number}
* @private
*/
osapi.container.GadgetSite.DEFAULT_HEIGHT_ = 200;
/**
* Default width of gadget. Refer to --
* http://code.google.com/apis/gadgets/docs/legacy/reference.html.
* @type {number}
* @private
*/
osapi.container.GadgetSite.DEFAULT_WIDTH_ = 320;
/**
* Default view of gadget.
* @type {string}
* @private
*/
osapi.container.GadgetSite.DEFAULT_VIEW_ = 'default';