const base = require("./base");
require("./observable");
const ripe = base.ripe;
/**
* The version of the RIPE SDK currently in load, should
* be in sync with the package information.
*/
ripe.VERSION = "3.3.3";
/**
* Object that contains global (static) information to be used by
* the RIPE infrastructure (eg global identifier counter).
*/
ripe.ripeGlobals = {
id: 0
};
/**
* @class
* @augments Observable
* @classdesc Represents a customizable model.
*
* @param {String} brand The brand of the model.
* @param {String} model The name of the model.
* @param {Object} options An object with the options to configure the Ripe instance.
*/
ripe.Ripe = function(brand, model, options = {}) {
ripe.Observable.call(this);
ripe.Ripe.prototype.init.call(this, brand, model, options);
};
ripe.Ripe.prototype = ripe.build(ripe.Observable.prototype);
ripe.Ripe.prototype.constructor = ripe.Ripe;
/**
* @ignore
*/
ripe.RipeBase = function(brand, model, options = {}) {
return new ripe.Ripe(brand, model, options);
};
/**
* Builds a new Ripe instance using the provided normalized configuration
* structure as the basis for its configuration.
*
* @param {Object} structure The normalized configuration structure that is
* going to be used in the initialization of the new Ripe instance.
* @param {Boolean} safe If the safe object mode creation should be used.
* @param {Object} options The extra options to be used in the initialization
* process of the Ripe instance.
*/
ripe.Ripe.fromStructure = async function(structure, safe = true, options = {}) {
const instance = new ripe.Ripe(options);
await instance.setStructure(structure, safe);
return instance;
};
/**
* The initializer of the class, to be called whenever the instance
* is going to become active.
*
* Sets the various values for the Ripe instance taking into account
* the provided configuration and defaulting values policy.
*/
ripe.Ripe.prototype.init = async function(brand = null, model = null, options = {}) {
// generates a new global identifier and adds the current
// instance to the list og globally managed ones
ripe.ripeGlobals.id++;
this.id = ripe.ripeGlobals.id;
// runs the defaulting operation so that it's possible to
// provide only the first parameters as the options
if (typeof brand === "object" && brand !== null) {
options = brand;
brand = options.brand || null;
model = options.model || null;
}
// determines if the init operation should be avoided (eg: for static usage)
// if so the control flow is returned immediately (init prevented)
const init = options.init === undefined ? true : options.init;
if (!init) return;
// sets the various values in the instance taking into
// account the default values
this.initials = "";
this.engraving = null;
this.initialsExtra = {};
this.initialsBuilder = this._initialsBuilder.bind(this);
this.ctx = {};
this.children = this.children || [];
this.plugins = this.plugins || [];
this.history = [];
this.historyPointer = -1;
this.loadedConfig = null;
this.choices = null;
this.ready = false;
this.configured = false;
this.bundles = false;
this.partCounter = 0;
this.updateCounter = 0;
this.initialsCounter = 0;
this.updatePromise = null;
this.cancelPromise = null;
this.error = null;
// extends the default options with the ones provided by the
// developer upon this initializer call
options = ripe.assign(
{
options: false
},
options
);
this.setOptions(options);
// in case the guess URL mode is active then a remote call should be
// performed in order to take decisions on the proper production URL
if (this.guessUrl) await this._guessURL();
// iterates over all the plugins present in the options (meant
// to be registered) and adds them to the current instance
for (const plugin of options.plugins || []) {
this.addPlugin(plugin);
}
// if diagnostic headers have not been disabled then
// registers the diag plugin to automatically add
// diagnostic headers to every remote request
if (this.useDiag) {
const diagPlugin = new ripe.Ripe.plugins.DiagPlugin();
this.addPlugin(diagPlugin);
}
// registers for the config (finished) event so that the execution may
// be able to notify the server side logic and change the current state
// if that's required by the server side
this.bind("config", async function() {
let result = null;
if (!this.remoteOnConfig) return;
const ctxRequest = (this.ctxRequest = (this.ctxRequest || 0) + 1);
try {
result = await this.onConfigP({
brand: this.brand,
model: this.model
});
} catch (err) {
if (err instanceof ripe.RemoteError) return;
else throw err;
}
if (ctxRequest !== this.ctxRequest) return;
this._handleCtx(result);
});
// registers for the part (set) operation so that the execution may
// be able to notify the server side logic and change the current state
// if that's required by the server side
this.bind("part", async function(name, value) {
let result = null;
if (!this.remoteOnPart) return;
const ctxRequest = (this.ctxRequest = (this.ctxRequest || 0) + 1);
try {
result = await this.onPartP({
name: name,
value: value
});
} catch (err) {
if (err instanceof ripe.RemoteError) return;
else throw err;
}
if (ctxRequest !== this.ctxRequest) return;
this._handleCtx(result);
});
// registers for the initials (set) operation so that the execution may
// be able to notify the server side logic and change the current state
// if that's required by the server side
this.bind("initials", async function(initials, engraving, params) {
let result = null;
if (!this.remoteOnInitials) return;
if (params.noRemote) return;
const ctxRequest = (this.ctxRequest = (this.ctxRequest || 0) + 1);
try {
result = await this.onInitialsP({
group: "main",
value: initials,
engraving: engraving
});
} catch (err) {
if (err instanceof ripe.RemoteError) return;
else throw err;
}
if (ctxRequest !== this.ctxRequest) return;
this._handleCtx(result);
});
// registers for the initials_extra (set) operation so that the execution may
// be able to notify the server side logic and change the current state
// if that's required by the server side
this.bind("initials_extra", async function(initialsExtra, params, { previous }) {
let result = null;
if (!this.remoteOnInitials) return;
if (params.noRemote) return;
const ctxRequest = (this.ctxRequest = (this.ctxRequest || 0) + 1);
for (const [key, value] of Object.entries(initialsExtra)) {
// in case the value of the initials extra group did not
// change when compared with the previous version then
// there's no need for `onInitials` execution
const previousValue =
previous && previous[key] ? previous[key] : { initials: "", engraving: null };
if (
value.initials === previousValue.initials &&
value.engraving === previousValue.engraving
) {
continue;
}
try {
result = await this.onInitialsP({
group: key,
value: value.initials,
engraving: value.engraving
});
} catch (err) {
if (err instanceof ripe.RemoteError) return;
else throw err;
}
if (ctxRequest !== this.ctxRequest) return;
this._handleCtx(result);
}
});
// listens for the post parts event and saves the current configuration
// for the undo operations (history control)
this.bind("post_parts", function(parts, options) {
// in case the current operation was an undo and redo one there's
// nothing to be done (no history stack change)
if (options && ["undo", "redo"].indexOf(options.action) !== -1) {
return;
}
// pushes the current state of the configuration (parts) into
// the history stack allowing undo and redo
this._pushHistory();
});
try {
// runs the configuration operation on the current instance, using
// the requested parameters and options, multiple configuration
// operations may be executed over the object life-time
await this.config(brand, model, options);
} catch (error) {
// calls the error handler for the current handler to update the
// internal items of the Ripe Instance
this._errorHandler(error);
// returns the control flow immediately as the exception has been
// properly handled for the current context
return;
}
// runs the initialization of the locale bundles, provided by the
// remote handle, this is required for proper initialization
if (this.useBundles) this._initBundles().catch(err => this._errorHandler(err));
};
/**
* The deinitializer to be called when it should stop responding
* to updates so that any necessary cleanup operations can be executed.
*/
ripe.Ripe.prototype.deinit = async function() {
let index = null;
for (index = this.children.length - 1; index >= 0; index--) {
const child = this.children[index];
await this.unbindInteractable(child);
}
for (index = this.plugins.length - 1; index >= 0; index--) {
const plugin = this.plugins[index];
this.removePlugin(plugin);
}
ripe.Observable.prototype.deinit.call(this);
};
/**
* Explicit entry point to the initial update.
*
* This method should be called before any significant RIPE operation
* can be performed on the instance.
*
* @returns {Object} The current Ripe Instance (for pipelining).
*/
ripe.Ripe.prototype.load = function() {
this.update(undefined, { reason: "load" });
return this;
};
/**
* Explicit entry point for the unloading of the Ripe Instance.
*
* Should be called for a clean exit of the instance.
*
* @returns {Object} The current Ripe Instance (for pipelining).
*/
ripe.Ripe.prototype.unload = function() {
return this;
};
/**
* Same as `load` but providing a promise oriented solution
* ready to be "awaited".
*
* @returns {Object} The current Ripe Instance (for pipelining).
*/
ripe.Ripe.prototype.loadP = async function() {
await this.update(undefined, { reason: "load" });
return this;
};
/**
* Same as `unload` but providing a promise oriented solution
* ready to be "awaited".
*
* @returns {Object} The current Ripe Instance (for pipelining).
*/
ripe.Ripe.prototype.unloadP = async function() {
return this;
};
/**
* Sets the model to be customised by providing both the brand
* and the model for the update.
*
* @param {String} brand The brand of the model.
* @param {String} model The name of the model.
* @param {Object} options An object with the options to configure the Ripe instance, such as:
* - 'parts' - The initial parts of the model.
* - 'initials' - The initial value for the initials of the model.
* - 'engraving' - The initial engraving value of the model.
* - 'initialsExtra' - The initial value for the initials extra.
* - 'country' - The country where the model will be sold.
* - 'currency' - The currency that should be used to calculate the price.
* - 'locale' - The locale to be used by default when localizing values.
* - 'flag' - A specific attribute of the model.
* - 'remoteCalls' - If the remote calls (eg: 'on_config') should be called in the middle of configuration.
* - 'useBundles' - If the bundles should be loaded during initial loading.
* - 'useDefaults' - If the default parts of the model should be used when no initials parts are set.
* - 'useCombinations' - If the combinations should be loaded as part of the initial RIPE loading.
* - 'usePrice' - If the price should be automatically retrieved whenever there is a customization change.
* - 'useDiag' - If the diagnostics module should be used.
* - 'safe' - If the call should 'await' for all the composing operations before returning or if instead
* should allow operations to be performed in a parallel and detached manner.
*/
ripe.Ripe.prototype.config = async function(brand, model, options = {}) {
// unsets the configured flag so that all the sensitive
// configuration related operation are disabled while
// the config operation is being performed, this is
// required because there's a lot of parallelism in the
// execution workflow of the config and by setting this
// flag some data race conditions are avoided
this.configured = false;
// cancels any pending operation on the child elements
// so that no more operations are performed, any new
// operation could ony be considered a wat of resources
await this.cancel();
// sets the most structural values of this entity
// that represent the configuration to be used
this.brand = brand;
this.model = model;
// resets the history related values as the current
// model has changed and no previous history is possible
this.history = [];
this.historyPointer = -1;
// sets the new options using the current options
// as default values and sets the update flag to
// true if it is not set
options = ripe.assign(
{},
this.options,
{
variant: null,
version: null,
dku: null,
parts: {}
},
options
);
this.setOptions(options);
// in case there's a DKU defined for the current config then
// an extra resolution step must occur, to be able to obtain
// the configuration of the current customization
if (this.dku) {
const config = await this.configDkuP(this.dku);
this.brand = config.brand;
this.model = config.model;
this.parts = config.parts === undefined ? this.parts : config.parts;
this.initials = config.initials === undefined ? this.initials : config.initials;
this.engraving = config.engraving === undefined ? this.engraving : config.engraving;
this.initialsExtra =
config.initials_extra === undefined && config.initialsExtra === undefined
? this.initialsExtra
: config.initialsExtra || config.initials_extra;
}
// determines if a valid model is currently defined for the ripe
// instance, as this is going to change some logic behaviour
const hasModel = Boolean(this.brand && this.model);
// in case no model is currently loaded it's time to return the
// control flow as all of the structures are currently loaded
if (hasModel === false) {
this.loadedConfig = null;
if (this.ready === false) {
this.ready = true;
this.trigger("ready");
}
return;
}
// triggers the 'pre_config' event so that the listeners
// can cleanup if needed, from the previous configuration
await this.trigger("pre_config", brand, model, options);
try {
// retrieves the configuration for the currently loaded model so
// that others may use it freely (cache mechanism)
this.loadedConfig = await this.getConfigP();
} catch (err) {
// builds the base message taking into consideration if the
// version has been explicity defined
let message = `Not possible to get configuration for '${brand}' and '${model}'`;
if (this.options.version) message += ` version ${this.options.version}`;
// raises a new error indicating the real cause for the new
// error being thrown under the current execution logic
throw new ripe.OperationalError(message, err);
}
// creates a "new" choices from the provided configuration for the
// model that has just been "loaded" and sets it as the new set of
// choices for the configuration context
this.setChoices(this._toChoices(this.loadedConfig));
// determines if the defaults for the selected model should
// be loaded so that the parts structure is initially populated
const hasParts = this.parts && Object.keys(this.parts).length !== 0;
const loadDefaults = !hasParts && this.useDefaults && hasModel;
// determines the proper initial parts for the model taking into account
// if the defaults should be loaded
const parts = loadDefaults ? this.loadedConfig.defaults : this.parts;
// creates an array that is going to store the multiple promises that
// are required for the proper loading of the configuration, both promises
// can run in parallel as their loading is independent from each other
const parallelPromises = [];
// loads initials builder implementation by evaluating the build's logic,
// which can contain methods that might override the default behavior
if (this.useInitialsBuilderLogic) parallelPromises.push(this._loadInitialsBuilder());
// updates the parts of the current instance so that the internals of it
// reflect the newly loaded configuration, notice that we're not going to
// wait until the update is finished (opportunistic)
parallelPromises.push(
this.setParts(parts, true, {
partEvents: false,
runUpdate: false
})
);
await Promise.all(parallelPromises);
// in case both the initials and the engraving value are set in the options
// runs the updating of the internal state to update the initials
if (options.initials && options.engraving) {
const setInitialsPromise = this.setInitials(options.initials, options.engraving, false);
if (options.safe) await setInitialsPromise;
}
// in case the initials extra are defined then runs the setting of the initials
// extra on the current instance (without update events)
if (options.initialsExtra) {
const setInitialsExtraPromise = this.setInitialsExtra(options.initialsExtra, false);
if (options.safe) await setInitialsExtraPromise;
}
// triggers the config event notifying any listener that the (base)
// configuration for this main Ripe Instance has changed and waits
// for the listeners to conclude their operations
await this.trigger("config", this.loadedConfig, options);
// determines if the ready flag is already set for the current instance
// and if that's not the case updates it and triggers the ready event
if (this.ready === false) {
this.ready = true;
this.trigger("ready");
}
// notifies that the config has changed and waits for listeners before
// concluding the config operation
await this.trigger("post_config", this.loadedConfig, options);
// sets the configured flag as valid, meaning that any configuration
// related operation is considered safe from now on
this.configured = true;
// triggers the remote operations, that should be executed
// only after the complete set of post confirm promises are met
const remotePromise = this.remote();
if (options.safe) await remotePromise;
// runs the initial update operation, so that all the visuals and children
// objects are properly updated according to the new configuration
const updatePromise = this.update(undefined, {
noAwaitLayout: true,
reason: "config"
});
if (options.safe) await updatePromise;
};
/**
* @ignore
*/
ripe.Ripe.prototype.remote = async function() {
// makes sure that both the brand and the model values are defined
// for the current instance as they are needed for the remove operation
// that are going to be performed
if (!this.brand || !this.model) {
return;
}
// tries to determine if the combinations available should be
// loaded for the current model and if that's the case start the
// loading process for them, setting then the result in the instance
const loadCombinations = this.useCombinations;
if (loadCombinations) {
this.combinations = await this.getCombinationsP();
this.trigger("combinations", this.combinations);
}
};
/**
* Retrieves the normalized structure that uniquely represents
* the current configuration "situation".
*
* @param {Boolean} safe If the structure should be retrieved
* using a safe approach (deep copy).
* @returns The normalized map structure that represents the
* current configuration "situation".
*/
ripe.Ripe.prototype.getStructure = async function(safe = true) {
const structure = {};
if (this.brand) structure.brand = this.brand;
if (this.model) structure.model = this.model;
if (this.variant) structure.variant = this.variant;
if (this.version) structure.version = this.version;
if (this.parts && Object.keys(this.parts).length > 0) {
structure.parts = this.parts;
}
if (this.initials) structure.initials = this.initials;
if (this.engraving) structure.engraving = this.engraving;
if (this.initialsExtra && Object.keys(this.initialsExtra).length > 0) {
structure.initials_extra = this.initialsExtra;
}
return safe ? JSON.parse(JSON.stringify(structure)) : structure;
};
/**
* Updates the current internal state of the Ripe instance with the
* contents defined by the provided structure (snapshot).
*
* @param {Object} structure The object structure that represents the configuration
* "situation" that is going to be set in the Ripe instance.
* @param {Boolean} safe If the operation should be performed using a
* safe strategy (deep copy in objects).
*/
ripe.Ripe.prototype.setStructure = async function(structure, safe = true) {
const options = {};
const brand = structure.brand || null;
const model = structure.model || null;
options.variant = structure.variant || null;
options.version = structure.version || null;
options.parts =
(structure.parts &&
(safe ? JSON.parse(JSON.stringify(structure.parts)) : structure.parts)) ||
{};
options.initials = structure.initials || "";
options.engraving = structure.engraving || null;
options.initialsExtra =
(structure.initials_extra &&
Object.keys(structure.initials_extra).length > 0 &&
(safe
? JSON.parse(JSON.stringify(structure.initials_extra))
: structure.initials_Extra)) ||
{};
await this.config(brand, model, options);
};
/**
* Sets Ripe instance options according to the defaulting policy.
*
* @param {Object} options An object with the options to configure the Ripe instance, such as:
* - 'variant' - The variant of the model.
* - 'version' - The version of the model, obtained from the containing build.
* - 'dku' - The DKU (Dynamic Keeping Unit) to be used in the configuration (if any).
* - 'parts' - The initial parts of the model.
* - 'country' - The country where the model will be sold.
* - 'currency' - The currency that should be used to calculate the price.
* - 'locale' - The locale to be used by default when localizing values.
* - 'flag' - A specific attribute of the model.
* - 'format' - The format of the image that is going to be retrieved in case of image visual and interactive.
* - 'size' - The default size in pixels to be used by children for composition.
* - 'backgroundColor' - The background color in RGB format to be used for images.
* - 'guess' - If the optimistic guess mode should be used for config resolution (internal).
* - 'guessUrl' - If base production URL should be guessed using GeoIP information.
* - 'remoteCalls' - If the remote calls (eg: 'on_config') should be called in the middle of configuration.
* - 'useBundles' - If the bundles should be loaded during initial loading.
* - 'useDefaults' - If the default parts of the model should be used when no initials parts are set.
* - 'useCombinations' - If the combinations should be loaded as part of the initial RIPE loading.
* - 'usePrice' - If the price should be automatically retrieved whenever there is a customization change.
* - 'useDiag' - If the diagnostics module should be used.
*/
ripe.Ripe.prototype.setOptions = function(options = {}) {
this.options = options;
this.variant = this.options.variant || null;
this.version = this.options.version || null;
this.dku = this.options.dku || null;
this.url = this.options.url || "https://sandbox.platforme.com/api/";
this.webUrl = this.options.webUrl || "https://sandbox.platforme.com/";
this.composeUrl = this.options.composeUrl || `${this.url}compose`;
this.params = this.options.params || {};
this.headers = this.options.headers || {};
this.parts = this.options.parts || {};
this.country = this.options.country || null;
this.currency = this.options.currency || null;
this.locale = this.options.locale || null;
this.flag = this.options.flag || null;
this.format = this.options.format || null;
this.formatBase = this.options.format || null;
this.size = this.options.size || null;
this.backgroundColor = this.options.backgroundColor || "";
this.guess = this.options.guess === undefined ? undefined : this.options.guess;
this.guessUrl = this.options.guessUrl === undefined ? undefined : this.options.guessUrl;
this.remoteCalls = this.options.remoteCalls === undefined ? true : this.options.remoteCalls;
this.remoteOnConfig =
this.options.remoteOnConfig === undefined ? this.remoteCalls : this.options.remoteOnConfig;
this.remoteOnPart =
this.options.remoteOnPart === undefined ? this.remoteCalls : this.options.remoteOnPart;
this.remoteOnInitials =
this.options.remoteOnInitials === undefined
? this.remoteCalls
: this.options.remoteOnInitials;
this.noBundles = this.options.noBundles === undefined ? false : this.options.noBundles;
this.useBundles =
this.options.useBundles === undefined ? !this.noBundles : this.options.useBundles;
this.noDefaults = this.options.noDefaults === undefined ? false : this.options.noDefaults;
this.useDefaults =
this.options.useDefaults === undefined ? !this.noDefaults : this.options.useDefaults;
this.noCombinations =
this.options.noCombinations === undefined ? false : this.options.noCombinations;
this.useCombinations =
this.options.useCombinations === undefined
? !this.noCombinations
: this.options.useCombinations;
this.noPrice = this.options.noPrice === undefined ? false : this.options.noPrice;
this.usePrice = this.options.usePrice === undefined ? !this.noPrice : this.options.usePrice;
this.noDiag = this.options.noDiag === undefined ? false : this.options.noDiag;
this.useDiag = this.options.useDiag === undefined ? !this.noDiag : this.options.useDiag;
this.noAwaitLayout =
this.options.noAwaitLayout === undefined ? false : this.options.noAwaitLayout;
this.useInitialsBuilderLogic =
this.options.useInitialsBuilderLogic === undefined
? true
: this.options.useInitialsBuilderLogic;
this.composeLogic = this.options.composeLogic === undefined ? false : this.options.composeLogic;
this.authCallback = this.options.authCallback === undefined ? null : this.options.authCallback;
// in case the requested format is the "dynamic" lossless one
// tries to find the best lossless image format taking into account
// the current browser environment
if (this.format === "lossless") {
this.format = this._supportsWebp() ? "webp" : "png";
}
// in case the lossful meta-format is defined defines the best possible
// lossful format taking into account the environment
if (this.format === "lossful") {
this.format = "jpeg";
}
// runs the background color normalization process that removes
// the typical cardinal character from the definition
this.backgroundColor = this.backgroundColor.replace("#", "");
};
/**
* Changes the material and color of the provided part.
*
* This operations is an expensive one and should be used carefully
* to avoid unwanted resource usage.
*
* If many operations are meant to be used at the same time the `setParts`
* parts method should be used instead, as it is better suited for bulk
* based operations.
*
* @param {String} part The name of the part to be changed.
* @param {String} material The material to change to.
* @param {String} color The color to change to.
* @param {Boolean} events If the parts events should be triggered (defaults to 'true').
* @param {Object} options The options to be used in the set part operations (for internal use).
*/
ripe.Ripe.prototype.setPart = async function(part, material, color, events = true, options = {}) {
const runUpdate = options.runUpdate === undefined ? true : options.runUpdate;
const waitUpdate = options.waitUpdate === undefined ? true : options.waitUpdate;
if (!events) {
await this._setPart(part, material, color);
}
await this.trigger("pre_parts", this.parts, options);
await this._setPart(part, material, color);
await this.trigger("parts", this.parts, options);
await this.trigger("post_parts", this.parts, options);
// in case the update is not meant to be ran then returns the
// control flow immediately (nothing remaining to be done)
if (!runUpdate) return;
// propagates the state change in the internal structures to the
// children elements of this Ripe Instance
const promise = this.update(undefined, { reason: "set part" });
// in case the wait update options is valid (by default) then waits
// until the update promise is fulfilled
if (waitUpdate) await promise;
};
/**
* Allows changing the customization of a set of parts in bulk.
*
* @param {Object} parts An Object or array with part, material, color triplets to be set.
* @param {Boolean} events If the parts events should be triggered (defaults to 'true').
* @param {Object} options An object with options to configure the operation (for internal use).
*/
ripe.Ripe.prototype.setParts = async function(update, events = true, options = {}) {
const partEvents = options.partEvents === undefined ? true : options.partEvents;
const runUpdate = options.runUpdate === undefined ? true : options.runUpdate;
const waitUpdate = options.waitUpdate === undefined ? true : options.waitUpdate;
if (typeof update === "object" && !Array.isArray(update)) {
update = this._partsList(update);
}
if (!events) {
await this._setParts(update, partEvents);
return;
}
await this.trigger("pre_parts", this.parts, options);
await this._setParts(update, partEvents);
await this.trigger("parts", this.parts, options);
await this.trigger("post_parts", this.parts, options);
// in case the update is not meant to be ran then returns the
// control flow immediately (nothing remaining to be done)
if (!runUpdate) return;
// propagates the state change in the internal structures to the
// children elements of this Ripe Instance
const promise = this.update(undefined, { reason: "set parts" });
// in case the wait update options is valid (by default) then waits
// until the update promise is fulfilled
if (waitUpdate) await promise;
};
/**
* Changes the initials of the model, this is considered a simple
* legacy oriented strategy as the `setInitialsExtra` method should
* be used for more complex scenarios with multiple groups.
*
* @param {String} initials The initials value to be set.
* @param {String} engraving The type of engraving to be set.
* @param {Boolean} events If the events associated with the initials
* change should be triggered.
* @param {Boolean} override If the options value should be override meaning
* that further config updates will have this new initials set.
* @param {Object} params Extra parameters that control the behaviour of
* the set initials operation.
*/
ripe.Ripe.prototype.setInitials = async function(
initials,
engraving,
events = true,
override = false,
params = {}
) {
if (typeof initials === "object") {
events = engraving === undefined ? true : engraving;
const result = await this.setInitialsExtra(initials, events);
return result;
}
// generates a new initials counter that controls if the initials
// state has changes (set initials), this way it's possible to
// prevent out of order execution of update states
this.initialsCounter += 1;
const id = this.initialsCounter;
// triggers the event indicating the the start of the
// the (set) initials operation (notifies listeners)
await this.trigger("pre_initials", { id: id });
// sets the base instance fields for both the initials and the
// engraving and updates the initials extra on the main group,
// providing a compatibility layer between the initials and the
// initials extra mode of working
this.initials = initials || "";
this.engraving = engraving || null;
this.initialsExtra = {
main: {
initials: initials || "",
engraving: engraving || null
}
};
if (!this.initials && this.engraving) {
throw new Error("Engraving set without initials");
}
// in case the override flag is set then the options fields
// are also update with the new initials values
if (override) {
this.options.initials = this.initials;
this.options.engraving = this.engraving;
this.options.initialsExtra = this.initialsExtra;
}
// in case the events should not be triggered then returns
// the control flow immediately, nothing remaining to be done
if (!events) return this;
// creates a "snapshot" of the current initials state so that the
// update may be performed over the currently defined set of initials
const state = this._getState();
// triggers the initials event notifying any listening
// object about the changes
await this.trigger("initials", initials, engraving, params, { id: id });
// runs the update operation so that all the listening
// components can properly update their visuals, notice
// that this execution is only performed in case this is
// still the most up-to-date initials operation, avoiding
// possible out-of-order execution of update operations
if (id === this.initialsCounter) {
this.update(state, { reason: "set initials" });
}
// triggers the event indicating the the end of the
// the (set) initials operation (notifies listeners)
await this.trigger("post_initials", { id: id });
// returns the current instance (good for pipelining)
return this;
};
/**
* Changes the initials of the model using an object as the input which
* allows setting the initials for multiple groups at the same time.
*
* @param {Object} initialsExtra Object that contains the values of the
* initials and engraving for all the initial groups.
* @param {Boolean} events If the events associated with the changing of
* the initials (extra) should be triggered.
* @param {Boolean} override If the options value should be override meaning
* that further config updates will have this new initials extra set.
* @param {Object} params Extra parameters that control the behaviour of
* the set initials operation.
*/
ripe.Ripe.prototype.setInitialsExtra = async function(
initialsExtra,
events = true,
override = false,
params = {}
) {
const groups = Object.keys(initialsExtra);
const isEmpty = groups.length === 0;
const mainGroup = groups.includes("main") ? "main" : groups[0];
const mainInitials = initialsExtra[mainGroup];
// generates a new initials counter that controls if the initials
// state has changes (set initials), this way it's possible to
// prevent out of order execution of update states
this.initialsCounter += 1;
const id = this.initialsCounter;
// triggers the event indicating the the start of the
// the (set) initials extra operation (notifies listeners)
await this.trigger("pre_initials_extra", { id: id });
// "saves" the value of the previous initials extra structure
// so that it can be passed latter as part of the event context
const previous = Object.assign({}, this.initialsExtra);
if (isEmpty) {
this.initials = "";
this.engraving = null;
this.initialsExtra = {};
} else {
this.initials = mainInitials.initials || "";
this.engraving = mainInitials.engraving || null;
this.initialsExtra = initialsExtra;
}
for (const [key, value] of Object.entries(this.initialsExtra)) {
if (value.initials && !value.engraving) {
value.engraving = null;
}
if (!value.initials && value.engraving) {
throw new Error(`Engraving set without initials for group ${key}`);
}
}
// in case the override flag is set then the options fields
// are also update with the new initials values
if (override) {
this.options.initials = this.initials;
this.options.engraving = this.engraving;
this.options.initialsExtra = this.initialsExtra;
}
if (!events) return this;
// creates a "snapshot" of the current initials state so that the
// update may be performed over the currently defined set of initials
const state = this._getState();
// triggers the initials extra event notifying any
// listening object about the changes
await this.trigger("initials_extra", initialsExtra, params, { id: id, previous: previous });
// runs the update operation so that all the listening
// components can properly update their visuals, notice
// that this execution is only performed in case this is
// still the most up-to-date initials operation, avoiding
// possible out-of-order execution of update operations
if (id === this.initialsCounter) {
this.update(state, { reason: "set initials extra" });
}
// triggers the event indicating the the end of the
// the (set) initials extra operation (notifies listeners)
await this.trigger("post_initials_extra", { id: id });
// returns the current instance (good for pipelining)
return this;
};
/**
* Retrieves the value of the current base context defined in
* the instance.
*
* @returns {Object} The base context currently set.
*/
ripe.Ripe.prototype.getCtx = function(ctx) {
return this.ctx;
};
/**
* Changes the current base context object (ctx) that is
* going to be sent for (3D) build logic on crucial workflow
* state changes.
*
* @param {Object} ctx The new base context to be used.
*/
ripe.Ripe.prototype.setCtx = function(ctx) {
this.ctx = ctx;
};
/**
* Returns the model's configuration loaded from the Platforme's system.
* The config version loaded by this method is the one "cached" in the
* instance, if there's any.
*
* @returns {Object} The model's configuration.
*/
ripe.Ripe.prototype.getLoadedConfig = function() {
return this.loadedConfig;
};
/**
* Returns the current state (eg: availability) for the parts materials
* and colors associated with the current customization session.
*
* @returns {Object} The object that contains the state for every single
* part, material, and color.
*/
ripe.Ripe.prototype.getChoices = function() {
return this.choices;
};
/**
* Updates the current internal state for parts material and colors, properly
* notifying any "listener" about these changes.
*
* @param {Object} choices The object that contains the state for every single
* part, material, and color.
* @param {Boolean} events If the choices events should be triggered (defaults
* to 'true').
*/
ripe.Ripe.prototype.setChoices = function(choices, events = true) {
// updates the internal object with the choices that are now
// going to be set
this.choices = choices;
// in case no event triggering is required no the control flow
// must return immediately
if (!events) return;
// triggers the choices event that should change the available
// set of choices in the visual/UI assets
this.trigger("choices", this.choices);
};
/**
* Returns the model's available frames, in an object structure
* that maps a certain face with the number of available frames
* for such face.
*
* This function makes use of the loaded config in case there's
* one, otherwise triggers the loading of the config.
*
* @returns {Object} The model's available frames.
*/
ripe.Ripe.prototype.getFrames = async function(callback) {
if (this.options.frames) {
if (callback) callback(this.options.frames);
return this.options.frames;
}
const config = this.loadedConfig ? this.loadedConfig : await this.getConfigP();
const frames = {};
for (let index = 0; index < config.faces.length; index++) {
const face = config.faces[index];
frames[face] = 1;
}
// ensures that the "legacy" side face has the a value
// populated with the "legacy" frames field in case there's
// none populated by the standard processing loop (above)
// this only happens in case the side face is defined
if (config.faces.indexOf("side") !== -1) {
frames.side = config.frames;
}
// iterates over the complete set of faces to populate the frames
// structure with the most up-to-date strategy using the faces map
// that contains all the information about each face
for (const [face, faceM] of Object.entries(config.faces_m)) {
frames[face] = faceM.frames;
}
// calls the callback with the resolved frame (unsafe) and returns
// the frames map to the caller method
if (callback) callback(frames);
return frames;
};
/**
* Updates the format setting for the current ripe instance, propagating
* the change to any interested child.
*
* Optionally an update operation may be performed so that the format
* changes are reflected in the user interface.
*
* @param {String} format The image format to be used in the ripe instance
* (eg: png, webp, jpeg).
* @param {Boolean} override If the options value should be override meaning
* that further config updates will have this new format set.
* @param {Boolean} update If an update operation should be perform asynchronous.
*/
ripe.Ripe.prototype.setFormat = async function(format, override = true, update = true) {
if (format === this.options.format) return;
this.format = format;
this.getChildren("Configurator").forEach(c => {
c.format = format;
});
if (override) this.options.format = format;
if (update) this.update(undefined, { reason: "set format" });
this.trigger("settings");
return this;
};
/**
* Updates the size setting for the current ripe instance, propagating
* the change to any interested child.
*
* Optionally an update operation may be performed so that the size
* changes are reflected in the user interface.
*
* @param {String} size The size (in pixels) of the image to be used.
* @param {Boolean} override If the options value should be override meaning
* that further config updates will have this new format set.
* @param {Boolean} update If an update operation should be perform asynchronous.
*/
ripe.Ripe.prototype.setSize = async function(size, override = true, update = true) {
if (size === this.options.size) return;
this.size = size;
this.getChildren("Configurator").forEach(c => {
c.size = size;
});
if (override) this.options.size = size;
if (update) this.update(undefined, { reason: "set size" });
this.trigger("settings");
return this;
};
/**
* Updates the background color setting for the current ripe instance,
* propagating the change to any interested child.
*
* Optionally an update operation may be performed so that the background
* background color changes are reflected in the user interface.
*
* @param {String} backgroundColor The background color in hexadecimal to be set.
* @param {Boolean} override If the options value should be override meaning
* that further config updates will have this new format set.
* @param {Boolean} update If an update operation should be perform asynchronous.
*/
ripe.Ripe.prototype.setBackgroundColor = async function(
backgroundColor,
override = true,
update = true
) {
if (backgroundColor) backgroundColor = backgroundColor.replace("#", "");
if (backgroundColor === this.options.backgroundColor) return;
this.backgroundColor = backgroundColor;
this.getChildren("Configurator").forEach(c => {
c.backgroundColor = backgroundColor;
});
if (override) this.options.backgroundColor = backgroundColor;
if (update) this.update(undefined, { reason: "set background color" });
this.trigger("settings");
return this;
};
/**
* Retrieves the complete set of child elements of this Ripe instance
* that fulfill the provided type criteria.
*
* @param {String} type The type of child as a string to filter children.
* @return {Array} The child elements that fill the provided type.
*/
ripe.Ripe.prototype.getChildren = function(type = null) {
if (type === null) return this.children;
return this.children.filter(child => type === null || child.type.startsWith(type));
};
/**
* Binds an Image to this Ripe instance.
*
* @param {Image} element The Image to be used by the Ripe instance.
* @param {Object} options An Object with options to configure the Image instance.
* @returns {Image} The Image instance created.
*/
ripe.Ripe.prototype.bindImage = function(element, options = {}) {
options = Object.assign({}, { format: this.format }, options);
const image = new ripe.Image(this, element, options);
return this.bindInteractable(image);
};
/**
* Binds a video thumbnail Image to this Ripe instance.
*
* @param {Image} element The Image to be used by the Ripe instance.
* @param {Object} options An Object with options to configure the Image instance.
* @returns {Image} The Image instance created.
*/
ripe.Ripe.prototype.bindVideoThumbnail = function(element, options = {}) {
options = Object.assign(
{},
{
imageUrlProvider: this._getVideoThumbnailURL.bind(this),
frameValidator: this.hasVideo.bind(this),
doubleBuffering: false
},
options
);
const image = new ripe.Image(this, element, options);
return this.bindInteractable(image);
};
/**
* Binds an Configurator to this Ripe instance.
*
* @param {Configurator} element The Configurator to be used by the Ripe instance.
* @param {Object} options An Object with options to configure the Configurator instance.
* @returns {Configurator} The Configurator instance created.
*/
ripe.Ripe.prototype.bindConfigurator = function(element, options = {}) {
options = Object.assign({}, { format: this.format }, options);
switch (options.type) {
case "csr":
return this.bindConfiguratorCsr(element, options);
case "prc":
default:
return this.bindConfiguratorPrc(element, options);
}
};
/**
* Binds an Interactable to this Ripe instance.
*
* @param {Interactable} element The Interactable to be used by the Ripe instance.
* @param {Object} options An Object with options to configure the Interactable instance.
* @returns {Interactable} The Interactable instance created.
*/
ripe.Ripe.prototype.bindInteractable = function(element) {
this.children.push(element);
this.trigger("bound", element);
return element;
};
/**
* Unbinds ab Interactable from this Ripe instance.
*
* @param {Interactable} element The Interactable instance to be unbound.
* @returns {Interactable} Returns the unbounded Interactable.
*/
ripe.Ripe.prototype.unbindInteractable = async function(element) {
await element.deinit();
this.children.splice(this.children.indexOf(element), 1);
this.trigger("unbound", element);
};
/**
* Unbinds ab Image from this Ripe instance.
*
* @param {Image} element The Image instance to be unbound.
* @returns {Image} Returns the unbounded Image.
*/
ripe.Ripe.prototype.unbindImage = ripe.Ripe.prototype.unbindInteractable;
/**
* Unbinds ab Configurator from this Ripe instance.
*
* @param {Configurator} element The Image instance to be unbound.
* @returns {Configurator} Returns the unbounded Configurator.
*/
ripe.Ripe.prototype.unbindConfigurator = ripe.Ripe.prototype.unbindInteractable;
/**
* Selects a part of the model.
* Triggers a 'selected_part' event with the part.
*
* @param {String} part The name of the part to be selected.
* @param {Object} options An Object with options to configure the operation.
*/
ripe.Ripe.prototype.selectPart = function(part, options = {}) {
this.trigger("selected_part", part);
};
/**
* Deselects a part of the model.
* Triggers a 'deselected_part' event with the part.
*
* @param {String} part The name of the part to be deselected.
* @param {Object} options An Object with options to configure the operation.
*/
ripe.Ripe.prototype.deselectPart = function(part, options = {}) {
this.trigger("deselected_part", part);
};
/**
* Triggers the update of the children so that they represent the
* current state of the model.
*
* This is considered the many state change operation and should be
* called whenever a relevant internal state value is changed so that
* the visuals are updated in accordance.
*
* @param {Object} state An Object with the current customization and
* personalization, if not provided the current internal state of the
* instance will be used instead.
* @param {Object} options Set of update options that change the way
* the update operation is going to be performed.
* @param {Array} children The set of children that are going to be affected
* by the updated operation, if not provided all of the currently registered
* children in the instance will be used.
* @return The result of the update operation, meaning that if any child
* operation has been performed the result is true otherwise in case this is
* a no-op from the "visual" point of view the result is false.
*/
ripe.Ripe.prototype.update = async function(state = null, options = {}, children = null) {
// in case the force flag is not set and the ready or the configured
// values are not set (instance not ready for updates)
if (!options.force && (this.ready === false || this.configured === false)) {
return false;
}
// tries to retrieve the state of the configuration for which an update
// operation is going to be requested
state = state || this._getState();
// defaults the children variable to the current set of registered
// children, as expected by specification
children = children || this.children;
// increments the update counter, meaning that a new update operation
// is going to be performed (and requires a proper unique identifier)
this.updateCounter += 1;
const id = this.updateCounter;
const _update = async () => {
await this.trigger("pre_update", {
id: id,
state: state,
options: options
});
const promises = [];
for (let index = 0; index < children.length; index++) {
const child = children[index];
promises.push(child.update(state, options));
}
if (this.ready) {
await this.trigger("update", {
id: id,
state: state,
options: options
});
}
// in case the use price flag is set then we should "automagically"
// retrieve the price for the currently changed configuration
if (this.ready && this.configured && this.usePrice) {
const timestamp = Date.now();
this._priceTimestamp = timestamp;
this.getPriceP(state)
.then(value => {
if (this._priceTimestamp > timestamp) return;
this.trigger("price", value);
})
.catch(err => this.trigger("price_error", err));
}
// waits for all the promises "responsible" for the visual updating
// the children of the instance and then verifies if any of them was
// effectively updated (not cached), that is considered to be the
// result of the update operation as whole (indicates if this was an
// effective update operation or if otherwise was a cache match)
const results = await Promise.all(promises);
const result = results.some(v => v !== false);
const canceled = results.some(v => Boolean(v && v.canceled));
await this.trigger("post_update", {
id: id,
state: state,
options: options,
result: result,
canceled: canceled
});
return result;
};
// runs the cancel operation, so that any pending update is canceled
// without any possible visual changes and consuming the least resources
// possible by any of the child elements
await this.cancel(options, children);
// runs the waiting for all the pending promises for update operations
// so that we can safely run the new update promise after all the other
// previously registered ones are "flushed", after te update the promise
// reference is forced to null to indicate that no more promises exist
if (this.updatePromise && (!options.noAwaitLayout || !this.noAwaitLayout)) {
await this.updatePromise;
}
this.updatePromise = null;
// in case the current update operation is no longer the latest one then
// there's no need to continue with the operation
if (id !== this.updateCounter) return;
try {
this.updatePromise = _update();
if (options.noAwaitLayout || this.noAwaitLayout) return true;
const result = await this.updatePromise;
return result;
} finally {
if (!options.noAwaitLayout) {
this.updatePromise = null;
}
}
};
ripe.Ripe.prototype.cancel = async function(options = {}, children = null) {
// defaults the children variable to the current set of registered
// children, as expected by specification
children = children || this.children;
const _cancel = async () => {
await this.trigger("pre_cancel", { id: this.updateCounter });
const promises = [];
for (let index = 0; index < children.length; index++) {
const child = children[index];
promises.push(child.cancel(options));
}
let results = [];
this.cancelPromise = Promise.all(promises);
try {
results = await this.cancelPromise;
} finally {
this.cancelPromise = null;
}
const result = results.some(v => v !== false);
const canceled = results.some(v => Boolean(v && v.canceled));
// in case there's an update promise pending waits for it
// so that we're sure and safe that we can run a new one
if (this.updatePromise) await this.updatePromise;
await this.trigger("post_cancel", {
id: this.updateCounter,
result: result,
canceled: canceled
});
return result;
};
try {
this.cancelPromise = _cancel();
const result = await this.cancelPromise;
return result;
} finally {
this.cancelPromise = null;
}
};
/**
* Reverses the last change to the parts. It is possible
* to undo all the changes done from the initial state.
*/
ripe.Ripe.prototype.undo = async function() {
if (!this.canUndo()) {
return;
}
this.historyPointer -= 1;
const parts = this.history[this.historyPointer];
if (parts) await this.setParts(parts, true, { action: "undo", partEvents: false });
};
/**
* Executes the same operation as `undo` but goes all the way
* to the bottom of the stack that controls the history.
*/
ripe.Ripe.prototype.undoAll = async function() {
if (!this.canUndo()) {
return;
}
this.historyPointer = 0;
const parts = this.history[this.historyPointer];
if (parts) await this.setParts(parts, true, { action: "undo", partEvents: false });
};
/**
* Reapplies the last change to the parts that was undone.
* Notice that if there's a change when the history pointer
* is in the middle of the stack the complete stack forward
* is removed (history re-written).
*/
ripe.Ripe.prototype.redo = async function() {
if (!this.canRedo()) {
return;
}
this.historyPointer += 1;
const parts = this.history[this.historyPointer];
if (parts) await this.setParts(parts, true, { action: "redo", partEvents: false });
};
/**
* Executes the same operation as `redo` but goes all the way
* to the top of the stack that controls the history.
*/
ripe.Ripe.prototype.redoAll = async function() {
if (!this.canRedo()) {
return;
}
this.historyPointer = this.history.length - 1;
const parts = this.history[this.historyPointer];
if (parts) await this.setParts(parts, true, { action: "redo", partEvents: false });
};
/**
* Indicates if there are part changes to undo.
*
* @returns {Boolean} If there are changes to reverse in the
* current parts history stack.
*/
ripe.Ripe.prototype.canUndo = function() {
return this.historyPointer > 0;
};
/**
* Indicates if there are part changes to redo.
*
* @returns {Boolean} If there are changes to reapply pending
* in the history stack.
*/
ripe.Ripe.prototype.canRedo = function() {
return this.history.length - 1 > this.historyPointer;
};
/**
* Returns a promise that is fulfilled once the Ripe Instance
* is ready to be used.
*
* This can be used to actively wait for the initialization of
* the Ripe Instance under an async environment.
*
* @returns {Promise} The promise to be fulfilled once the instance
* is ready to be used.
*/
ripe.Ripe.prototype.isReady = async function() {
await new Promise((resolve, reject) => {
if (this.ready) {
resolve();
} else if (this.error) {
reject(this.error);
} else {
this.bind("ready", resolve);
this.bind("error", reject);
}
});
};
/**
* Returns a promise that is resolved once the remote locale bundles
* are retrieved from their remote locations.
*
* This is relevant for situations where proper location is required
* for a certain scenario (eg: sizes).
*
* @returns {Promise} The promise to be fulfilled on the base locale
* bundles are loaded.
*/
ripe.Ripe.prototype.hasBundles = async function() {
await new Promise((resolve, reject) => {
if (this.bundles) resolve();
else this.bind("bundles", resolve);
});
};
/**
* Registers a plugin to this Ripe instance.
*
* @param {Plugin} plugin The plugin to be registered.
*/
ripe.Ripe.prototype.addPlugin = function(plugin) {
plugin.register(this);
this.plugins.push(plugin);
};
/**
* Unregisters a plugin to this Ripe instance.
*
* @param {Plugin} plugin The plugin to be unregistered.
*/
ripe.Ripe.prototype.removePlugin = function(plugin) {
plugin.unregister(this);
this.plugins.splice(this.plugins.indexOf(plugin), 1);
};
/**
* Normalizes the parts dictionary by taking into account optional parts
* that should be set even for empty situations.
*
* @param {Object} parts The parts object that should be cloned and then
* ensured to have the optional parts set.
* @returns {Object} A copy of the provided parts with the optional parts
* set even if not defined.
*/
ripe.Ripe.prototype.normalizeParts = function(parts) {
if (!parts) return parts;
const defaults = this.loadedConfig.defaults;
const _parts = ripe.clone(parts);
for (const part in defaults) {
if (!defaults[part].optional) continue;
if (_parts[part] !== undefined) continue;
_parts[part] = {
material: undefined,
color: undefined
};
}
return _parts;
};
/**
* Retrieves the dimension of the image associated with the provided name
* and for the requested face.
* The returned dimension structure should contain information about: size,
* format and other meta-information.
*
* @param {String} name The name of the dimension we want to get information about.
* @param {String} face The face we want to get the dimension of.
* @returns {Object} An object containing the dimension information for the
* requested name and face.
*/
ripe.Ripe.prototype.getDimension = function(name = "$base", face = null) {
if (!this.loadedConfig) return null;
const dimensions = this.loadedConfig.dimensions;
if (!dimensions || Object.keys(dimensions).length === 0) {
return null;
}
const dimensionSpec = dimensions[name];
if (!dimensionSpec || Object.keys(dimensionSpec).length === 0) {
return null;
}
if (!face) return dimensionSpec;
if (!dimensionSpec.faces) return dimensionSpec;
return dimensionSpec.faces[face] ? dimensionSpec.faces[face] : dimensionSpec;
};
/**
* Obtains a tuple (array) containing the best possible size for the
* provided dimension and face.
* Uses the dimension retrieval process to obtain that size, defaulting
* to the base configuration `size` attribute in case no specific
* dimension value is available.
*
* @param {String} dimension The name of the dimension we want to get
* information about.
* @param {String} face The face we want to get the dimension of.
* @returns {Array} An array containing both the width and the height
* of the image for the given dimension and face.
*/
ripe.Ripe.prototype.getSize = function(dimension = "$base", face = null) {
if (!this.loadedConfig) return null;
const dimensionSpec = this.getDimension(dimension, face);
if (!dimensionSpec) return this.loadedConfig.size;
if (!dimensionSpec.size) return this.loadedConfig.size;
return dimensionSpec.size;
};
/**
* @ignore
*/
ripe.Ripe.prototype._guessURL = async function() {
const result = await this.geoResolveP();
const country = result.country ? result.country.iso_code : null;
switch (country) {
case "CN":
this.url = "https://app.cn.platforme.com:8444/api/";
this.webUrl = "https://app.cn.platforme.com:8444/";
this.options.url = this.url;
this.options.webUrl = this.webUrl;
break;
default:
this.url = "https://app.platforme.com/api/";
this.webUrl = "https://app.platforme.com/";
this.options.url = this.url;
this.options.webUrl = this.webUrl;
break;
}
};
/**
* @ignore
*/
ripe.Ripe.prototype._initBundles = async function(locale = this.locale, defaultLocale = "en_us") {
// initializes the sequence of locales that are going to be loaded
// taking into consideration if a default locale is explicitly defined
// and then converts the array into a set to avoid duplicates
const locales = defaultLocale ? [defaultLocale, locale] : [locale];
const localesSet = Array.from(new Set(locales));
// builds tuples of locales and respective bundle promises
const localeBundleTuples = [];
for (const locale of localesSet) {
localeBundleTuples.push(
[locale, this.localeBundleP(locale, "scales")],
[locale, this.localeBundleP(locale, "sizes")]
);
}
// deconstructs the tuple to respective locales and bundle promises
// then fetch all bundles in parallel
const [bundlesLocales, bundlesPromises] = localeBundleTuples.reduce(
(array, [locale, bundlePromise]) => [
[...array[0], locale],
[...array[1], bundlePromise]
],
[[], []]
);
const bundles = await Promise.all(bundlesPromises);
// adds the fetched bundles to the bundle registry
bundles.forEach((bundle, index) => {
const locale = bundlesLocales[index];
this.addBundle(bundle, locale);
});
this.bundles = true;
this.trigger("bundles", localesSet, bundles);
return {
locales: localesSet,
bundles: bundles
};
};
/**
* @ignore
*/
ripe.Ripe.prototype._getState = function(safe = true) {
return safe
? JSON.parse(JSON.stringify(this._getState(false)))
: {
brand: this.brand,
model: this.model,
parts: this.parts,
initials: this.initials,
engraving: this.engraving,
initialsExtra: this.initialsExtra,
variant: this.variant
};
};
/**
* @ignore
*/
ripe.Ripe.prototype._setPart = async function(
part,
material,
color,
events = true,
force = false
) {
// ensures that there's one valid configuration loaded
// in the current instance, required for part setting
if (!this.loadedConfig) {
throw Error("Model config is not loaded");
}
// if the material or color are not set then this
// is considered a removal operation and the part
// is removed from the parts structure if it's
// optional or an error is thrown if it's required
const partInfo = this.loadedConfig.defaults[part];
const isRequired = partInfo.optional !== true;
const remove = Boolean(material && color) === false;
if (isRequired && remove) {
throw Error(`Part '${part}' can't be removed`);
}
// retrieves the current value structure for the part
// that is going to be changed and determines if its value
// is already the same as the new one to be set, this is
// going to influence the triggering of events
const current = this.parts[part] || {};
const isSame = remove
? current.material === undefined && current.color === undefined
: current.material === material && current.color === color;
// in case the current value for the part is already the same
// as the requested new one and the force flag is not set returns
// immediately with the false flag indicating that no operation
// has been performed
if (!force && isSame) {
return false;
}
// increments the part "change" counter, so that it's possible
// o track unique part change operations
this.partCounter++;
// updates the value object with the newly requested values, notice
// than in case this is a removal a null value is set for both the
// material and color keys
const value = {
material: remove ? null : material,
color: remove ? null : color
};
// "builds" the inline closure function that handles the
// changing of the parts structure according to the new
// requested state (material and color)
const updatePart = () => {
if (remove) {
delete this.parts[part];
} else {
this.parts[part] = value;
}
};
// in case no events should be raised for the part change
// operation then just updates the parts structure and then
// returns the control flow immediately
if (!events) {
updatePart();
return true;
}
// triggers the update part operation properly encapsulated
// around the associated events (allowing proper interception)
await this.trigger("pre_part", part, value);
updatePart();
await this.trigger("part", part, value);
await this.trigger("post_part", part, value);
// returns a valid value indicating that a concrete operation
// of part changing has been performed
return true;
};
/**
* @ignore
*/
ripe.Ripe.prototype._setParts = async function(update, events = true) {
for (let index = 0; index < update.length; index++) {
const part = update[index];
await this._setPart(part[0], part[1], part[2], events);
}
};
/**
* @ignore
*/
ripe.Ripe.prototype._partsList = function(parts) {
parts = parts || this.parts;
const partsList = [];
for (const part in parts) {
const value = parts[part];
partsList.push([part, value.material, value.color]);
}
return partsList;
};
/**
* @ignore
*/
ripe.Ripe.prototype._pushHistory = function() {
if (!this.parts || !Object.keys(this.parts).length) {
return;
}
if (ripe.equal(this.parts, this.history[this.historyPointer])) {
return;
}
const _parts = this.normalizeParts(this.parts);
this.history = this.history.slice(0, this.historyPointer + 1);
this.history.push(_parts);
this.historyPointer = this.history.length - 1;
};
/**
* The default fallback error handler to be used for
* every single detached async context.
*
* @param {Error} error The error that is going to be
* handled.
*/
ripe.Ripe.prototype._errorHandler = function(error) {
// sets the error in the current instance and then triggers the
// error event on the current instance (notification)
this.ready = this.ready || false;
this.error = error;
this.trigger("error", error);
console.error(error.message || error);
};
/**
* Handles the changes in the provided resulting context (ctx)
* changing the internal state and triggering relevant events.
*
* @param {Object} result The resulting ctx object that is going to
* be used in the changing of the internal state.
*/
ripe.Ripe.prototype._handleCtx = function(result) {
// handles some of the pre-conditions for the handling of the
// "received context", so that all the required fields for
// handling are properly populated before the execution logic
if (result === undefined || result === null) return;
if (result.parts === undefined || result.parts === null) return;
// runs the defaulting of the message meaning that a sequence (array)
// is always defined for proper logic handling
result.messages = result.messages === undefined ? [] : result.messages;
// run the reset operation on the parts object as it's going
// to be re-created using the provided context parts definition
Object.keys(this.parts).forEach(key => delete this.parts[key]);
this.parts = Object.assign(this.parts, result.parts);
if (result.initials && !ripe.equal(result.initials, this.initialsExtra)) {
this.setInitialsExtra(result.initials, true, false, { noRemote: true });
}
if (result.choices && !ripe.equal(result.choices, this.choices)) {
this.setChoices(result.choices);
}
for (const [name, value] of result.messages) {
this.trigger("message", name, value);
}
};
/**
* Builds the choices structure that is going to control
* the state for parts materials and colors under the current
* customization session.
*
* @param {Object} loadedConfig The configuration structure that
* has just been loaded.
* @returns {Object} The state object that can be used to control
* the state of parts, materials and colors.
*/
ripe.Ripe.prototype._toChoices = function(loadedConfig) {
const choices = {};
for (const part of loadedConfig.parts) {
if (loadedConfig.defaults[part.name].hidden) continue;
if (loadedConfig.hidden && loadedConfig.hidden.includes(part.name)) continue;
const materialsState = {};
choices[part.name] = {
available: true,
materials: materialsState
};
for (const material of part.materials) {
const colorsState = {};
materialsState[material.name] = {
available: true,
colors: colorsState
};
for (const color of material.colors) {
colorsState[color] = {
available: true
};
}
}
}
return choices;
};
ripe.Ripe.prototype._supportsWebp = function() {
const element = document.createElement("canvas");
if (!(element.getContext && element.getContext("2d"))) return false;
return element.toDataURL("image/webp").indexOf("data:image/webp") === 0;
};
/**
* Loads the initials builder logic from the remote data source.
* This is done by loading a remote Javascript file that should contain
* the `initialsBuilder` method should be present in the base object
* of such Javascript file.
*/
ripe.Ripe.prototype._loadInitialsBuilder = async function() {
// get initials builder of the build and model, if there is one
// defined, later used in the image's initials builder logic
const logicScriptText = await this.getLogicP({
brand: this.brand,
model: this.model,
version: this.version,
format: "js"
});
// eslint-disable-next-line no-eval
const logicScript = eval(logicScriptText);
this.initialsBuilder = logicScript
? logicScript.initialsBuilder.bind(this)
: this._initialsBuilder.bind(this);
};
/**
* Generates all profiles based on the provided engraving string and the
* contexts provided.
* This is a base implementation which should be overridden if any specific
* behaviour is meant to be provided for a specific context (model).
*
* @param {String} initials The initials string of the personalization.
* @param {String} engraving The normalized engraving string that is going
* to be used in the profile generation (should comply with the naming
* standard).
* @param {String} group The initials group that is going to be used to
* build the profiles.
* @param {String} viewport The viewport that is going to be used to
* build the profiles.
* @param {Array} context The context list that is used to permutate all
* the profiles to apply specific views to the initials image.
* @returns {Object} An object containing the initials of the personalization and
* the sequence of generated profiles properly ordered from the most
* concrete (more specific) to the least concrete (more general).
*/
ripe.Ripe.prototype._initialsBuilder = async function(
initials,
engraving,
group = null,
viewport = null,
context = null,
ctx = {}
) {
let profiles = this._generateProfiles(group, viewport);
profiles = this._buildProfiles(engraving, profiles, context);
return {
initials: initials,
profile: profiles
};
};
/**
* Generates a set of extra profiles according to the provided group
* and viewport.
*
* @param {String} group The name of the "initials" group, to which the
* profiles are going to be generated.
* @param {String} viewport The name of the viewport, to which the
* profiles are going to be generated.
* @returns {Array} The profiles definition list that was generated from the
* provided group and viewport "input".
*/
ripe.Ripe.prototype._generateProfiles = function(group, viewport) {
const values = [];
if (group && viewport) {
values.push({
type: "group_viewport",
name: group + ":" + viewport
});
}
if (group) {
values.push({
type: "group",
name: group
});
}
if (viewport) {
values.push({
type: "viewport",
name: viewport
});
}
return values;
};
/**
* Builds the complete set of profiles to be used in the initials composition
* for the provided normalized engraving string and taking into account the
* provided context.
*
* @param {String} engraving The normalized engraving string that is going to
* be used in the profile generation (should comply with the naming standard).
* @param {Array} profiles The extra sequence containing multiple values to be
* added to the parsed engraving ones and be used in the initials generation
* of the combination for the profiles generation.
* @param {Array} context The context list that be added to the profile complete
* name on the profile generation and, in the last stage of the generation, to
* the beginning of the final profiles list.
* @param {String} sep String that is going to be used as the separator between
* the base profile string and the context.
* @returns {Array} The sequence of generated profiles properly ordered from the
* most concrete (more specific) to the least concrete (more general).
*/
ripe.Ripe.prototype._buildProfiles = function(engraving, profiles, context = null, sep = ":") {
// parses the provided engraving string so that it's possible to iterate
// over the multiple structured values of it, then adds these same values
// to the base values passed as argument to the method
const engravingProfiles = engraving ? this.parseEngraving(engraving).values : [];
profiles = [...engravingProfiles, ...profiles];
// converts a possible null context into an empty array, supporting
// no context and only using the given profiles
context = context || [];
// retrieves the ordered set of property types, which will be used to
// order the profiles
const propertyTypes = this.getProperties().propertyTypes;
// sorts the property values by the order declared on the model config,
// this is critical for proper value usage (in a sequence)
profiles = profiles.sort((a, b) => {
const typeAIndex = propertyTypes.includes(a.type)
? propertyTypes.indexOf(a.type)
: propertyTypes.length;
const typeBIndex = propertyTypes.includes(b.type)
? propertyTypes.indexOf(b.type)
: propertyTypes.length;
return typeAIndex - typeBIndex;
});
// creates the list to hold the multiple combinations of values
// and then iterates over the range of values size to create the
// multiple combinations for the current values
let combinations = [];
for (let i = 0; i < profiles.length; i++) {
combinations = [...combinations, ...ripe.combinations(profiles, i + 1)];
}
combinations.reverse();
// iterates over the profiles and append the context
// to them, resulting in all the profile combinations
// for the provided context
profiles = {};
for (const combination of combinations) {
let profile = [];
let profileWithName = [];
// iterates over the context values and construct all
// the permutations with the existing combinations, both
// normal and with their type and names reversed
for (const contextValue of context) {
profile = [contextValue];
profileWithName = [contextValue];
for (const value of combination) {
// ignore null values for profiles, allowing compatibility
// with previous implementations
if (!value) continue;
// support for string format, offering backward compatibility
// with existing initials builders
if (typeof value === "string") profile.push(value);
else {
// add property name and the string composed of its name and type
// to the profile
profile.push(value.type ? `${value.type}::${value.name}` : value.name);
profileWithName.push(value.name);
}
}
// add the profile string combination to the profile list, as well as
// the context value
const profileString = profile.join(sep);
const profileStringWithName = profileWithName.join(sep);
if (!profiles[profileString]) profiles[profileString] = true;
if (!profiles[profileStringWithName]) profiles[profileStringWithName] = true;
}
profile = [];
profileWithName = [];
// iterate over the combination values and creates all
// the permutations with both the name::type format and
// the its reverse
for (const value of combination) {
// ignore null values for profiles, allowing compatibility
// with previous implementations
if (!value) continue;
// support for string format, offering backward compatibility
// with existing initials builders
if (typeof value === "string") profile.push(value);
else {
// add property name and the string composed of its name and type
// to the profile
profile.push(value.type ? `${value.type}::${value.name}` : value.name);
profileWithName.push(value.name);
}
}
const profileString = profile.join(sep);
const profileStringWithName = profileWithName.join(sep);
if (!profiles[profileString]) profiles[profileString] = true;
if (!profiles[profileStringWithName]) profiles[profileStringWithName] = true;
}
return [...Object.keys(profiles), ...context];
};
// eslint-disable-next-line no-unused-vars,no-var
var Ripe = ripe.Ripe;
if (typeof window !== "undefined") window.Ripe = ripe.Ripe;