Model: example.
namespace
This is the JavaScript Model for the example.
namespace.
// Shim to prevent Safari private browsing mode from breaking the entire page.
require('./vendor/localstorage_safari_private_shim.js');
var _ = require('lodash');
var RSVP = require('rsvp');
// Configuration
var configs = require('./configs.js');
var metrics = require('./metrics.js');
// SDK
var Controller = require('./Controller.js');
var User = require('./analytics/User.js');
// utils
var utils = require('./utils.js');
var Constants = require('./constants.js');
var http = require('./http.js');
// Analytics
var Analytics = require('./analytics/Analytics.js');
var CollectTracker = require('./analytics/CollectTracker.js');
var GaTracker = require('./analytics/GaTracker.js');
// set during init
var analytics, controller, defaultApiParams, globalBindingFile;
// set during render
var renderingDependencies;
// Default SDK params
var sdkParams = {
locale: 'en_US', // or 'zh_CN'
layout: 'vertical-list', // or 'carousel' for swipe
numDVs: 2,
queryParam: null,
searchbox: '#example-searchbox',
container: '#example-content',
searchCallback: null,
clickHandling: true,
bundle: 'default'
};
/**
* Passes a blob of data to all of the registered analytics trackers.
*
* @param {Object} data - The data to send to the trackers. Data format is tracker-specific and undocumented so good
* luck with that.
*
* @memberof example
*/
var track = function(data) {
return controller.track(data);
};
// 'Promisifies' require.ensure for our rendering dependencies (useful so that
// we can use promises all the way down for e.g. error handling.) The provided
// callback will get an object containing the dependencies.
var ensureRenderingDependencies = function() {
return new RSVP.Promise(function(resolve) {
if(__CODE_SPLITTING__){
require.ensure([
"react",
"react-dom",
'./rendering/CardsContainer.jsx'
], function(require) {
renderingDependencies = {
React: require('react'),
ReactDOM: require('react-dom'),
CardsContainer: require('./rendering/CardsContainer.jsx')
};
resolve(renderingDependencies);
});
} else {
renderingDependencies = {
React: require('react'),
ReactDOM: require('react-dom'),
CardsContainer: require('./rendering/CardsContainer.jsx')
};
resolve(renderingDependencies);
}
});
};
// Renders an HTML message into the given container.
var displayMessage = function(html, container) {
container = utils.selectElement(container || sdkParams.container);
if (!container)
utils.error("Could not display message " + html + " (no container element specified.)");
if (renderingDependencies)
renderingDependencies.ReactDOM.unmountComponentAtNode(container);
container.innerHTML = '<p class=\'Example-display-msg\'>' + html + '</p>';
};
/**
* Returns a Promise which resolves to an array of final deepview populated SearchResult instances.
* Populated deepviews can be found in SearchResult.staticStates[i].deepState.dv.content.
*
* @param {example.SearchResult[]} results - Array of SearchResult instances
* @param {string} bundle - Determines which format to use for each schema. One of 'default' or 'rightHalfImage'. Defaults to 'default'.
* @param {string} format - Determines which 'format' to render. Ex. 'mrec1', 'mrec2', 'info', 'wideImageWithGrid'. See {@link https://docs.google.com/a/example.com/spreadsheets/d/15Pr1qGkytP2Oya0gxcUGHfVw-1_iE_Vj8PRXY5ZG7Ik/edit?usp=sharing | Libary} for complete list.
*
* @memberof example
*/
var populateResults = function(results, bundle, format){
return globalBindingFile.then(function(gbf) {
return controller.populate(results, bundle || 'default', format, gbf);
});
};
// Returns a Promise which resolves to a final populated SearchResult
//
// {string} searchResult - SearchResult instance with deepviews.
// {string} format - Determines which 'format' to render. Ex. 'mrec1', 'mrec2', 'info', 'wideImageWithGrid'. See {@link https://docs.google.com/a/example.com/spreadsheets/d/15Pr1qGkytP2Oya0gxcUGHfVw-1_iE_Vj8PRXY5ZG7Ik/edit?usp=sharing | Libary} for complete list.
var populateOneResult = function(searchResult, format){
return globalBindingFile.then(function(gbf) {
return controller.populateSearchResult(searchResult, format, gbf);
});
};
/**
* Renders a React template with a page of search results into the provided container DOM element, one per search result.
* Returns a promise containing the rendered React container component.
*
* @param {example.SearchResult[]} populatedResults - The final array of Search Results with populated DeepViewDatas.
* @param {Object} options - Define specific option variables: container (DOMElement) and clickHandling (bool)
* @param {DOMElement} options.container - The DOM element (or DOM element selector) to render the into.
* @param {boolean} options.clickHandling - Boolean flag to enable/disable handling of click events on rendered Deepviews (CardBodies)
* @param {string} options.bundle - Determines which `format` to use per each `schema` returned in SERP experiences.
* See 'formatBundle' block in {@link https://github.com/example/trelleborg/blob/new_format/example/sdk/global/globalSchemaFormatBinding.json|global binding file} for more details.
* @param {string} options.format - Determines which 'format' to render. Ex. 'mrec1', 'mrec2', 'info', 'wideImageWithGrid'. See {@link https://docs.google.com/a/example.com/spreadsheets/d/15Pr1qGkytP2Oya0gxcUGHfVw-1_iE_Vj8PRXY5ZG7Ik/edit?usp=sharing | Libary} for complete list.
* @param {string} options.layout - Controls how results are laid out in the container. One of either 'vertical-list' or
* 'carousel'.
*
* @memberof example
*/
var renderSERP = function(resultData, options) {
options = options || {};
return ensureRenderingDependencies().then(function(deps) {
if(_.isUndefined(controller)) {
utils.error("Controller was not initialized. Did you call example.init first?");
return false;
}
metrics.recordStart("Rendering SERP");
var container = utils.selectElement(_.has(options, 'container') ? options.container : sdkParams.container);
var layout = _.has(options, 'layout') ? options.layout : sdkParams.layout;
var clickHandling = _.has(options, 'clickHandling') ? options.clickHandling : sdkParams.clickHandling;
if(!resultData.populatedResults || resultData.populatedResults.length === 0){
displayMessage("No results found.", container);
} else {
var rootComponent = deps.ReactDOM.render(deps.React.createElement(deps.CardsContainer, {
numDVs: sdkParams.numDVs,
analytics: analytics,
layout: layout,
isInteractive: clickHandling
}), container);
rootComponent.resetSearch(resultData.populatedResults);
}
metrics.recordFinish("Rendering SERP");
return {
rootComponent: rootComponent,
dynamicResults: resultData.dynamicResults
};
});
};
/**
* Shortcut for rendering a single ; identical to calling {@link example.renderSERP|renderSERP} with the "single-card" layout.
* Returns a promise containing the rendered React container component.
*
* @param {example.SearchResult} result - The search results to render into the container.
* @param {Object} options - Define specific option variables: container (DOMElement) and clickHandling (bool)
* @param {DOMElement} options.container - The DOM element (or DOM element selector) to render the into.
* @param {boolean} options.clickHandling - Boolean flag to enable/disable handling of click events on rendered Deepviews (CardBodies)
* @param {string} options.bundle - Determines which `format` to use per each `schema` returned in SERP experiences.
* See 'formatBundle' block in {@link https://github.com/example/trelleborg/blob/new_format/example/sdk/global/globalSchemaFormatBinding.json|global binding file} for more details.
* @param {string} options.format - Determines which 'format' to render. Ex. 'mrec1', 'mrec2', 'info', 'wideImageWithGrid'. See {@link https://docs.google.com/a/example.com/spreadsheets/d/15Pr1qGkytP2Oya0gxcUGHfVw-1_iE_Vj8PRXY5ZG7Ik/edit?usp=sharing | Libary} for complete list.
* @param {string} options.layout - Controls how results are laid out in the container. One of either 'vertical-list' or
* 'carousel'.
*
* @memberof example
*/
var renderCard = function(result, options) {
// 'format' should only be specified when rendering 1 card (ie. for ads). If none provided use the one set in sdkParams.
options = options || {};
options.format = options.format || undefined;
options.layout = "single-card";
return renderSERP({ populatedResults: [result] }, options);
};
// Attaches an event handler that will perform a search and render results whenever the form of the
// given search box is submitted.
var attachOnSubmit = function(searchbox) {
var searchboxElement = utils.selectElement(searchbox);
if (searchboxElement && searchboxElement.form){
searchboxElement.form.addEventListener("submit", function(e) {
e.preventDefault();
example.run(searchboxElement.value).then(sdkParams.searchCallback || _.noop);
});
}
};
/**
* Returns a Promise that resolves to a populated SearchResult (ready for rendering)
* or rejects and returns the furl passed in.
*
* @param {string} furl - Required. A single furl string for static or dynamic s.
* @param {string} format - One of our formats. 'info', 'place', etc... Defaults to 'info'.
* @param {Object} apiParams - Optional. Overrides the default api params when hitting /s.
* @param {integer} delay - Delay in ms between queries to v4/s. Defaults to 1000.
* @param {integer} maxTries - The max number of attempts to query v4/s. Defaults to 3.
*
* @memberof example
*/
var lookup = function(furl, format, apiParams, delay, maxTries){
if(typeof furl !== 'string' || !/^func:/.test(furl)){
utils.error('furl arg must be a valid furl string');
}
return new RSVP.Promise(function(resolve, reject) {
// When using _.defaults, leftmost arguments override all others.
return controller.fetchSearchResult(furl, _.defaults(apiParams || {}, defaultApiParams), delay, maxTries)
.then(function(searchResult){
return searchResult
? populateOneResult(searchResult, format || 'info').then(resolve)
: reject(furl);
});
});
};
/**
* Convenience function that looks up furls and then renders them in the container provided in example.init.
*
* @param {string} furl - Required. A single furl string for static or dynamic s.
* @param {string} format - One of our formats. 'info', 'place', etc... Defaults to 'info'.
* @param {Object} apiParams - Optional. Overrides the default api params when hitting /s.
*
* @memberof example
*/
var lookupAndRender = function(furl, format, apiParams) {
if(typeof furl !== 'string' || !/^func:/.test(furl)){
utils.error('furl arg must be a valid furl string');
}
if(!utils.selectElement(sdkParams.container)){
utils.error('Missing placeholder container ' + sdkParams.container);
return;
}
return lookup(furl, format, _.defaults(apiParams || {}, defaultApiParams)).then(renderCard);
};
/**
* Performs a search using the example API and returns a promised ordered list of {@link
* example.SearchResult|SearchResults} matching the query.
*
* @param {string} q - The query text to search for.
* @param {Object} apiParams - Other parameter overrides to send to the example API.
*
* @memberof example
*/
var search = function(q, apiParams) {
return controller.search(_.defaults({q: q}, apiParams || {}, defaultApiParams));
};
// Handles the lookup of dynamic furls by hitting v4/s for each furl,
// then replaces loading card with full content, or removes the card from SERP.
// #lookup will hit v4/s once every 1000 ms up to 3 times. This can be configured in configs.js.
//
// {Object} postRenderData - All the info needed to render dynamic s.
// {example.SearchResult[]} postRenderData.dynamicResults - Search Results missing content (ie. dynamic s)
// {ReactComponent} postRenderData.rootComponent - The root React Component (usually CardsContainer.jsx) which manages rendering.
var handleDynamics = function(postRenderData){
var rootComponent = postRenderData.rootComponent;
if(postRenderData.dynamicResults && postRenderData.dynamicResults.length){
postRenderData.dynamicResults.forEach(function(pr){
lookup(pr.furl, pr.format, null, Constants.S_DELAY_MS, Constants.S_MAX_TRIES)
.then(rootComponent.updateResult, rootComponent.removeResult);
});
}
};
/**
* Convenience method for performing a search and rendering. Uses the preconfigured SDK settings to perform a search
* and render the results into the default container. On any error, displays a message in the default container.
*
* @param {string} q - The query text to search for.
*
* @memberof example
*/
var run = function(q) {
var params = _.defaults({q: q}, defaultApiParams);
return controller.search(params)
.then(populateResults)
.then(renderSERP)
.then(handleDynamics)
.catch(function(err) {
if (err === 'Network timeout') {
displayMessage("No network connection available.", sdkParams.container);
} else {
utils.error(err);
}
throw err;
});
};
// Returns a list of Tracker objects which should record analytics tracking events
// as per the settings object passed in by the client.
var initTrackers = function(trackerSettings, credentials) {
var trackers = [new CollectTracker(credentials)];
if(trackerSettings){
if(trackerSettings.ga){ // Google Analytics
trackers.push(new GaTracker(trackerSettings.ga[0], trackerSettings.ga[1]) );
}
}
return trackers;
};
// Reads any existing user tracking information and returns an Analytics object
// ready to record tracking events for the current user.
var initAnalytics = function(trackers, environment, existingUser) {
var currentUser = existingUser || new User(utils.guid());
var analyticsInterface = new Analytics(trackers, environment, currentUser, defaultApiParams);
if (!existingUser) { // save the new user and send a First Time User tracking event recording their existence
currentUser.save();
analyticsInterface.record('firsttimeuser');
}
return analyticsInterface;
};
/**
* Initializes the SDK with some information which will be applicable to every subsequent search.
* Generic options:
*
* - `locale`: The name of the locale web-sdk is intended for. One of "en_US" (US) or "zh_CN" (China).
* Defaults to "en_US".
*
* - `env`: The name of an environment hosting the example API and related resources like bindings. One of "prod",
* "canary", or "stage." Defaults to prod.
*
* - `credentials`: An object `{ partnerId: 'your-id', partnerSecret: 'your-secret' }` containing your example
* credentials. These will be used when communicating with the example API.
*
* - `internalTrackers`: An {id: config} mapping containing configuration for built-in analytics trackers. See the
* individual tracker documentation for more details.
*
* Options that control default integration behavior:
*
* - `layout`: Specifies the default layout for rendered search cards. Either "vertical-list" or "carousel".
*
* - `searchbox`: A DOM element or selector pointing to a form element that can contain a query. When the element's
* parent form is submitted, the value of the form element will be used as the search query sent to the example API.
*
* - `queryParam`: The name of a URL parameter that may be passed to the hosting page. When init is called, if
* this parameter is present, a search will run immediately with the value of this parameter as the query.
*
* - `container`: A DOM element or selector specifying the default container for search results. This container will
* be used when rendering results after e.g. {@link example.run|run} run is called.
*
* - `searchCallback`: A callback that will fire whenever a search has successfully been performed automatically (either
* on load or on form submit.)
*
* @param {Object} params - An object containing initialization parameters as described above.
*
* @function init
* @memberof example
*/
var init = function(initParams) {
ensureRenderingDependencies(); // this eager-loads the templates chunk
if (!initParams.credentials || !initParams.credentials.partnerId) {
utils.print('No partner credentials provided.');
return;
}
if(initParams.locale){
if(_.has(configs.environments, initParams.locale)){
sdkParams.locale = initParams.locale;
} else {
utils.print("Invalid locale in .init \'" + initParams.locale + "\'. Defaulting to \'" + sdkParams.locale + "\'.");
}
}
var environment = configs.environments[sdkParams.locale][initParams.env || "prod"];
if (!environment) {
utils.print("Invalid environment: " + initParams.env);
return;
}
// Default api params
defaultApiParams = {
geoloc: utils.storeNewGeoloc(),
user_agent: navigator.userAgent,
platform_ids: utils.getPlatformIds(),
limit: 10,
dv_limit: 3,
custom_id: 75675980,
auto_filter: 1,
filter_empty_results: 0,
fix_spelling: 0,
include_descriptions: 0,
include_screenshots: 0,
include_editions: 1,
include_authors: 1,
include_snippets: 0,
display_config: { platform: 'all' },
version: process.build ? process.build.version : null
};
var trackers = initTrackers(initParams.internalTrackers, initParams.credentials);
analytics = initAnalytics(trackers, environment, User.load());
controller = new Controller(analytics, environment, initParams.credentials, sdkParams.locale);
metrics.recordStart("Fetching global binding file");
globalBindingFile = http.get(environment.cdnHost + "/" + Constants.GLOBAL_BINDING_FILE_PATH).then(function(gbf) {
metrics.recordFinish("Fetching global binding file");
return gbf;
});
sdkParams.bundle = initParams.bundle || sdkParams.bundle;
sdkParams.container = initParams.container || sdkParams.container;
sdkParams.searchbox = initParams.searchbox || sdkParams.searchbox;
sdkParams.queryParam = initParams.queryParam || sdkParams.queryParam;
sdkParams.searchCallback = initParams.searchCallback || sdkParams.searchCallback;
sdkParams.layout = initParams.layout || sdkParams.layout;
sdkParams.clickHandling = _.has(initParams, 'clickHandling') ? initParams.clickHandling : sdkParams.clickHandling;
if (sdkParams.queryParam) {
var query = utils.parseQS(location.search)[sdkParams.queryParam];
if (query) {
example.run(query).then(sdkParams.searchCallback || _.noop);
} else {
utils.print('"' + sdkParams.queryParam + '"' + ' was not found in the URL query string.');
}
}
if (sdkParams.searchbox){
attachOnSubmit(sdkParams.searchbox);
}
};
/**
* The namespace containing all SDK functionality. To use, call {@link example.init|init} followed by e.g. {@link example.run|run}.
* @namespace example
*/
module.exports = {
init: init,
run: run,
track: track,
search: search,
lookup: lookup,
lookupAndRender: lookupAndRender,
renderCard: renderCard,
renderSERP: renderSERP,
populateResults: populateResults,
DeepView: require('./models/DeepView.js'),
DeepViewData: require('./models/DeepViewData.js'),
SearchResult: require('./models/SearchResult.js'),
EditionMetadata: require('./models/EditionMetadata.js'),
metrics: metrics,
build: process.build || {}
};
// Shim to prevent Safari private browsing mode from breaking the entire page.
require('./vendor/localstorage_safari_private_shim.js');
var _ = require('lodash');
var RSVP = require('rsvp');
// Configuration
var configs = require('./configs.js');
var metrics = require('./metrics.js');
// SDK
var Controller = require('./Controller.js');
var User = require('./analytics/User.js');
// utils
var utils = require('./utils.js');
var Constants = require('./constants.js');
var http = require('./http.js');
// Analytics
var Analytics = require('./analytics/Analytics.js');
var CollectTracker = require('./analytics/CollectTracker.js');
var GaTracker = require('./analytics/GaTracker.js');
// set during init
var analytics, controller, defaultApiParams, globalBindingFile;
// set during render
var renderingDependencies;
// Default SDK params
var sdkParams = {
locale: 'en_US', // or 'zh_CN'
layout: 'vertical-list', // or 'carousel' for swipe
numDVs: 2,
queryParam: null,
searchbox: '#example-searchbox',
container: '#example-content',
searchCallback: null,
clickHandling: true,
bundle: 'default'
};
/**
* Passes a blob of data to all of the registered analytics trackers.
*
* @param {Object} data - The data to send to the trackers. Data format is tracker-specific and undocumented so good
* luck with that.
*
* @memberof example
*/
var track = function(data) {
return controller.track(data);
};
// 'Promisifies' require.ensure for our rendering dependencies (useful so that
// we can use promises all the way down for e.g. error handling.) The provided
// callback will get an object containing the dependencies.
var ensureRenderingDependencies = function() {
return new RSVP.Promise(function(resolve) {
if(__CODE_SPLITTING__){
require.ensure([
"react",
"react-dom",
'./rendering/CardsContainer.jsx'
], function(require) {
renderingDependencies = {
React: require('react'),
ReactDOM: require('react-dom'),
CardsContainer: require('./rendering/CardsContainer.jsx')
};
resolve(renderingDependencies);
});
} else {
renderingDependencies = {
React: require('react'),
ReactDOM: require('react-dom'),
CardsContainer: require('./rendering/CardsContainer.jsx')
};
resolve(renderingDependencies);
}
});
};
// Renders an HTML message into the given container.
var displayMessage = function(html, container) {
container = utils.selectElement(container || sdkParams.container);
if (!container)
utils.error("Could not display message " + html + " (no container element specified.)");
if (renderingDependencies)
renderingDependencies.ReactDOM.unmountComponentAtNode(container);
container.innerHTML = '<p class=\'qxy-display-msg\'>' + html + '</p>';
};
/**
* Returns a Promise which resolves to an array of final deepview populated SearchResult instances.
* Populated deepviews can be found in SearchResult.staticStates[i].deepState.dv.content.
*
* @param {example.SearchResult[]} results - Array of SearchResult instances
* @param {string} bundle - Determines which format to use for each schema. One of 'default' or 'rightHalfImage'. Defaults to 'default'.
* @param {string} format - Determines which 'format' to render. Ex. 'mrec1', 'mrec2', 'info', 'wideImageWithGrid'. See {@link https://docs.google.com/a/example.com/spreadsheets/d/15Pr1qGkytP2Oya0gxcUGHfVw-1_iE_Vj8PRXY5ZG7Ik/edit?usp=sharing | Libary} for complete list.
*
* @memberof example
*/
var populateResults = function(results, bundle, format){
return globalBindingFile.then(function(gbf) {
return controller.populate(results, bundle || 'default', format, gbf);
});
};
// Returns a Promise which resolves to a final populated SearchResult
//
// {string} searchResult - SearchResult instance with deepviews.
// {string} format - Determines which 'format' to render. Ex. 'mrec1', 'mrec2', 'info', 'wideImageWithGrid'. See {@link https://docs.google.com/a/example.com/spreadsheets/d/15Pr1qGkytP2Oya0gxcUGHfVw-1_iE_Vj8PRXY5ZG7Ik/edit?usp=sharing | Libary} for complete list.
var populateOneResult = function(searchResult, format){
return globalBindingFile.then(function(gbf) {
return controller.populateSearchResult(searchResult, format, gbf);
});
};
/**
* Renders a React template with a page of search results into the provided container DOM element, one per search result.
* Returns a promise containing the rendered React container component.
*
* @param {example.SearchResult[]} populatedResults - The final array of Search Results with populated DeepViewDatas.
* @param {Object} options - Define specific option variables: container (DOMElement) and clickHandling (bool)
* @param {DOMElement} options.container - The DOM element (or DOM element selector) to render the into.
* @param {boolean} options.clickHandling - Boolean flag to enable/disable handling of click events on rendered Deepviews (CardBodies)
* @param {string} options.bundle - Determines which `format` to use per each `schema` returned in SERP experiences.
*
* @param {string} options.format - Determines which 'format' to render. Ex. 'mrec1', 'mrec2', 'info', 'wideImageWithGrid'.
* @param {string} options.layout - Controls how results are laid out in the container. One of either 'vertical-list' or
* 'carousel'.
*
* @memberof example
*/
var renderSERP = function(resultData, options) {
options = options || {};
return ensureRenderingDependencies().then(function(deps) {
if(_.isUndefined(controller)) {
utils.error("Controller was not initialized. Did you call example.init first?");
return false;
}
metrics.recordStart("Rendering SERP");
var container = utils.selectElement(_.has(options, 'container') ? options.container : sdkParams.container);
var layout = _.has(options, 'layout') ? options.layout : sdkParams.layout;
var clickHandling = _.has(options, 'clickHandling') ? options.clickHandling : sdkParams.clickHandling;
if(!resultData.populatedResults || resultData.populatedResults.length === 0){
displayMessage("No results found.", container);
} else {
var rootComponent = deps.ReactDOM.render(deps.React.createElement(deps.CardsContainer, {
numDVs: sdkParams.numDVs,
analytics: analytics,
layout: layout,
isInteractive: clickHandling
}), container);
rootComponent.resetSearch(resultData.populatedResults);
}
metrics.recordFinish("Rendering SERP");
return {
rootComponent: rootComponent,
dynamicResults: resultData.dynamicResults
};
});
};
/**
* Shortcut for rendering a single ; identical to calling {@link example.renderSERP|renderSERP} with the "single-card" layout.
* Returns a promise containing the rendered React container component.
*
* @param {example.SearchResult} result - The search results to render into the container.
* @param {Object} options - Define specific option variables: container (DOMElement) and clickHandling (bool)
* @param {DOMElement} options.container - The DOM element (or DOM element selector) to render the card into.
* @param {boolean} options.clickHandling - Boolean flag to enable/disable handling of click events on rendered Deepviews (CardBodies)
* @param {string} options.bundle - Determines which `format` to use per each `schema` returned in SERP experiences.
* See 'formatBundle' block in {@link https://github.com/example/trelleborg/blob/new_format/example/sdk/global/globalSchemaFormatBinding.json|global binding file} for more details.
* @param {string} options.format - Determines which 'format' to render. Ex. 'mrec1', 'mrec2', 'info', 'wideImageWithGrid'. See {@link https://docs.google.com/a/example.com/spreadsheets/d/15Pr1qGkytP2Oya0gxcUGHfVw-1_iE_Vj8PRXY5ZG7Ik/edit?usp=sharing | Libary} for complete list.
* @param {string} options.layout - Controls how results are laid out in the container. One of either 'vertical-list' or
* 'carousel'.
*
* @memberof example
*/
var renderCard = function(result, options) {
// 'format' should only be specified when rendering 1 card (ie. for ads). If none provided use the one set in sdkParams.
options = options || {};
options.format = options.format || undefined;
options.layout = "single-card";
return renderSERP({ populatedResults: [result] }, options);
};
// Attaches an event handler that will perform a search and render results whenever the form of the
// given search box is submitted.
var attachOnSubmit = function(searchbox) {
var searchboxElement = utils.selectElement(searchbox);
if (searchboxElement && searchboxElement.form){
searchboxElement.form.addEventListener("submit", function(e) {
e.preventDefault();
example.run(searchboxElement.value).then(sdkParams.searchCallback || _.noop);
});
}
};
/**
* Returns a Promise that resolves to a populated SearchResult (ready for rendering)
* or rejects and returns the furl passed in.
*
* @param {string} furl - Required. A single furl string for static or dynamic s.
* @param {string} format - One of our formats. 'info', 'place', etc... Defaults to 'info'.
* @param {Object} apiParams - Optional. Overrides the default api params when hitting /s.
* @param {integer} delay - Delay in ms between queries to v4/s. Defaults to 1000.
* @param {integer} maxTries - The max number of attempts to query v4/s. Defaults to 3.
*
* @memberof example
*/
var lookup = function(furl, format, apiParams, delay, maxTries){
if(typeof furl !== 'string' || !/^func:/.test(furl)){
utils.error('furl arg must be a valid furl string');
}
return new RSVP.Promise(function(resolve, reject) {
// When using _.defaults, leftmost arguments override all others.
return controller.fetchSearchResult(furl, _.defaults(apiParams || {}, defaultApiParams), delay, maxTries)
.then(function(searchResult){
return searchResult
? populateOneResult(searchResult, format || 'info').then(resolve)
: reject(furl);
});
});
};
/**
* Convenience function that looks up furls and then renders them in the container provided in example.init.
*
* @param {string} furl - Required. A single furl string for static or dynamic s.
* @param {string} format - One of our formats. 'info', 'place', etc... Defaults to 'info'.
* @param {Object} apiParams - Optional. Overrides the default api params when hitting /s.
*
* @memberof example
*/
var lookupAndRender = function(furl, format, apiParams) {
if(typeof furl !== 'string' || !/^func:/.test(furl)){
utils.error('furl arg must be a valid furl string');
}
if(!utils.selectElement(sdkParams.container)){
utils.error('Missing placeholder container ' + sdkParams.container);
return;
}
return lookup(furl, format, _.defaults(apiParams || {}, defaultApiParams)).then(renderCard);
};
/**
* Performs a search using the example API and returns a promised ordered list of {@link
* example.SearchResult|SearchResults} matching the query.
*
* @param {string} q - The query text to search for.
* @param {Object} apiParams - Other parameter overrides to send to the example API.
*
* @memberof example
*/
var search = function(q, apiParams) {
return controller.search(_.defaults({q: q}, apiParams || {}, defaultApiParams));
};
// Handles the lookup of dynamic furls by hitting v4/s for each furl,
// then replaces loading card with full content, or removes the card from SERP.
// #lookup will hit v4/s once every 1000 ms up to 3 times. This can be configured in configs.js.
//
// {Object} postRenderData - All the info needed to render dynamic s.
// {example.SearchResult[]} postRenderData.dynamicResults - Search Results missing content (ie. dynamic s)
// {ReactComponent} postRenderData.rootComponent - The root React Component (usually CardsContainer.jsx) which manages rendering.
var handleDynamics = function(postRenderData){
var rootComponent = postRenderData.rootComponent;
if(postRenderData.dynamicResults && postRenderData.dynamicResults.length){
postRenderData.dynamicResults.forEach(function(pr){
lookup(pr.furl, pr.format, null, Constants.S_DELAY_MS, Constants.S_MAX_TRIES)
.then(rootComponent.updateResult, rootComponent.removeResult);
});
}
};
/**
* Convenience method for performing a search and rendering. Uses the preconfigured SDK settings to perform a search
* and render the results into the default container. On any error, displays a message in the default container.
*
* @param {string} q - The query text to search for.
*
* @memberof example
*/
var run = function(q) {
var params = _.defaults({q: q}, defaultApiParams);
return controller.search(params)
.then(populateResults)
.then(renderSERP)
.then(handleDynamics)
.catch(function(err) {
if (err === 'Network timeout') {
displayMessage("No network connection available.", sdkParams.container);
} else {
utils.error(err);
}
throw err;
});
};
// Returns a list of Tracker objects which should record analytics tracking events
// as per the settings object passed in by the client.
var initTrackers = function(trackerSettings, credentials) {
var trackers = [new CollectTracker(credentials)];
if(trackerSettings){
if(trackerSettings.ga){ // Google Analytics
trackers.push(new GaTracker(trackerSettings.ga[0], trackerSettings.ga[1]) );
}
}
return trackers;
};
// Reads any existing user tracking information and returns an Analytics object
// ready to record tracking events for the current user.
var initAnalytics = function(trackers, environment, existingUser) {
var currentUser = existingUser || new User(utils.guid());
var analyticsInterface = new Analytics(trackers, environment, currentUser, defaultApiParams);
if (!existingUser) { // save the new user and send a First Time User tracking event recording their existence
currentUser.save();
analyticsInterface.record('firsttimeuser');
}
return analyticsInterface;
};
/**
* Initializes the SDK with some information which will be applicable to every subsequent search.
* Generic options:
*
* - `locale`: The name of the locale web-sdk is intended for. One of "en_US" (US) or "zh_CN" (China).
* Defaults to "en_US".
*
* - `env`: The name of an environment hosting the example API and related resources like bindings. One of "prod",
* "canary", or "stage." Defaults to prod.
*
* - `credentials`: An object `{ partnerId: 'your-id', partnerSecret: 'your-secret' }` containing your example
* credentials. These will be used when communicating with the example API.
*
* - `internalTrackers`: An {id: config} mapping containing configuration for built-in analytics trackers. See the
* individual tracker documentation for more details.
*
* Options that control default integration behavior:
*
* - `layout`: Specifies the default layout for rendered search cards. Either "vertical-list" or "carousel".
*
* - `searchbox`: A DOM element or selector pointing to a form element that can contain a query. When the element's
* parent form is submitted, the value of the form element will be used as the search query sent to the example API.
*
* - `queryParam`: The name of a URL parameter that may be passed to the hosting page. When init is called, if
* this parameter is present, a search will run immediately with the value of this parameter as the query.
*
* - `container`: A DOM element or selector specifying the default container for search results. This container will
* be used when rendering results after e.g. {@link example.run|run} run is called.
*
* - `searchCallback`: A callback that will fire whenever a search has successfully been performed automatically (either
* on load or on form submit.)
*
* @param {Object} params - An object containing initialization parameters as described above.
*
* @function init
* @memberof example
*/
var init = function(initParams) {
ensureRenderingDependencies(); // this eager-loads the templates chunk
if (!initParams.credentials || !initParams.credentials.partnerId) {
utils.print('No partner credentials provided.');
return;
}
if(initParams.locale){
if(_.has(configs.environments, initParams.locale)){
sdkParams.locale = initParams.locale;
} else {
utils.print("Invalid locale in .init \'" + initParams.locale + "\'. Defaulting to \'" + sdkParams.locale + "\'.");
}
}
var environment = configs.environments[sdkParams.locale][initParams.env || "prod"];
if (!environment) {
utils.print("Invalid environment: " + initParams.env);
return;
}
// Default api params
defaultApiParams = {
geoloc: utils.storeNewGeoloc(),
user_agent: navigator.userAgent,
platform_ids: utils.getPlatformIds(),
limit: 10,
dv_limit: 3,
custom_id: 75675980,
auto_filter: 1,
filter_empty_results: 0,
fix_spelling: 0,
include_descriptions: 0,
include_screenshots: 0,
include_editions: 1,
include_authors: 1,
include_snippets: 0,
display_config: { platform: 'all' },
version: process.build ? process.build.version : null
};
var trackers = initTrackers(initParams.internalTrackers, initParams.credentials);
analytics = initAnalytics(trackers, environment, User.load());
controller = new Controller(analytics, environment, initParams.credentials, sdkParams.locale);
metrics.recordStart("Fetching global binding file");
globalBindingFile = http.get(environment.cdnHost + "/" + Constants.GLOBAL_BINDING_FILE_PATH).then(function(gbf) {
metrics.recordFinish("Fetching global binding file");
return gbf;
});
sdkParams.bundle = initParams.bundle || sdkParams.bundle;
sdkParams.container = initParams.container || sdkParams.container;
sdkParams.searchbox = initParams.searchbox || sdkParams.searchbox;
sdkParams.queryParam = initParams.queryParam || sdkParams.queryParam;
sdkParams.searchCallback = initParams.searchCallback || sdkParams.searchCallback;
sdkParams.layout = initParams.layout || sdkParams.layout;
sdkParams.clickHandling = _.has(initParams, 'clickHandling') ? initParams.clickHandling : sdkParams.clickHandling;
if (sdkParams.queryParam) {
var query = utils.parseQS(location.search)[sdkParams.queryParam];
if (query) {
example.run(query).then(sdkParams.searchCallback || _.noop);
} else {
utils.print('"' + sdkParams.queryParam + '"' + ' was not found in the URL query string.');
}
}
if (sdkParams.searchbox){
attachOnSubmit(sdkParams.searchbox);
}
};
/**
* The namespace containing all SDK functionality. To use, call {@link example.init|init} followed by e.g. {@link example.run|run}.
* @namespace example
*/
module.exports = {
init: init,
run: run,
track: track,
search: search,
lookup: lookup,
lookupAndRender: lookupAndRender,
renderCard: renderCard,
renderSERP: renderSERP,
populateResults: populateResults,
DeepView: require('./models/DeepView.js'),
DeepViewData: require('./models/DeepViewData.js'),
SearchResult: require('./models/SearchResult.js'),
EditionMetadata: require('./models/EditionMetadata.js'),
metrics: metrics,
build: process.build || {}
};