@ninetailed/experience.js-plugin-preview
Version:
Ninetailed SDK plugin for preview
656 lines (642 loc) • 25.4 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var experience_jsShared = require('@ninetailed/experience.js-shared');
var experience_js = require('@ninetailed/experience.js');
var experience_jsPluginAnalytics = require('@ninetailed/experience.js-plugin-analytics');
var radash = require('radash');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return Object.freeze(n);
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
const CONTAINER_WIDTH = 432;
const BUTTON_WIDTH = 48;
const BUTTON_HEIGHT = 192;
const BUTTON_BOTTOM_POSITION = 128;
const TRANSFORM_CLOSED = `translate(${CONTAINER_WIDTH - BUTTON_WIDTH}px, 0px)`;
const TRANSFORM_CLOSED_HIDE = `translate(${CONTAINER_WIDTH}px, 0px)`;
const TRANSFORM_OPEN = `translate(0px, 0px)`;
class WidgetContainer {
constructor(options) {
var _a, _b;
this.options = options;
this.container = document.createElement('div');
this.container.classList.add(WidgetContainer.CONTAINER_CLASS);
this.container.style.position = 'fixed';
this.container.style.zIndex = '999999';
this.container.style.right = '0px';
this.container.style.bottom = `${BUTTON_BOTTOM_POSITION}px`;
this.container.style.width = `${CONTAINER_WIDTH}px`;
this.container.style.height = `${BUTTON_HEIGHT}px`;
this.container.style.overflow = 'hidden';
if ((_b = (_a = options.ui) === null || _a === void 0 ? void 0 : _a.opener) === null || _b === void 0 ? void 0 : _b.hide) {
this.container.style.transform = TRANSFORM_CLOSED_HIDE;
} else {
this.container.style.transform = TRANSFORM_CLOSED;
}
this.container.style.transitionTimingFunction = 'cubic-bezier(0.4, 0, 0.2, 1)';
this.container.style.transitionDuration = '700ms';
this.container.style.transitionProperty = 'transform';
document.body.appendChild(this.container);
}
open() {
this.container.style.transform = TRANSFORM_OPEN;
this.container.style.height = '100vh';
this.container.style.bottom = `0px`;
}
close() {
var _a, _b;
if ((_b = (_a = this.options.ui) === null || _a === void 0 ? void 0 : _a.opener) === null || _b === void 0 ? void 0 : _b.hide) {
this.container.style.transform = TRANSFORM_CLOSED_HIDE;
} else {
this.container.style.transform = TRANSFORM_CLOSED;
}
setTimeout(() => {
this.container.style.height = `${BUTTON_HEIGHT}px`;
this.container.style.bottom = `${BUTTON_BOTTOM_POSITION}px`;
}, 700);
}
get element() {
return this.container;
}
static isContainerAttached() {
return document.querySelector(`.${WidgetContainer.CONTAINER_CLASS}`) !== null;
}
}
WidgetContainer.CONTAINER_CLASS = 'nt-preview-widget-container';
var _a;
class NinetailedPreviewPlugin extends experience_jsPluginAnalytics.NinetailedPlugin {
constructor(options) {
super();
this.options = options;
this.name = 'ninetailed:preview' + Math.random();
this.isOpen = false;
this.experiences = [];
this.audienceDefinitions = [];
this.audienceOverwrites = {};
this.experienceVariantIndexOverwrites = {};
this.variableOverwrites = {};
this.profile = null;
this.changes = [];
this.container = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.bridge = null;
/**
* Since several instances of the plugin can be created, we need to make sure only one is marked as active.
*/
this.isActiveInstance = false;
this.onChangeEmitter = new experience_js.OnChangeEmitter();
this.clientId = null;
this.environment = null;
this.initialize = () => __awaiter(this, void 0, void 0, function* () {
var _b, _c;
if (typeof window !== 'undefined') {
if (WidgetContainer.isContainerAttached()) {
experience_jsShared.logger.warn('Preview plugin is already attached.');
this.isActiveInstance = false;
return;
}
const {
PreviewBridge
} = yield Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('@ninetailed/experience.js-preview-bridge')); });
this.isActiveInstance = true;
this.container = new WidgetContainer({
ui: this.options.ui
});
this.bridge = PreviewBridge({
url: this.options.url,
nonce: this.options.nonce
});
this.bridge.render(this.container.element);
window.ninetailed = Object.assign({}, window.ninetailed, {
plugins: Object.assign(Object.assign({}, (_b = window.ninetailed) === null || _b === void 0 ? void 0 : _b.plugins), {
preview: this.windowApi
})
});
this.bridge.updateProps({
props: this.pluginApi
});
if (!((_c = this.changes) === null || _c === void 0 ? void 0 : _c.length)) {
this.onChange();
}
}
});
this.loaded = () => true;
this[_a] = ({
payload
}) => {
if (!this.isActiveInstance) {
return;
}
if (payload === null || payload === void 0 ? void 0 : payload.profile) {
this.onProfileChange(payload.profile, payload.changes || []);
}
};
/**
* Implements the HasChangesModificationMiddleware interface
* Returns a middleware function that applies variable overwrites to changes
*/
this.getChangesModificationMiddleware = () => {
if (!this.isActiveInstance || Object.keys(this.variableOverwrites).length === 0 && Object.keys(this.audienceOverwrites).length === 0) {
return undefined;
}
return ({
changes: inputChanges
}) => {
if (!inputChanges || inputChanges.length === 0) {
return {
changes: inputChanges
};
}
const filteredChanges = inputChanges.filter(change => {
var _b, _c;
const experienceId = (_b = change.meta) === null || _b === void 0 ? void 0 : _b.experienceId;
if (!experienceId) return true;
const experience = this.experiences.find(e => e.id === experienceId);
if (!((_c = experience === null || experience === void 0 ? void 0 : experience.audience) === null || _c === void 0 ? void 0 : _c.id)) return true;
return this.audienceOverwrites[experience.audience.id] !== false;
});
return {
changes: this.getEffectiveChanges(filteredChanges)
};
};
};
this.getExperienceSelectionMiddleware = ({
baseline,
experiences
}) => {
if (!this.isActiveInstance) {
return;
}
return () => {
const experienceIds = Object.keys(this.pluginApi.experienceVariantIndexes);
const experience = experiences.find(experience => {
return experienceIds.includes(experience.id);
});
if (!experience) {
return {
experience: null,
variant: baseline,
variantIndex: 0
};
}
// Handle entry replacements as before
const entryReplacementComponents = experience.components.filter(component => component.type === experience_jsShared.ComponentTypeEnum.EntryReplacement && 'id' in component.baseline);
const baselineComponent = entryReplacementComponents.find(component => component.baseline.id === baseline.id);
const variantIndex = this.pluginApi.experienceVariantIndexes[experience.id];
// Continue with entry replacement handling
if (!baselineComponent) {
return {
experience,
variant: baseline,
variantIndex: 0
};
}
const allVariants = [baseline, ...baselineComponent.variants];
if (allVariants.length <= variantIndex) {
return {
experience,
variant: baseline,
variantIndex: 0
};
}
const variant = allVariants[variantIndex];
if (!variant) {
return {
experience,
variant: baseline,
variantIndex: 0
};
}
return {
experience,
variant,
variantIndex
};
};
};
this.onChange = () => {
var _b;
experience_jsShared.logger.debug('Ninetailed Preview Plugin onChange pluginApi:', this.pluginApi);
if (typeof window !== 'undefined') {
window.ninetailed = Object.assign({}, window.ninetailed, {
plugins: Object.assign(Object.assign({}, (_b = window.ninetailed) === null || _b === void 0 ? void 0 : _b.plugins), {
preview: this.windowApi
})
});
}
this.bridge.updateProps({
props: this.pluginApi
});
this.onChangeEmitter.invokeListeners();
};
this.onProfileChange = (profile, changes) => {
this.profile = profile;
experience_jsShared.logger.debug('Profile changed:', {
profile,
changes
});
// If changes are provided, update them
if (changes) {
this.onChangesChange(changes);
}
this.onChange();
};
/**
* Handles changes from the SDK and applies any variable overrides.
* This should be called whenever the original changes are updated.
*/
this.onChangesChange = incomingChanges => {
if (!this.isActiveInstance) {
return;
}
experience_jsShared.logger.debug('Received changes:', incomingChanges);
// Store the original changes
this.changes = incomingChanges;
// Notify listeners and update UI
this.onChange();
};
this.setCredentials = ({
clientId,
environment
}) => {
this.clientId = clientId;
this.environment = environment;
};
this.experiences = options.experiences || [];
this.audienceDefinitions = options.audiences || [];
this.onOpenExperienceEditor = options.onOpenExperienceEditor;
this.onOpenAudienceEditor = options.onOpenAudienceEditor;
}
open() {
var _b;
if (!this.isActiveInstance) {
return;
}
(_b = this.container) === null || _b === void 0 ? void 0 : _b.open();
this.isOpen = true;
this.onChange();
}
close() {
var _b;
if (!this.isActiveInstance) {
return;
}
(_b = this.container) === null || _b === void 0 ? void 0 : _b.close();
setTimeout(() => {
this.isOpen = false;
this.onChange();
}, 700);
}
toggle() {
if (!this.isActiveInstance) {
return;
}
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
activateAudience(id) {
if (!this.isActiveInstance) {
return;
}
if (!this.isKnownAudience(id)) {
experience_jsShared.logger.warn(`You cannot activate an unknown audience (id: ${id}).`);
return;
}
this.audienceOverwrites = Object.assign(Object.assign({}, this.audienceOverwrites), {
[id]: true
});
this.experienceVariantIndexOverwrites = Object.assign(Object.assign({}, this.experienceVariantIndexOverwrites), this.experiences.filter(experience => {
var _b;
return ((_b = experience.audience) === null || _b === void 0 ? void 0 : _b.id) === id;
}).map(experience => experience.id).reduce((acc, curr) => {
return Object.assign(Object.assign({}, acc), {
[curr]: this.experienceVariantIndexes[curr] || 0
});
}, {}));
this.onChange();
}
deactivateAudience(id) {
if (!this.isActiveInstance) {
return;
}
if (!this.isKnownAudience(id)) {
experience_jsShared.logger.warn(`You cannot deactivate an unknown audience (id: ${id}). How did you get it in the first place?`);
return;
}
this.audienceOverwrites = Object.assign(Object.assign({}, this.audienceOverwrites), {
[id]: false
});
this.onChange();
}
resetAudience(id) {
if (!this.isActiveInstance) {
return;
}
if (!this.isKnownAudience(id)) {
experience_jsShared.logger.warn(`You cannot reset an unknown audience (id: ${id}). How did you get it in the first place?`);
return;
}
// Identify all experiences that belong to this audience
const experiencesToReset = this.experiences.filter(experience => {
var _b;
return ((_b = experience.audience) === null || _b === void 0 ? void 0 : _b.id) === id;
});
if (experiencesToReset.length > 0) {
const experienceIdsToReset = experiencesToReset.map(e => e.id);
// 1. Clear any variable overwrites that were set for these experiences (allows natural evaluation)
this.variableOverwrites = Object.fromEntries(Object.entries(this.variableOverwrites).filter(([, change]) => {
var _b;
return !(((_b = change.meta) === null || _b === void 0 ? void 0 : _b.experienceId) && experienceIdsToReset.includes(change.meta.experienceId));
}));
// 2. Clear experience variant index overwrites for these experiences (allows natural variant selection)
this.experienceVariantIndexOverwrites = Object.fromEntries(Object.entries(this.experienceVariantIndexOverwrites).filter(([key]) => !experienceIdsToReset.includes(key)));
}
// 3. Remove audience override (allows natural audience evaluation)
const _b = this.audienceOverwrites,
_c = id;
_b[_c];
const audienceOverwrites = __rest(_b, [typeof _c === "symbol" ? _c : _c + ""]);
this.audienceOverwrites = audienceOverwrites;
this.onChange();
}
setExperienceVariant({
experienceId,
variantIndex
}) {
if (!this.isActiveInstance) {
return;
}
const experience = this.experiences.find(experience => experience.id === experienceId);
if (!experience) {
experience_jsShared.logger.warn(`Cannot activate a variant for an unknown experience (id: ${experienceId})`);
return;
}
if (experience.audience && !this.activeAudiences.some(id => {
var _b;
return id === ((_b = experience.audience) === null || _b === void 0 ? void 0 : _b.id);
})) {
experience_jsShared.logger.warn(`Cannot activate a variant for an experience (id: ${experienceId}) which is not in the active audiences.`);
return;
}
const isValidIndex = experience.components.map(component => component.variants.length + 1).every(length => length > variantIndex);
if (!isValidIndex) {
experience_jsShared.logger.warn(`You activated a variant at index ${variantIndex} for the experience (id: ${experienceId}). Not all components have that many variants, you may see the baseline for some.`);
}
// Update the experience variant index
this.experienceVariantIndexOverwrites = Object.assign(Object.assign({}, this.experienceVariantIndexOverwrites), {
[experienceId]: variantIndex
});
// Process all components to extract variable values
experience.components.forEach(component => {
var _b, _c;
if (component.type === experience_jsShared.ComponentTypeEnum.InlineVariable) {
const key = component.key;
const value = variantIndex === 0 ? component.baseline.value : (_c = (_b = component.variants[variantIndex - 1]) === null || _b === void 0 ? void 0 : _b.value) !== null && _c !== void 0 ? _c : component.baseline.value;
// Set the variable value
this.setVariableValue({
experienceId,
key,
value,
variantIndex
});
}
});
// Trigger change notification - this updates the middleware
this.onChange();
}
resetExperience(experienceId) {
if (!this.isActiveInstance) {
return;
}
const _b = this.experienceVariantIndexOverwrites,
_c = experienceId;
_b[_c];
const experienceVariantIndexOverwrites = __rest(_b, [typeof _c === "symbol" ? _c : _c + ""]);
this.experienceVariantIndexOverwrites = experienceVariantIndexOverwrites;
this.onChange();
}
reset() {
if (!this.isActiveInstance) {
return;
}
if (typeof window !== 'undefined' && window.ninetailed && typeof window.ninetailed.reset === 'function') {
window.ninetailed.reset();
}
}
/**
* Sets a variable value override for preview
*/
setVariableValue({
experienceId,
key,
value,
variantIndex
}) {
var _b;
if (!this.isActiveInstance) {
return;
}
const overrideKey = `${experienceId}:${key}`;
// Only create new object if actually changing
if (radash.isEqual((_b = this.variableOverwrites[overrideKey]) === null || _b === void 0 ? void 0 : _b.value, value)) {
return; // No change needed
}
const change = {
type: experience_jsShared.ChangeTypes.Variable,
key,
value,
meta: {
experienceId,
variantIndex
}
};
// Update variable overwrites
this.variableOverwrites = Object.assign(Object.assign({}, this.variableOverwrites), {
[overrideKey]: change
});
// Notify listeners
this.onChangeEmitter.invokeListeners();
this.onChange();
}
openExperienceEditor(experience) {
if (this.onOpenExperienceEditor && typeof this.onOpenExperienceEditor === 'function') return this.onOpenExperienceEditor(experience);
}
/**
* @deprecated This method will be removed in a future release. Use `openExperienceEditor` instead to see the experience insights.
*/
openExperienceAnalytics(experience) {
experience_jsShared.logger.warn('The `openExperienceAnalytics` method is deprecated and will be removed in a future release. Use `openExperienceEditor` instead to see the experience insights.');
window.open(`https://app.ninetailed.io/${this.clientId}/${this.environment}/experiences/${experience.id}`, '_blank');
}
openAudienceEditor(audience) {
if (this.onOpenAudienceEditor && typeof this.onOpenAudienceEditor === 'function') return this.onOpenAudienceEditor(audience);
}
get pluginApi() {
var _b;
return {
version: "7.18.9" ,
open: this.open.bind(this),
close: this.close.bind(this),
toggle: this.toggle.bind(this),
isOpen: this.isOpen,
activateAudience: this.activateAudience.bind(this),
deactivateAudience: this.deactivateAudience.bind(this),
resetAudience: this.resetAudience.bind(this),
apiAudiences: ((_b = this.profile) === null || _b === void 0 ? void 0 : _b.audiences) || [],
audienceOverwrites: this.audienceOverwrites,
activeAudiences: this.activeAudiences,
audienceDefinitions: this.audienceDefinitions,
setExperienceVariant: this.setExperienceVariant.bind(this),
resetExperience: this.resetExperience.bind(this),
apiExperienceVariantIndexes: this.apiExperienceVariantIndexes,
experienceVariantIndexes: Object.assign(Object.assign({}, this.experienceVariantIndexes), this.experienceVariantIndexOverwrites),
experienceVariantIndexOverwrites: this.experienceVariantIndexOverwrites,
reset: this.reset.bind(this),
experiences: this.experiences,
openExperienceEditor: this.onOpenExperienceEditor ? this.openExperienceEditor.bind(this) : undefined,
openExperienceAnalytics: this.openExperienceAnalytics.bind(this),
openAudienceEditor: this.onOpenAudienceEditor ? this.openAudienceEditor.bind(this) : undefined
};
}
get windowApi() {
return {
version: '2.0.0',
open: this.open.bind(this),
close: this.close.bind(this),
toggle: this.toggle.bind(this),
activateAudience: this.activateAudience.bind(this),
deactivateAudience: this.deactivateAudience.bind(this),
resetAudience: this.resetAudience.bind(this),
activeAudiences: this.activeAudiences,
setExperienceVariant: this.setExperienceVariant.bind(this),
resetExperience: this.resetExperience.bind(this),
experienceVariantIndexes: Object.assign(Object.assign({}, this.experienceVariantIndexes), this.experienceVariantIndexOverwrites),
setVariableValue: this.setVariableValue.bind(this),
variableOverwrites: this.variableOverwrites
};
}
isKnownAudience(id) {
return this.potentialAudiences.some(audience => audience.id === id);
}
get potentialAudiences() {
const audiencesFromExperiences = this.experiences.map(experience => experience.audience).filter(audience => !!audience);
return experience_jsShared.unionBy(this.audienceDefinitions, audiencesFromExperiences, 'id');
}
get activeAudiences() {
var _b;
const deactivatedAudiences = Object.entries(this.audienceOverwrites).filter(([id, active]) => !active).map(([id]) => id);
const activatedAudiences = Object.entries(this.audienceOverwrites).filter(([id, active]) => active).map(([id]) => id);
return [...(((_b = this.profile) === null || _b === void 0 ? void 0 : _b.audiences) || []), ...activatedAudiences].filter(id => !deactivatedAudiences.includes(id));
}
calculateExperienceVariantIndexes(profile) {
const matchedExperiences = this.experiences.filter(experience => experience_js.isExperienceMatch({
experience,
profile
}));
return matchedExperiences.reduce((acc, experience) => {
const distribution = experience_js.selectDistribution({
experience,
profile
});
return Object.assign(Object.assign({}, acc), {
[experience.id]: distribution.index
});
}, {});
}
get apiExperienceVariantIndexes() {
const profile = this.profile;
if (!profile) {
return {};
}
return this.calculateExperienceVariantIndexes(profile);
}
get experienceVariantIndexes() {
const profile = this.profile;
if (!profile) {
return {};
}
return this.calculateExperienceVariantIndexes(Object.assign(Object.assign({}, profile), {
audiences: this.activeAudiences
}));
}
/**
* Get the override key for a variable
*/
getOverrideKey(experienceId, key) {
return `${experienceId}:${key}`;
}
/**
* Get effective changes by applying overwrites - compute on demand
*/
getEffectiveChanges(inputChanges = this.changes) {
if (!inputChanges || Object.keys(this.variableOverwrites).length === 0) {
return inputChanges || [];
}
// Filter out changes that we're overriding
const filteredChanges = inputChanges.filter(change => {
var _b;
if (change.type !== experience_jsShared.ChangeTypes.Variable) return true;
const changeKey = ((_b = change.meta) === null || _b === void 0 ? void 0 : _b.experienceId) ? this.getOverrideKey(change.meta.experienceId, change.key) : change.key;
return !this.variableOverwrites[changeKey];
});
const effectiveChanges = [...filteredChanges, ...Object.values(this.variableOverwrites)];
experience_jsShared.logger.debug('Overridden changes after applying override:', effectiveChanges);
// Add our overrides to create the final result
return effectiveChanges;
}
}
_a = experience_js.PROFILE_CHANGE;
if (typeof window === 'object' && !('process' in window)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
window.process = {};
}
exports.NinetailedPreviewPlugin = NinetailedPreviewPlugin;
exports["default"] = NinetailedPreviewPlugin;