UNPKG

@ninetailed/experience.js-plugin-preview

Version:
656 lines (642 loc) 25.4 kB
'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;