diff --git a/unitylibs/blocks/unity/unity.css b/unitylibs/blocks/unity/unity.css
index e69de29..4a774a0 100644
--- a/unitylibs/blocks/unity/unity.css
+++ b/unitylibs/blocks/unity/unity.css
@@ -0,0 +1,3 @@
+.unity {
+ display: none !important;
+}
diff --git a/unitylibs/blocks/unity/unity.js b/unitylibs/blocks/unity/unity.js
index a427006..e6a74d7 100644
--- a/unitylibs/blocks/unity/unity.js
+++ b/unitylibs/blocks/unity/unity.js
@@ -24,5 +24,5 @@ export default async function init(el) {
});
await stylePromise;
const { default: wfinit } = await import(`${unitylibs}/core/workflow/workflow.js`);
- await wfinit(el, projectName, unitylibs);
+ await wfinit(el, projectName, unitylibs, 'v2');
}
diff --git a/unitylibs/core/features/progress-circle/progress-circle.css b/unitylibs/core/features/progress-circle/progress-circle.css
index a729e22..29a6852 100644
--- a/unitylibs/core/features/progress-circle/progress-circle.css
+++ b/unitylibs/core/features/progress-circle/progress-circle.css
@@ -1,4 +1,5 @@
-.unity-enabled .interactive-area .progress-holder {
+.unity-enabled .interactive-area .progress-holder,
+.unity-enabled .interactive-area .progress-circle {
display: none;
justify-content: center;
align-items: center;
@@ -11,7 +12,8 @@
z-index: 2;
}
-.unity-enabled .interactive-area.loading .progress-holder {
+.unity-enabled .interactive-area.loading .progress-holder,
+.unity-enabled .interactive-area .progress-circle.show {
display: flex;
}
diff --git a/unitylibs/core/styles/splash-screen.css b/unitylibs/core/styles/splash-screen.css
new file mode 100644
index 0000000..73a5d1a
--- /dev/null
+++ b/unitylibs/core/styles/splash-screen.css
@@ -0,0 +1,150 @@
+body > .splash-loader {
+ z-index: 10000000;
+}
+
+.splash-loader {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ max-height: 100%;
+ max-width: 100%;
+ color: #000;
+ z-index: 3;
+}
+
+.splash-loader .section {
+ height: 100%;
+ width: 100%;
+ max-height: 100%;
+ max-width: 100%;
+}
+
+.splash-loader.show {
+ display: flex !important;
+}
+
+.splash-loader .text {
+ padding: 0;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.splash-loader .text .con-button {
+ display: none;
+}
+
+.progress-holder {
+ width: 100%;
+}
+
+.hide-splash-overflow {
+ overflow: hidden;
+}
+
+.progress-holder .spectrum-ProgressBar {
+ position: relative;
+ display: inline-flex;
+ flex-flow: row wrap;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 15px;
+ vertical-align: top;
+ inline-size: 192px;
+ width: 100%;
+}
+
+.progress-holder .spectrum-ProgressBar .spectrum-ProgressBar-label,
+.progress-holder .spectrum-ProgressBar .spectrum-ProgressBar-percentage {
+ text-align: start;
+ color: rgba(34,34,34);
+ }
+
+.progress-holder .spectrum-ProgressBar .spectrum-ProgressBar-label {
+ display: none;
+}
+
+.progress-holder .spectrum-ProgressBar .spectrum-ProgressBar-percentage {
+ align-self: flex-start;
+ margin-inline-start: 5px;
+}
+
+.progress-holder .spectrum-ProgressBar .spectrum-ProgressBar-track {
+ overflow: hidden;
+ inline-size: 100%;
+ block-size: 8px;
+ border-radius: 5px;
+ background: rgba(213,213,213);
+}
+
+.progress-holder .spectrum-ProgressBar .spectrum-ProgressBar-fill {
+ border: none;
+ block-size: 8px;
+ transition: width 1s;
+ background: #FA0F00;
+}
+
+.progress-holder .spectrum-ProgressBar--sideLabel {
+ display: inline-flex;
+ flex-flow: row;
+ justify-content: space-between;
+}
+
+.progress-holder .spectrum-ProgressBar--sideLabel .spectrum-ProgressBar-track {
+ flex: 1 1 10px;
+}
+
+.progress-holder .spectrum-ProgressBar--sideLabel .spectrum-ProgressBar-label {
+ flex-grow: 0;
+ margin-inline-end: 12px;
+ margin-block-end: 0;
+}
+
+.progress-holder .spectrum-ProgressBar--sideLabel .spectrum-ProgressBar-percentage {
+ order: 3;
+ text-align: end;
+ margin-inline-start: 12px;
+ font-size: var(--type-body-l-size);
+ line-height: var(--type-body-l-lh);
+ font-weight: 700;
+}
+
+.splash-loader .text .icon-area img,
+.splash-loader .text video {
+ width: 104px;
+ height: auto;
+}
+
+@media screen and (min-width: 600px) {
+ .splash-loader .text .icon-area img,
+ .splash-loader .text video {
+ width: 286px;
+ height: auto;
+ }
+
+ .splash-loader h1,
+ .splash-loader h2,
+ .splash-loader h3,
+ .splash-loader h4,
+ .splash-loader h5 {
+ font-size: var(--type-heading-xl-size);
+ line-height: var(--type-heading-xl-lh);
+ font-weight: 700;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .splash-loader .text .con-button {
+ display: flex;
+ }
+
+ .progress-holder {
+ min-width: 400px;
+ }
+}
+
diff --git a/unitylibs/core/styles/styles.css b/unitylibs/core/styles/styles.css
index e984fd5..458f11b 100644
--- a/unitylibs/core/styles/styles.css
+++ b/unitylibs/core/styles/styles.css
@@ -8,7 +8,10 @@
--animation-blue: #1273E6;
}
-.unity,
+.unity {
+ display: none !important;
+}
+
.unity-enabled .interactive-area .unity-widget.decorating {
display: none;
}
@@ -140,12 +143,12 @@
}
.unity-enabled .interactive-area .widget-refresh-button svg path,
-.unity-enabled .interactive-area .unity-action-btn .btn-icon svg path {
+.unity-enabled .interactive-area .unity-action-btn:not(.continue-in-app-button) .btn-icon svg path {
fill: var(--color-white);
}
.unity-enabled .interactive-area.light .widget-refresh-button svg path,
-.unity-enabled .interactive-area.light .unity-action-btn .btn-icon svg path {
+.unity-enabled .interactive-area.light .unity-action-btn:not(.continue-in-app-button) .btn-icon svg path {
fill: var(--color-black);
}
diff --git a/unitylibs/core/workflow/unity-config.js b/unitylibs/core/workflow/unity-config.js
deleted file mode 100644
index e56200a..0000000
--- a/unitylibs/core/workflow/unity-config.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const config = {
- '.marquee': {
- type: 'img',
- selector: '.asset picture, .image picture',
- handler: 'render',
- renderWidget: true,
- },
- '.aside': {
- type: 'img',
- selector: '.asset picture, .image picture',
- handler: 'render',
- renderWidget: true,
- },
-};
diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js
new file mode 100644
index 0000000..86a4f66
--- /dev/null
+++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js
@@ -0,0 +1,456 @@
+/* eslint-disable no-await-in-loop */
+/* eslint-disable class-methods-use-this */
+/* eslint-disable no-restricted-syntax */
+
+import {
+ unityConfig,
+ getUnityLibs,
+ createTag,
+ localizeLink,
+ priorityLoad,
+ loadArea,
+ loadImg,
+} from '../../../scripts/utils.js';
+import getError from '../../../scripts/errors.js';
+
+export default class ActionBinder {
+ constructor(unityEl, workflowCfg, wfblock, canvasArea, actionMap = {}, limits = {}) {
+ this.unityEl = unityEl;
+ this.workflowCfg = workflowCfg;
+ this.block = wfblock;
+ this.actionMap = actionMap;
+ this.limits = limits;
+ this.canvasArea = canvasArea;
+ this.operations = [];
+ this.acrobatApiConfig = this.getAcrobatApiConfig();
+ this.serviceHandler = null;
+ this.splashScreenEl = null;
+ this.promiseStack = [];
+ this.LOADER_DELAY = 800;
+ this.LOADER_INCREMENT = 30;
+ this.LOADER_LIMIT = 95;
+ }
+
+ getAcrobatApiConfig() {
+ unityConfig.acrobatEndpoint = {
+ createAsset: `${unityConfig.apiEndPoint}/asset`,
+ finalizeAsset: `${unityConfig.apiEndPoint}/asset/finalize`,
+ getMetadata: `${unityConfig.apiEndPoint}/asset/metadata`,
+ };
+ return unityConfig;
+ }
+
+ async handlePreloads() {
+ const parr = [`${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/service-handler.js`];
+ if (this.workflowCfg.targetCfg.showSplashScreen) {
+ parr.push(
+ `${getUnityLibs()}/core/styles/splash-screen.css`,
+ `${this.splashFragmentLink}.plain.html`,
+ );
+ }
+ await priorityLoad(parr);
+ }
+
+ updateProgressBar(layer, percentage) {
+ const p = Math.min(percentage, this.LOADER_LIMIT);
+ const spb = layer.querySelector('.spectrum-ProgressBar');
+ spb?.setAttribute('value', p);
+ spb?.setAttribute('aria-valuenow', p);
+ layer.querySelector('.spectrum-ProgressBar-percentage').innerHTML = `${p}%`;
+ layer.querySelector('.spectrum-ProgressBar-fill').style.width = `${p}%`;
+ }
+
+ createProgressBar() {
+ const pdom = `
`;
+ return createTag('div', { class: 'progress-holder' }, pdom);
+ }
+
+ async acrobatActionMaps(values, files, eventName) {
+ await this.handlePreloads();
+ const { default: ServiceHandler } = await import(`${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/service-handler.js`);
+ this.serviceHandler = new ServiceHandler(
+ this.workflowCfg.targetCfg.renderWidget,
+ this.canvasArea,
+ );
+ for (const value of values) {
+ switch (true) {
+ case value.actionType === 'fillsign':
+ this.promiseStack = [];
+ await this.fillsign(files, eventName);
+ break;
+ case value.actionType === 'continueInApp':
+ this.LOADER_LIMIT = 100;
+ this.updateProgressBar(this.splashScreenEl, 100);
+ await this.continueInApp();
+ break;
+ case value.actionType === 'interrupt':
+ await this.cancelAcrobatOperation();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ async initActionListeners(b = this.block, actMap = this.actionMap) {
+ for (const [key, values] of Object.entries(actMap)) {
+ const el = b.querySelector(key);
+ if (!el) return;
+ switch (true) {
+ case el.nodeName === 'A':
+ el.addEventListener('click', async (e) => {
+ e.preventDefault();
+ await this.acrobatActionMaps(values);
+ });
+ break;
+ case el.nodeName === 'DIV':
+ el.addEventListener('drop', async (e) => {
+ e.preventDefault();
+ const files = this.extractFiles(e);
+ await this.acrobatActionMaps(values, files, 'drop');
+ });
+ break;
+ case el.nodeName === 'INPUT':
+ el.addEventListener('change', async (e) => {
+ const files = this.extractFiles(e);
+ await this.acrobatActionMaps(values, files, 'change');
+ e.target.value = '';
+ });
+ break;
+ default:
+ break;
+ }
+ }
+ if (b === this.block) this.splashScreenEl = await this.loadSplashFragment();
+ }
+
+ extractFiles(e) {
+ const files = [];
+ if (e.dataTransfer?.items) {
+ [...e.dataTransfer.items].forEach((item) => {
+ if (item.kind === 'file') {
+ const file = item.getAsFile();
+ files.push(file);
+ }
+ });
+ } else if (e.target?.files) {
+ [...e.target.files].forEach((file) => {
+ files.push(file);
+ });
+ }
+ return files;
+ }
+
+ async dispatchErrorToast(code, showError = true, status = null) {
+ if (showError) {
+ const message = code in this.workflowCfg.errors
+ ? this.workflowCfg.errors[code]
+ : await getError(this.workflowCfg.enabledFeatures[0], code);
+ this.block.dispatchEvent(new CustomEvent(
+ unityConfig.errorToastEvent,
+ { detail: { code, message: message || 'Unable to process the request', ...(status !== null && { status }) } },
+ ));
+ }
+ }
+
+ async fillsign(files, eventName) {
+ if (!files || files.length > this.limits.maxNumFiles) {
+ await this.dispatchErrorToast('verb_upload_error_only_accept_one_file');
+ return;
+ }
+ const file = files[0];
+ if (!file) return;
+ await this.singleFileUpload(file, eventName);
+ }
+
+ async getBlobData(file) {
+ const objUrl = URL.createObjectURL(file);
+ const response = await fetch(objUrl);
+ if (!response.ok) {
+ const error = new Error();
+ error.status = response.status;
+ throw error;
+ }
+ const blob = await response.blob();
+ URL.revokeObjectURL(objUrl);
+ return blob;
+ }
+
+ async uploadFileToUnity(storageUrl, blobData, fileType) {
+ const uploadOptions = {
+ method: 'PUT',
+ headers: { 'Content-Type': fileType },
+ body: blobData,
+ };
+ const response = await fetch(storageUrl, uploadOptions);
+ return response;
+ }
+
+ async chunkPdf(assetData, blobData, filetype) {
+ const totalChunks = Math.ceil(blobData.size / assetData.blocksize);
+ if (assetData.uploadUrls.length !== totalChunks) return;
+ const uploadPromises = Array.from({ length: totalChunks }, (_, i) => {
+ const start = i * assetData.blocksize;
+ const end = Math.min(start + assetData.blocksize, blobData.size);
+ const chunk = blobData.slice(start, end);
+ const url = assetData.uploadUrls[i];
+ return this.uploadFileToUnity(url.href, chunk, filetype);
+ });
+ this.promiseStack.push(...uploadPromises);
+ await Promise.all(this.promiseStack);
+ }
+
+ async continueInApp() {
+ if (!this.operations.length) return;
+ const { assetId, filename, filesize, filetype } = this.operations[this.operations.length - 1];
+ const cOpts = {
+ assetId,
+ targetProduct: this.workflowCfg.productName,
+ payload: {
+ languageRegion: this.workflowCfg.langRegion,
+ languageCode: this.workflowCfg.langCode,
+ verb: this.workflowCfg.enabledFeatures[0],
+ assetMetadata: {
+ [assetId]: {
+ name: filename,
+ size: filesize,
+ type: filetype,
+ },
+ },
+ },
+ };
+ this.promiseStack.push(
+ this.serviceHandler.postCallToService(
+ this.acrobatApiConfig.connectorApiEndPoint,
+ { body: JSON.stringify(cOpts) },
+ ),
+ );
+ await Promise.all(this.promiseStack)
+ .then((resArr) => {
+ const response = resArr[resArr.length - 1];
+ if (!response?.url) throw new Error('Error connecting to App');
+ this.block.dispatchEvent(new CustomEvent(unityConfig.trackAnalyticsEvent, { detail: { event: 'redirect to product' } }));
+ window.location.href = response.url;
+ })
+ .catch(async (e) => {
+ await this.showSplashScreen();
+ await this.dispatchErrorToast('verb_upload_error_generic', e?.showError);
+ });
+ }
+
+ async cancelAcrobatOperation() {
+ await this.showSplashScreen();
+ const e = new Error();
+ e.message = 'Operation termination requested.';
+ e.showError = false;
+ const cancelPromise = Promise.reject(e);
+ this.promiseStack.unshift(cancelPromise);
+ }
+
+ progressBarHandler(s, delay, i, initialize = false) {
+ if (!s) return;
+ delay = Math.min(delay + 100, 2000);
+ i = Math.max(i - 5, 5);
+ const progressBar = s.querySelector('.spectrum-ProgressBar');
+ if (!initialize && progressBar?.getAttribute('value') >= this.LOADER_LIMIT) return;
+ if (initialize) this.updateProgressBar(s, 0);
+ setTimeout(() => {
+ const v = initialize ? 0 : parseInt(progressBar.getAttribute('value'), 10);
+ this.updateProgressBar(s, v + i);
+ this.progressBarHandler(s, delay, i);
+ }, delay);
+ }
+
+ splashVisibilityController(displayOn) {
+ if (!displayOn) {
+ this.LOADER_LIMIT = 95;
+ this.splashScreenEl.parentElement?.classList.remove('hide-splash-overflow');
+ this.splashScreenEl.classList.remove('show');
+ return;
+ }
+ this.progressBarHandler(this.splashScreenEl, this.LOADER_DELAY, this.LOADER_INCREMENT, true);
+ this.splashScreenEl.classList.add('show');
+ this.splashScreenEl.parentElement?.classList.add('hide-splash-overflow');
+ }
+
+ async loadSplashFragment() {
+ if (!this.workflowCfg.targetCfg.showSplashScreen) return;
+ this.splashFragmentLink = localizeLink(`${window.location.origin}${this.workflowCfg.targetCfg.splashScreenConfig.fragmentLink}`);
+ const resp = await fetch(`${this.splashFragmentLink}.plain.html`);
+ const html = await resp.text();
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ const sections = doc.querySelectorAll('body > div');
+ const f = createTag('div', { class: 'fragment splash-loader decorate', style: 'display: none' });
+ f.append(...sections);
+ const splashDiv = document.querySelector(this.workflowCfg.targetCfg.splashScreenConfig.splashScreenParent);
+ splashDiv.append(f);
+ const img = f.querySelector('img');
+ if (img) loadImg(img);
+ await loadArea(f);
+ return f;
+ }
+
+ async handleSplashProgressBar() {
+ const pb = this.createProgressBar();
+ this.splashScreenEl.querySelector('.icon-progress-bar').replaceWith(pb);
+ this.progressBarHandler(this.splashScreenEl, this.LOADER_DELAY, this.LOADER_INCREMENT, true);
+ }
+
+ handleOperationCancel() {
+ const actMap = { 'a.con-button[href*="#_cancel"]': [{ actionType: 'interrupt' }] };
+ this.initActionListeners(this.splashScreenEl, actMap);
+ }
+
+ async showSplashScreen(displayOn = false) {
+ if (!this.splashScreenEl && !this.workflowCfg.targetCfg.showSplashScreen) return;
+ if (this.splashScreenEl.classList.contains('decorate')) {
+ if (this.splashScreenEl.querySelector('.icon-progress-bar')) await this.handleSplashProgressBar();
+ if (this.splashScreenEl.querySelector('a.con-button[href*="#_cancel"]')) this.handleOperationCancel();
+ this.splashScreenEl.classList.remove('decorate');
+ }
+ this.splashVisibilityController(displayOn);
+ }
+
+ async verifyContent(assetData) {
+ try {
+ const finalAssetData = {
+ surfaceId: unityConfig.surfaceId,
+ targetProduct: this.workflowCfg.productName,
+ assetId: assetData.id,
+ };
+ const finalizeJson = await this.serviceHandler.postCallToService(
+ this.acrobatApiConfig.acrobatEndpoint.finalizeAsset,
+ { body: JSON.stringify(finalAssetData), signal: AbortSignal.timeout?.(15000)},
+ );
+ if (!finalizeJson || Object.keys(finalizeJson).length !== 0) {
+ await this.showSplashScreen();
+ await this.dispatchErrorToast('verb_upload_error_generic');
+ this.operations = [];
+ return false;
+ }
+ const intervalDuration = 500;
+ const totalDuration = 5000;
+ let metadata = {};
+ let intervalId;
+ let requestInProgress = false;
+ let metadataExists = false;
+ return new Promise((resolve) => {
+ const handleMetadata = async () => {
+ if (metadata.numPages > this.limits.maxNumPages) {
+ await this.showSplashScreen();
+ await this.dispatchErrorToast('verb_upload_error_max_page_count');
+ resolve(false);
+ return;
+ }
+ resolve(true);
+ }
+ intervalId = setInterval(async () => {
+ if (requestInProgress) return;
+ requestInProgress = true;
+ metadata = await this.serviceHandler.getCallToService(
+ this.acrobatApiConfig.acrobatEndpoint.getMetadata,
+ { id: assetData.id },
+ );
+ requestInProgress = false;
+ if (metadata?.numPages !== undefined) {
+ clearInterval(intervalId);
+ clearTimeout(timeoutId);
+ metadataExists = true;
+ await handleMetadata();
+ }
+ }, intervalDuration);
+ const timeoutId = setTimeout(async () => {
+ clearInterval(intervalId);
+ if (!metadataExists) resolve(true);
+ else await handleMetadata();
+ }, totalDuration);
+ });
+ } catch (e) {
+ await this.showSplashScreen();
+ await this.dispatchErrorToast('verb_upload_error_generic', e?.showError, e?.status);
+ this.operations = [];
+ return false;
+ }
+ }
+
+ async singleFileUpload(file, eventName) {
+ if (file.type !== 'application/pdf') {
+ await this.dispatchErrorToast('verb_upload_error_unsupported_type');
+ return;
+ }
+ if (!file.size) {
+ await this.dispatchErrorToast('verb_upload_error_empty_file');
+ return;
+ }
+ if (file.size > this.limits.maxFileSize) {
+ await this.dispatchErrorToast('verb_upload_error_file_too_large');
+ return;
+ }
+ const fileData = {
+ type: file.type,
+ size: file.size,
+ count: 1,
+ };
+ this.block.dispatchEvent(
+ new CustomEvent(
+ unityConfig.trackAnalyticsEvent,
+ { detail: { event: eventName, data: fileData } },
+ ),
+ );
+ let assetData = null;
+ try {
+ await this.showSplashScreen(true);
+ const blobData = await this.getBlobData(file);
+ const data = {
+ surfaceId: unityConfig.surfaceId,
+ targetProduct: this.workflowCfg.productName,
+ name: file.name,
+ size: file.size,
+ format: file.type,
+ };
+ assetData = await this.serviceHandler.postCallToService(
+ this.acrobatApiConfig.acrobatEndpoint.createAsset,
+ { body: JSON.stringify(data) },
+ );
+ this.block.dispatchEvent(new CustomEvent(unityConfig.trackAnalyticsEvent, { detail: { event: 'uploading' } }));
+ await this.chunkPdf(assetData, blobData, file.type);
+ const operationItem = {
+ assetId: assetData.id,
+ filename: file.name,
+ filesize: file.size,
+ filetype: file.type,
+ };
+ this.operations.push(operationItem);
+ } catch (e) {
+ await this.showSplashScreen();
+ switch (e.status) {
+ case 409:
+ await this.dispatchErrorToast('verb_upload_error_duplicate_asset', e.showError);
+ break;
+ case 401:
+ await this.dispatchErrorToast(e.message === 'notentitled' ? 'verb_upload_error_no_storage_provision' : 'verb_upload_error_generic', e.showError);
+ break;
+ case 403:
+ if (e.message === 'quotaexceeded') await this.dispatchErrorToast('verb_upload_error_max_quota_exceeded', e.showError);
+ else await this.dispatchErrorToast('verb_upload_error_no_storage_provision', e.showError);
+ break;
+ default:
+ await this.dispatchErrorToast('verb_upload_error_generic', e.showError, e.status);
+ break;
+ }
+ return;
+ }
+ const verified = await this.verifyContent(assetData);
+ if (!verified) {
+ this.operations = [];
+ return;
+ }
+ this.block.dispatchEvent(new CustomEvent(unityConfig.trackAnalyticsEvent, { detail: { event: 'uploaded' } }));
+ }
+}
diff --git a/unitylibs/core/workflow/workflow-acrobat/service-handler.js b/unitylibs/core/workflow/workflow-acrobat/service-handler.js
new file mode 100644
index 0000000..5c7c344
--- /dev/null
+++ b/unitylibs/core/workflow/workflow-acrobat/service-handler.js
@@ -0,0 +1,70 @@
+/* eslint-disable eqeqeq */
+/* eslint-disable no-await-in-loop */
+/* eslint-disable class-methods-use-this */
+/* eslint-disable no-restricted-syntax */
+
+import {
+ getGuestAccessToken,
+ unityConfig,
+} from '../../../scripts/utils.js';
+
+export default class ServiceHandler {
+ constructor(renderWidget = false, canvasArea = null) {
+ this.renderWidget = renderWidget;
+ this.canvasArea = canvasArea;
+ }
+
+ getHeaders() {
+ return {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: getGuestAccessToken(),
+ 'x-api-key': unityConfig.apiKey,
+ },
+ };
+ }
+
+ async fetchFromService(url, options) {
+ try {
+ const response = await fetch(url, options);
+ const error = new Error();
+ const contentLength = response.headers.get('Content-Length');
+ if (response.status !== 200) {
+ if (contentLength !== '0') {
+ const resJson = await response.json();
+ ['quotaexceeded', 'notentitled'].forEach((errorMessage) => {
+ if (resJson.reason?.includes(errorMessage)) error.message = errorMessage;
+ });
+ }
+ error.status = response.status;
+ throw error;
+ }
+ if (contentLength === '0') return {};
+ return response.json();
+ } catch (error) {
+ if (error.name === 'TimeoutError' || error.name === 'AbortError') {
+ error.status = 504;
+ }
+ throw error;
+ }
+ }
+
+ async postCallToService(api, options) {
+ const postOpts = {
+ method: 'POST',
+ ...this.getHeaders(),
+ ...options,
+ };
+ return this.fetchFromService(api, postOpts);
+ }
+
+ async getCallToService(api, params) {
+ const getOpts = {
+ method: 'GET',
+ ...this.getHeaders(),
+ };
+ const queryString = new URLSearchParams(params).toString();
+ const url = `${api}?${queryString}`;
+ return this.fetchFromService(url, getOpts);
+ }
+}
diff --git a/unitylibs/core/workflow/workflow-acrobat/target-config.json b/unitylibs/core/workflow/workflow-acrobat/target-config.json
new file mode 100644
index 0000000..0101423
--- /dev/null
+++ b/unitylibs/core/workflow/workflow-acrobat/target-config.json
@@ -0,0 +1,53 @@
+{
+ "verb-widget.fillsign": {
+ "type": "pdf",
+ "selector": ".verb-wrapper",
+ "handler": "render",
+ "renderWidget": false,
+ "source": ".verb-wrapper .verb-container",
+ "target": ".verb-wrapper .verb-container",
+ "limits": {
+ "maxNumFiles": 1,
+ "maxFileSize": 104857600,
+ "maxNumPages": 100
+ },
+ "showSplashScreen": true,
+ "splashScreenConfig": {
+ "fragmentLink": "/dc-shared/fragments/shared-fragments/frictionless/splash-page/splashscreen",
+ "splashScreenParent": "body"
+ },
+ "actionMap": {
+ ".verb-wrapper": [
+ {
+ "actionType": "fillsign"
+ },
+ {
+ "actionType": "continueInApp"
+ }
+ ],
+ "#file-upload": [
+ {
+ "actionType": "fillsign"
+ },
+ {
+ "actionType": "continueInApp"
+ }
+ ]
+ }
+ },
+ "marquee": {
+ "type": "pdf",
+ "selector": ".action-area",
+ "handler": "render",
+ "renderWidget": false,
+ "source": ".action-area .con-button",
+ "target": ".action-area .con-button",
+ "actionMap": {
+ ".action-area .con-button": [
+ {
+ "actionType": "upload"
+ }
+ ]
+ }
+ }
+}
diff --git a/unitylibs/core/workflow/workflow-photoshop/action-binder.js b/unitylibs/core/workflow/workflow-photoshop/action-binder.js
new file mode 100644
index 0000000..b108fc5
--- /dev/null
+++ b/unitylibs/core/workflow/workflow-photoshop/action-binder.js
@@ -0,0 +1,374 @@
+/* eslint-disable eqeqeq */
+/* eslint-disable no-await-in-loop */
+/* eslint-disable class-methods-use-this */
+/* eslint-disable no-restricted-syntax */
+
+import {
+ unityConfig,
+ getUnityLibs,
+ loadImg,
+ loadStyle,
+ createTag,
+ loadSvgs,
+} from '../../../scripts/utils.js';
+
+export default class ActionBinder {
+ constructor(unityEl, workflowCfg, wfblock, canvasArea, actionMap = {}, limits = {}) {
+ this.workflowCfg = workflowCfg;
+ this.block = wfblock;
+ this.actionMap = actionMap;
+ this.canvasArea = canvasArea;
+ this.operations = [];
+ this.progressCircleEl = null;
+ this.errorToastEl = null;
+ this.psApiConfig = this.getPsApiConfig();
+ this.serviceHandler = null;
+ }
+
+ getPsApiConfig() {
+ unityConfig.psEndPoint = {
+ assetUpload: `${unityConfig.apiEndPoint}/asset`,
+ acmpCheck: `${unityConfig.apiEndPoint}/asset/finalize`,
+ removeBackground: `${unityConfig.apiEndPoint}/providers/PhotoshopRemoveBackground`,
+ changeBackground: `${unityConfig.apiEndPoint}/providers/PhotoshopChangeBackground`,
+ };
+ return unityConfig;
+ }
+
+ hideElement(item, b) {
+ if (typeof item === 'string') b?.querySelector(item)?.classList.remove('show');
+ else item?.classList.remove('show');
+ }
+
+ showElement(item, b) {
+ if (typeof item === 'string') b?.querySelector(item)?.classList.add('show');
+ else item?.classList.add('show');
+ }
+
+ toggleElement(item, b) {
+ if (typeof item === 'string') {
+ if (b?.querySelector(item)?.classList.contains('show')) b?.querySelector(item)?.classList.remove('show');
+ else b?.querySelector(item)?.classList.add('show');
+ return;
+ }
+ if (item?.classList.contains('show')) item?.classList.remove('show');
+ else item?.classList.add('show');
+ }
+
+ styleElement(itemSelector, propertyName, propertyValue) {
+ const item = this.block.querySelector(itemSelector);
+ item.style[propertyName] = propertyValue;
+ }
+
+ async psActionMaps(values, e) {
+ const { default: ServiceHandler } = await import(`${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/service-handler.js`);
+ this.serviceHandler = new ServiceHandler(
+ this.workflowCfg.targetCfg.renderWidget,
+ this.canvasArea,
+ );
+ if (this.workflowCfg.targetCfg.renderWidget) {
+ const svgs = this.canvasArea.querySelectorAll('.unity-widget img[src*=".svg"');
+ await loadSvgs(svgs);
+ if (!this.progressCircleEl) {
+ this.progressCircleEl = await this.createSpectrumProgress();
+ this.canvasArea.append(this.progressCircleEl);
+ }
+ }
+ for (const value of values) {
+ switch (true) {
+ case value.actionType == 'hide':
+ value.targets.forEach((t) => this.hideElement(t, this.block));
+ break;
+ case value.actionType == 'setCssStyle':
+ value.targets.forEach((t) => { this.styleElement(t, value.propertyName, value.propertyValue) });
+ break;
+ case value.actionType == 'show':
+ value.targets.forEach((t) => this.showElement(t, this.block));
+ break;
+ case value.actionType == 'toggle':
+ value.targets.forEach((t) => this.toggleElement(t, this.block));
+ break;
+ case value.actionType == 'removebg':
+ await this.removeBackground(value);
+ break;
+ case value.actionType == 'changebg':
+ await this.changeBackground(value);
+ break;
+ case value.actionType == 'imageAdjustment':
+ this.changeAdjustments(e.target.value, value);
+ break;
+ case value.actionType == 'upload':
+ this.userImgUpload(value, e);
+ break;
+ case value.actionType == 'continueInApp':
+ this.continueInApp(value, e);
+ break;
+ case value.actionType == 'refresh':
+ value.target.src = value.sourceSrc;
+ this.operations = [];
+ break;
+ default:
+ break;
+ }
+ }
+ if (this.workflowCfg.targetCfg.renderWidget && this.operations.length) {
+ this.canvasArea.querySelector('.widget-product-icon')?.classList.remove('show');
+ [...this.canvasArea.querySelectorAll('.widget-refresh-button')].forEach((w) => w.classList.add('show'));
+ }
+ }
+
+ initActionListeners() {
+ for (const [key, values] of Object.entries(this.actionMap)) {
+ const el = this.block.querySelector(key);
+ if (!el) return;
+ switch (true) {
+ case el.nodeName === 'A':
+ el.href = '#';
+ el.addEventListener('click', async (e) => {
+ e.preventDefault();
+ await this.psActionMaps(values, e);
+ });
+ break;
+ case el.nodeName === 'INPUT':
+ el.addEventListener('change', async (e) => {
+ await this.psActionMaps(values, e);
+ });
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ getImageBlobData(url) {
+ return new Promise((res, rej) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', url);
+ xhr.responseType = 'blob';
+ xhr.onload = () => {
+ if (xhr.status === 200) res(xhr.response);
+ else rej(xhr.status);
+ };
+ xhr.send();
+ });
+ }
+
+ async uploadImgToUnity(storageUrl, id, blobData, fileType) {
+ const uploadOptions = {
+ method: 'PUT',
+ headers: { 'Content-Type': fileType },
+ body: blobData,
+ };
+ const response = await fetch(storageUrl, uploadOptions);
+ if (response.status != 200) return '';
+ return id;
+ }
+
+ getFileType() {
+ if (this.operations.length) {
+ const lastOperation = this.operations[this.operations.length - 1];
+ if (lastOperation.operationType == 'upload') return lastOperation.fileType;
+ }
+ return 'image/jpeg';
+ }
+
+ async scanImgForSafety(assetId) {
+ const assetData = { assetId, targetProduct: this.workflowCfg.productName };
+ const optionsBody = { body: JSON.stringify(assetData) };
+ try {
+ this.serviceHandler.postCallToService(
+ this.psApiConfig.psEndPoint.acmpCheck,
+ optionsBody,
+ );
+ }
+ catch(e) {
+ // Finalize Api call
+ }
+ }
+
+ async uploadAsset(imgUrl) {
+ const resJson = await this.serviceHandler.postCallToService(
+ this.psApiConfig.psEndPoint.assetUpload,
+ {},
+ );
+ const { id, href } = resJson;
+ const blobData = await this.getImageBlobData(imgUrl);
+ const fileType = this.getFileType();
+ const assetId = await this.uploadImgToUnity(href, id, blobData, fileType);
+ const { origin } = new URL(imgUrl);
+ if ((imgUrl.startsWith('blob:')) || (origin != window.location.origin)) this.scanImgForSafety(assetId);
+ return assetId;
+ }
+
+ userImgUpload(params, e) {
+ this.canvasArea.querySelector('img').style.filter = '';
+ this.operations = [];
+ const file = e.target.files[0];
+ if (!file) return;
+ if (file.size > 400000000) {
+ // unityEl.dispatchEvent(new CustomEvent(errorToastEvent, { detail: { msg: eft } }));
+ // return;
+ }
+ const objUrl = URL.createObjectURL(file);
+ const { target } = params;
+ target.src = objUrl;
+ }
+
+ async removeBackground(params) {
+ const optype = 'removeBackground';
+ let { source, target } = params;
+ if (typeof(source) == 'string') source = this.block.querySelector(source);
+ if (typeof(target) == 'string') target = this.block.querySelector(target);
+ const operationItem = {
+ operationType: optype,
+ sourceAssetId: null,
+ sourceSrc: source.src,
+ assetId: null,
+ assetUrl: null,
+ };
+ let assetId = null;
+ if (this.operations.length) assetId = this.operations[this.operations - 1].assetId;
+ else assetId = await this.uploadAsset(source.src);
+ operationItem.sourceAssetId = assetId;
+ const removeBgOptions = { body: `{"surfaceId":"Unity","assets":[{"id": "${assetId}"}]}` };
+ const resJson = await this.serviceHandler.postCallToService(
+ this.psApiConfig.psEndPoint[optype],
+ removeBgOptions,
+ );
+ const opId = resJson.assetId;
+ operationItem.assetId = opId;
+ operationItem.assetUrl = resJson.outputUrl;
+ target.src = resJson.outputUrl;
+ await loadImg(target);
+ this.operations.push(operationItem);
+ }
+
+ async changeBackground(params) {
+ const opType = 'changeBackground';
+ let { source, target, backgroundSrc} = params;
+ if (typeof(source) == 'string') source = this.block.querySelector(source);
+ if (typeof(target) == 'string') target = this.block.querySelector(target);
+ if (typeof(backgroundSrc) == 'string' && !backgroundSrc.startsWith("http")) backgroundSrc = this.block.querySelector(backgroundSrc);
+ const operationItem = {
+ operationType: opType,
+ sourceSrc: source.src,
+ backgroundSrc: backgroundSrc.src,
+ assetId: null,
+ assetUrl: null,
+ fgId: null,
+ bgId: null,
+ };
+ const fgId = this.operations[this.operations.length - 1].assetId;
+ const bgId = await this.uploadAsset(backgroundSrc);
+ const changeBgOptions = {
+ body: `{
+ "assets": [{ "id": "${fgId}" },{ "id": "${bgId}" }],
+ "metadata": {
+ "foregroundImageId": "${fgId}",
+ "backgroundImageId": "${bgId}"
+ }
+ }`,
+ };
+ const resJson = await this.serviceHandler.postCallToService(
+ this.psApiConfig.psEndPoint[opType],
+ changeBgOptions,
+ );
+ const changeBgId = resJson.assetId;
+ operationItem.assetId = changeBgId;
+ operationItem.assetUrl = resJson.outputUrl;
+ operationItem.fgId = fgId;
+ operationItem.bgId = bgId;
+ target.src = resJson.outputUrl;
+ await loadImg(target);
+ this.operations.push(operationItem);
+ }
+
+ getFilterAttrValue(currFilter, filterName, value) {
+ if (!currFilter) return value;
+ const filterVals = currFilter.split(' ');
+ let hasFilter = false;
+ filterVals.forEach((f, i) => {
+ if (f.match(filterName)) {
+ hasFilter = true;
+ filterVals[i] = value;
+ }
+ });
+ if (!hasFilter) filterVals.push(value);
+ return filterVals.join(' ');
+ }
+
+ changeAdjustments(value, params) {
+ const { filterType, target } = params;
+ const operationItem = {
+ operationType: 'imageAdjustment',
+ adjustmentType: filterType,
+ filterValue: params,
+ };
+ const currFilter = target.style.filter;
+ switch (filterType) {
+ case 'hue':
+ target.style.filter = this.getFilterAttrValue(currFilter, 'hue-rotate', `hue-rotate(${value}deg)`);
+ break;
+ case 'saturation':
+ target.style.filter = this.getFilterAttrValue(currFilter, 'saturate', `saturate(${value}%)`);
+ break;
+ default:
+ break;
+ }
+ this.operations.push(operationItem);
+ }
+
+ continueInApp() {
+ const cOpts = {
+ assetId: null,
+ targetProduct: this.workflowCfg.productName,
+ payload: {
+ finalAssetId: null,
+ operations: [],
+ },
+ };
+ this.operations.forEach((op, i) => {
+ const idx = cOpts.payload.operations.length;
+ if ((i > 0) && (this.operations[i - 1].operationType == op.operationType)) {
+ cOpts.payload.operations[idx - 1][op.adjustmentType] = parseInt(op.filterValue.sliderElem.value, 10);
+ } else {
+ cOpts.payload.operations.push({ name: op.operationType });
+ if (op.sourceAssetId && !cOpts.assetId) cOpts.assetId = op.sourceAssetId;
+ if (op.assetId) cOpts.payload.finalAssetId = op.assetId;
+ if (op.operationType == 'changeBackground') cOpts.payload.operations[idx].assetIds = [op.assetId];
+ if (op.adjustmentType && op.filterValue) {
+ cOpts.payload.operations[idx][op.adjustmentType] = parseInt(op.filterValue.sliderElem.value, 10);
+ }
+ }
+ });
+ this.serviceHandler.postCallToService(
+ this.psApiConfig.connectorApiEndPoint,
+ { body: JSON.stringify(cOpts) },
+ );
+ }
+
+ async createSpectrumProgress() {
+ await new Promise((resolve) => {
+ loadStyle(`${getUnityLibs()}/core/features/progress-circle/progress-circle.css`, resolve);
+ });
+ const pdom = `
+ `;
+ const loader = createTag(
+ 'div',
+ { class: 'progress-circle' },
+ createTag('div', { class: 'spectrum-ProgressCircle spectrum-ProgressCircle--indeterminate' }, pdom),
+ );
+ return loader;
+ }
+}
diff --git a/unitylibs/core/workflow/workflow-photoshop/service-handler.js b/unitylibs/core/workflow/workflow-photoshop/service-handler.js
new file mode 100644
index 0000000..08f2ae1
--- /dev/null
+++ b/unitylibs/core/workflow/workflow-photoshop/service-handler.js
@@ -0,0 +1,75 @@
+/* eslint-disable eqeqeq */
+/* eslint-disable no-await-in-loop */
+/* eslint-disable class-methods-use-this */
+/* eslint-disable no-restricted-syntax */
+
+import {
+ getUnityLibs,
+ createTag,
+ getGuestAccessToken,
+ decorateDefaultLinkAnalytics,
+ unityConfig,
+} from '../../../scripts/utils.js';
+
+export default class ServiceHandler {
+ constructor(renderWidget = false, canvasArea = null) {
+ this.renderWidget = renderWidget;
+ this.errorToastEl = null;
+ this.canvasArea = canvasArea;
+ }
+
+ getHeaders() {
+ return {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: getGuestAccessToken(),
+ 'x-api-key': unityConfig.apiKey,
+ },
+ };
+ }
+
+ async postCallToService(api, options) {
+ const postOpts = {
+ method: 'POST',
+ ...this.getHeaders(),
+ ...options,
+ };
+ try {
+ const response = await fetch(api, postOpts);
+ const resJson = await response.json();
+ return resJson;
+ } catch (err) {
+ if (this.renderWidget) await this.errorToast(err);
+ }
+ return {};
+ }
+
+ async errorToast(e) {
+ console.log(e);
+ if (!this.errorToastEl) {
+ this.errorToastEl = await this.createErrorToast();
+ this.canvasArea.append(this.errorToastEl);
+ }
+ }
+
+ async createErrorToast() {
+ const [alertImg, closeImg] = await Promise.all([
+ fetch(`${getUnityLibs()}/img/icons/alert.svg`).then((res) => res.text()),
+ fetch(`${getUnityLibs()}/img/icons/close.svg`).then((res) => res.text()),
+ ]);
+ const alertContent = createTag('div', { class: 'alert-content' });
+ const errholder = createTag('div', { class: 'alert-holder' }, createTag('div', { class: 'alert-toast' }, alertContent));
+ const alertIcon = createTag('div', { class: 'alert-icon' }, alertImg);
+ const alertText = createTag('div', { class: 'alert-text' }, createTag('p', {}, 'Alert Text'));
+ alertIcon.append(alertText);
+ const alertClose = createTag('a', { class: 'alert-close', href: '#' }, closeImg);
+ alertClose.append(createTag('span', { class: 'alert-close-text' }, 'Close error toast'));
+ alertContent.append(alertIcon, alertClose);
+ alertClose.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.target.closest('.alert-holder').classList.remove('show');
+ });
+ decorateDefaultLinkAnalytics(errholder);
+ return errholder;
+ }
+}
diff --git a/unitylibs/core/workflow/workflow-photoshop/target-config.json b/unitylibs/core/workflow/workflow-photoshop/target-config.json
new file mode 100644
index 0000000..54f1abf
--- /dev/null
+++ b/unitylibs/core/workflow/workflow-photoshop/target-config.json
@@ -0,0 +1,76 @@
+{
+ "marquee.removebg": {
+ "type": "img",
+ "selector": ".asset picture, .image picture",
+ "handler": "render",
+ "renderWidget": false,
+ "source": ".asset picture img",
+ "target": ".asset picture img",
+ "actionMap": {
+ ".action-area .con-button": [
+ {
+ "actionType": "setCssStyle",
+ "propertyName": "opacity",
+ "propertyValue": 0.5,
+ "targets": [".asset picture img, .image picture img"]
+ },
+ {
+ "itemType": "button",
+ "actionType": "removebg",
+ "source": ".asset picture img, .image picture img",
+ "target": ".asset picture img, .image picture img"
+ },
+ {
+ "actionType": "setCssStyle",
+ "propertyName": "opacity",
+ "propertyValue": 1,
+ "targets": [".asset picture img, .image picture img"]
+ }
+ ]
+ }
+ },
+ "brick.brickunityconfig": {
+ "type": "img",
+ "selector": ".brick-media picture",
+ "handler": "render",
+ "renderWidget": false,
+ "source": ".brick-media picture img",
+ "target": ".brick-media picture img",
+ "actionMap": {
+ ".action-area .con-button": [
+ {
+ "actionType": "setCssStyle",
+ "propertyName": "opacity",
+ "propertyValue": 0.5,
+ "targets": [".brick-media picture img"]
+ },
+ {
+ "itemType": "button",
+ "actionType": "removebg",
+ "source": ".brick-media picture img",
+ "target": ".brick-media picture img"
+ },
+ {
+ "actionType": "setCssStyle",
+ "propertyName": "opacity",
+ "propertyValue": 1,
+ "targets": [".brick-media picture img"]
+ }
+ ]
+ }
+ },
+ "marquee": {
+ "type": "img",
+ "selector": ".asset picture, .image picture",
+ "handler": "render",
+ "renderWidget": true,
+ "source": ".asset picture img",
+ "target": ".asset picture img"
+ },
+ "aside": {
+ "type": "img",
+ "selector": ".asset picture, .image picture",
+ "handler": "render",
+ "renderWidget": true
+ }
+}
diff --git a/unitylibs/core/workflow/workflow-photoshop/widget.css b/unitylibs/core/workflow/workflow-photoshop/widget.css
new file mode 100644
index 0000000..8d5a508
--- /dev/null
+++ b/unitylibs/core/workflow/workflow-photoshop/widget.css
@@ -0,0 +1,160 @@
+.unity-enabled .interactive-area .unity-option-area .changebg-options-tray {
+ display: none;
+ flex-direction: row;
+ padding: 8px;
+}
+
+.unity-enabled .interactive-area .unity-option-area .changebg-options-tray.show {
+ display: flex;
+}
+
+.unity-enabled .interactive-area .unity-option-area .changebg-options-tray .changebg-option {
+ border-radius: 5.36px;
+ cursor: pointer;
+ border: 3px solid transparent;
+ overflow: hidden;
+ position: relative;
+ top: 0;
+ left: 0;
+ object-fit: cover;
+ height: 67px;
+}
+
+.unity-enabled .interactive-area .unity-option-area .changebg-options-tray .changebg-option:focus-visible {
+ outline: none;
+}
+
+.unity-enabled .interactive-area .unity-option-area .adjustment-options-tray {
+ display: none;
+ flex-direction: column;
+ gap: 15px;
+ padding: 16px 16px 0;
+}
+
+.unity-enabled .interactive-area .unity-option-area .adjustment-options-tray.show {
+ display: flex;
+}
+
+.unity-enabled .interactive-area .unity-option-area .adjustment-options-tray .adjustment-container {
+ height: 2px;
+ border-radius: 1px;
+ margin: 7px 0;
+ position: relative;
+}
+
+.unity-enabled .interactive-area .unity-option-area .adjustment-options-tray .adjustment-label {
+ font-size: var(--type-body-m-size);
+ line-height: var(--type-body-m-lh);
+}
+
+.unity-enabled .interactive-area .unity-option-area .adjustment-options-tray .adjustment-container.hue {
+ background: linear-gradient(90deg, #E90B03 0%, #EF9100 9%, #FFE003 20.5%, #5AFE00 32.5%, #00FF84 43.5%, #00F1EF 53.5%, #001AFF 66.86%, #8700FC 77.17%, #FD00D6 89.5%, #FF0015 100%);
+}
+
+.unity-enabled .interactive-area .unity-option-area .adjustment-options-tray .adjustment-container.saturation {
+ background: linear-gradient(90deg, #000 0%, #F20000 100%);
+}
+
+.unity-enabled .interactive-area .unity-option-area .adjustment-options-tray .adjustment-slider {
+ width: 100%;
+ opacity: 0;
+ position: absolute;
+ top: -7px;
+ z-index: 1;
+ margin: 0;
+}
+
+.unity-enabled .interactive-area .unity-option-area .adjustment-options-tray .adjustment-slider::-webkit-slider-thumb {
+ height: 20px;
+ width: 20px;
+ position: relative;
+ border-radius: 50%;
+ appearance: none;
+ background: transparent;
+ cursor: grab;
+ z-index: 2;
+}
+
+.unity-enabled .interactive-area .unity-option-area .adjustment-options-tray .adjustment-circle {
+ position: absolute;
+ block-size: 16px;
+ inline-size: 16px;
+ background-color: #A2A2A2;
+ border-radius: 50%;
+ top: -7px;
+ inset-block-start: 50%;
+ inset-inline-start: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.unity-enabled .interactive-area .unity-option-area .adjustment-options-tray .adjustment-circle .analytics-content {
+ display: none;
+}
+
+.unity-enabled .interactive-area .unity-option-area .changebg-options-tray .changebg-option:focus-visible,
+.unity-enabled .interactive-area .unity-option-area .changebg-options-tray .changebg-option:hover {
+ border: 3px solid var(--highlight-blue);
+}
+
+@media screen and (max-width: 600px) {
+ .unity-enabled .interactive-area .unity-option-area .changebg-options-tray {
+ padding: 5px 5px 0;
+ gap: 9px;
+ border-radius: 8px 8px 0 0;
+ }
+
+ .unity-enabled .interactive-area .unity-option-area .changebg-options-tray .changebg-option {
+ width: 100%;
+ }
+
+ .unity-enabled .interactive-area .unity-option-area .changebg-options-tray .changebg-option img {
+ width: 100%;
+ height: 67px;
+ min-height: 67px;
+ max-height: 67px;
+ position: relative;
+ top: 0;
+ left: 0;
+ object-fit: cover;
+ }
+
+ .unity-enabled .interactive-area .unity-action-btn:active .btn-text,
+ .unity-enabled .interactive-area .unity-action-btn:active .btn-icon svg path {
+ color: var(--highlight-blue);
+ fill: var(--highlight-blue);
+ }
+}
+
+@media screen and (min-width: 600px) and (min-width: 1200px) {
+ .aside.split.unity-enabled .split-image img {
+ position: relative;
+ }
+}
+
+@media screen and (min-width: 600px) {
+ .unity-enabled .interactive-area .unity-option-area .changebg-options-tray {
+ gap: 2px;
+ width: fit-content;
+ border-radius: 8px;
+ padding: 5px;
+ }
+
+ .unity-enabled .interactive-area .unity-option-area .changebg-options-tray img {
+ height: 67px;
+ min-height: 67px;
+ max-height: 67px;
+ width: 67px;
+ min-width: 67px;
+ max-width: 67px;
+ }
+
+ .unity-enabled .interactive-area .unity-option-area .adjustment-options-tray {
+ padding: 8px 16px;
+ width: 212px;
+ min-width: 212px;
+ }
+
+ .unity-enabled .interactive-area .unity-option-area .adjustment-options-tray .adjustment-label {
+ font-weight: bold;
+ }
+}
diff --git a/unitylibs/core/workflow/workflow-photoshop/widget.js b/unitylibs/core/workflow/workflow-photoshop/widget.js
new file mode 100644
index 0000000..0eb545f
--- /dev/null
+++ b/unitylibs/core/workflow/workflow-photoshop/widget.js
@@ -0,0 +1,313 @@
+import {
+ createTag,
+ decorateDefaultLinkAnalytics,
+ loadSvgs,
+} from '../../../scripts/utils.js';
+
+export default class UnityWidget {
+ constructor(target, el, workflowCfg) {
+ this.el = el;
+ this.target = target;
+ this.workflowCfg = workflowCfg;
+ this.widget = null;
+ this.actionMap = {};
+ }
+
+ async initWidget() {
+ const iWidget = createTag('div', { class: 'unity-widget' });
+ const unityaa = createTag('div', { class: 'unity-action-area' });
+ const unityoa = createTag('div', { class: 'unity-option-area' });
+ iWidget.append(unityoa, unityaa);
+ const refreshCfg = this.el.querySelector('.icon-product-icon');
+ if (refreshCfg) await this.addRestartOption(refreshCfg.closest('li'), unityaa);
+ this.workflowCfg.enabledFeatures.forEach((f, idx) => {
+ const addClasses = idx === 0 ? 'ps-action-btn show' : 'ps-action-btn';
+ this.addFeatureButtons(
+ f,
+ this.workflowCfg.featureCfg[idx],
+ unityaa,
+ unityoa,
+ addClasses,
+ );
+ });
+ const uploadCfg = this.el.querySelector('.icon-upload');
+ if (uploadCfg) this.addFeatureButtons('upload', uploadCfg.closest('li'), unityaa, unityoa, 'show');
+ const continueInApp = this.el.querySelector('.icon-app-connector');
+ if (continueInApp) this.addFeatureButtons('continue-in-app', continueInApp.closest('li'), unityaa, unityoa, '');
+ this.widget = iWidget;
+ const svgs = iWidget.querySelectorAll('.show img[src*=".svg"');
+ await loadSvgs(svgs);
+ this.target.append(iWidget);
+ decorateDefaultLinkAnalytics(iWidget);
+ return this.actionMap;
+ }
+
+ createActionBtn(btnCfg, btnClass) {
+ const txt = btnCfg.innerText;
+ const img = btnCfg.querySelector('img[src*=".svg"]');
+ const actionBtn = createTag('a', { href: '#', class: `unity-action-btn ${btnClass}` });
+ let swapOrder = false;
+ if (img) {
+ actionBtn.append(createTag('div', { class: 'btn-icon' }, img));
+ if (img.nextSibling?.nodeName == '#text') swapOrder = true;
+ }
+ if (txt) {
+ const btnTxt = createTag('div', { class: 'btn-text' }, txt.split('\n')[0].trim());
+ if (swapOrder) actionBtn.prepend(btnTxt);
+ else actionBtn.append(btnTxt);
+ }
+ return actionBtn;
+ }
+
+ initRefreshActionMap(w) {
+ this.actionMap[w] = [
+ {
+ actionType: 'hide',
+ targets: ['.ps-action-btn.show', '.unity-option-area .show', '.continue-in-app-button'],
+ }, {
+ actionType: 'show',
+ targets: ['.ps-action-btn'],
+ }, {
+ actionType: 'refresh',
+ sourceSrc: this.el.querySelector('img').src,
+ target: this.target.querySelector('img'),
+ },
+ ];
+ }
+
+ async addRestartOption(refreshCfg, unityaa) {
+ const [prodIcon, refreshIcon] = refreshCfg.querySelectorAll('img[src*=".svg"]');
+ const iconHolder = createTag('div', { class: 'widget-product-icon show' }, prodIcon);
+ const refreshAnalyics = createTag('div', { class: 'widget-refresh-text' }, 'Restart');
+ const refreshHolder = createTag('a', { href: '#', class: 'widget-refresh-button' }, refreshIcon);
+ refreshHolder.append(refreshAnalyics);
+ unityaa.append(iconHolder);
+ const mobileRefreshHolder = refreshHolder.cloneNode(true);
+ [refreshHolder, mobileRefreshHolder].forEach((w) => {
+ w.addEventListener('click', () => {
+ this.target.querySelector('img').style.filter = '';
+ iconHolder.classList.add('show');
+ refreshHolder.classList.remove('show');
+ mobileRefreshHolder.classList.remove('show');
+ });
+ });
+ this.initRefreshActionMap('.unity-action-area .widget-refresh-button');
+ this.initRefreshActionMap('.interactive-area > .widget-refresh-button');
+ unityaa.append(refreshHolder);
+ this.target.append(mobileRefreshHolder);
+ }
+
+ addFeatureButtons(featName, authCfg, actionArea, optionArea, addClasses) {
+ const btn = this.createActionBtn(authCfg, `${featName}-button ${addClasses}`);
+ actionArea.append(btn);
+ if (!authCfg.querySelector('ul')) {
+ switch (featName) {
+ case 'removebg':
+ this.initRemoveBgActions(featName, btn);
+ break;
+ case 'upload':
+ const inpel = createTag('input', { class: 'file-upload', type: 'file', accept: 'image/png,image/jpg,image/jpeg', tabIndex: -1 });
+ btn.append(inpel);
+ btn.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') inpel.click();
+ });
+ this.initUploadActions(featName);
+ break;
+ case 'continue-in-app':
+ this.initContinueInAppActions(featName);
+ break;
+ default:
+ break;
+ }
+ return;
+ }
+ this.addFeatureTray(featName, authCfg, optionArea, btn, addClasses);
+ }
+
+ initRemoveBgActions(featName, btn) {
+ this.actionMap[`.${featName}-button`] = [
+ {
+ actionType: 'show',
+ targets: ['.progress-circle'],
+ }, {
+ itemType: 'button',
+ actionType: featName,
+ source: this.target.querySelector('img'),
+ target: this.target.querySelector('img'),
+ }, {
+ actionType: 'show',
+ targets: ['.ps-action-btn.show + .ps-action-btn', '.changebg-options-tray', '.continue-in-app-button'],
+ }, {
+ actionType: 'hide',
+ targets: [btn, '.progress-circle'],
+ },
+ ];
+ }
+
+ initChangeBgActions(key, btn, bgImg, bgSelectorTray) {
+ this.actionMap[key] = [
+ {
+ actionType: 'show',
+ targets: ['.progress-circle'],
+ }, {
+ itemType: 'button',
+ actionType: 'changebg',
+ backgroundSrc: bgImg.src,
+ source: this.target.querySelector('img'),
+ target: this.target.querySelector('img'),
+ }, {
+ actionType: 'show',
+ targets: ['.ps-action-btn.show + .ps-action-btn', '.adjustment-options-tray', '.continue-in-app-button'],
+ }, {
+ actionType: 'hide',
+ targets: [btn, bgSelectorTray, '.progress-circle'],
+ },
+ ];
+ }
+
+ initUploadActions(featName) {
+ this.actionMap[`.${featName}-button .file-upload`] = [
+ {
+ actionType: 'show',
+ targets: ['.progress-circle'],
+ }, {
+ itemType: 'button',
+ actionType: 'upload',
+ assetType: 'img',
+ target: this.target.querySelector('img'),
+ }, {
+ itemType: 'button',
+ actionType: 'removebg',
+ source: this.target.querySelector('img'),
+ target: this.target.querySelector('img'),
+ }, {
+ actionType: 'show',
+ targets: ['.changebg-button'],
+ }, {
+ actionType: 'hide',
+ targets: ['.ps-action-btn.show', '.unity-option-area > div.show', '.progress-circle'],
+ },
+ ];
+ }
+
+ initContinueInAppActions(featName) {
+ this.actionMap[`.${featName}-button`] = [
+ {
+ itemType: 'button',
+ actionType: 'continueInApp',
+ appName: 'Photoshop',
+ },
+ ];
+ }
+
+ addFeatureTray(featName, authCfg, optionArea, btn, addClasses) {
+ switch (featName) {
+ case 'changebg': {
+ const tray = this.addChangeBgTray(btn, authCfg, optionArea, addClasses.indexOf('show') > -1);
+ this.actionMap[`.${featName}-button`] = [
+ {
+ actionType: 'toggle',
+ targets: [tray],
+ },
+ ];
+ break;
+ }
+ case 'slider': {
+ const tray = this.addAdjustmentTray(btn, authCfg, optionArea, addClasses.indexOf('show') > -1);
+ this.actionMap[`.${featName}-button`] = [
+ {
+ actionType: 'toggle',
+ targets: [tray],
+ },
+ ];
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ addChangeBgTray(btn, authCfg, optionArea, isVisible) {
+ const bgSelectorTray = createTag('div', { class: `changebg-options-tray ${isVisible ? 'show' : ''}` });
+ const bgOptions = authCfg.querySelectorAll(':scope ul li');
+ [...bgOptions].forEach((o, num) => {
+ let thumbnail = null;
+ let bgImg = null;
+ [thumbnail, bgImg] = o.querySelectorAll('img');
+ if (!bgImg) bgImg = thumbnail;
+ thumbnail.dataset.backgroundImg = bgImg.src;
+ const optionSelector = `changebg-option option-${num}`;
+ const a = createTag('a', { href: '#', class: optionSelector }, thumbnail);
+ bgSelectorTray.append(a);
+ this.initChangeBgActions(`.changebg-option.option-${num}`, btn, bgImg, bgSelectorTray);
+ a.addEventListener('click', (e) => { e.preventDefault(); });
+ });
+ optionArea.append(bgSelectorTray);
+ return bgSelectorTray;
+ }
+
+ addAdjustmentTray(btn, authCfg, optionArea, isVisible) {
+ const sliderTray = createTag('div', { class: `adjustment-options-tray ${isVisible ? 'show' : ''}` });
+ const sliderOptions = authCfg.querySelectorAll(':scope > ul li');
+ [...sliderOptions].forEach((o) => {
+ let iconName = null;
+ const psAction = o.querySelector(':scope > .icon');
+ [...psAction.classList].forEach((cn) => { if (cn.match('icon-')) iconName = cn; });
+ const [, actionName] = iconName.split('-');
+ switch (actionName) {
+ case 'hue':
+ this.createSlider(sliderTray, 'hue', o.innerText, -180, 180);
+ break;
+ case 'saturation':
+ this.createSlider(sliderTray, 'saturation', o.innerText, 0, 300);
+ break;
+ default:
+ break;
+ }
+ });
+ optionArea.append(sliderTray);
+ return sliderTray;
+ }
+
+ createSlider(tray, propertyName, label, minVal, maxVal) {
+ const actionDiv = createTag('div', { class: 'adjustment-option' });
+ const actionLabel = createTag('label', { class: 'adjustment-label' }, label);
+ const actionSliderDiv = createTag('div', { class: `adjustment-container ${propertyName}` });
+ const actionSliderInput = createTag('input', {
+ type: 'range',
+ min: minVal,
+ max: maxVal,
+ value: (minVal + maxVal) / 2,
+ class: `adjustment-slider ${propertyName}`,
+ });
+ const actionAnalytics = createTag('div', { class: 'analytics-content' }, `Adjust ${label} slider`);
+ const actionSliderCircle = createTag('a', { href: '#', class: `adjustment-circle ${propertyName}` }, actionAnalytics);
+ actionSliderDiv.append(actionSliderInput, actionSliderCircle);
+ actionDiv.append(actionLabel, actionSliderDiv);
+ this.actionMap[`.adjustment-slider.${propertyName}`] = [
+ {
+ actionType: 'show',
+ targets: ['.continue-in-app-button'],
+ }, {
+ itemType: 'slider',
+ actionType: 'imageAdjustment',
+ filterType: propertyName,
+ sliderElem: actionSliderInput,
+ target: this.target.querySelector('img'),
+ },
+ ];
+ actionSliderInput.addEventListener('input', () => {
+ const { value } = actionSliderInput;
+ const centerOffset = (value - minVal) / (maxVal - minVal);
+ const moveCircle = 3 + (centerOffset * 94);
+ actionSliderCircle.style.left = `${moveCircle}%`;
+ });
+ actionSliderInput.addEventListener('change', () => {
+ actionSliderCircle.click();
+ });
+ actionSliderCircle.addEventListener('click', (evt) => {
+ evt.preventDefault();
+ });
+ tray.append(actionDiv);
+ }
+}
diff --git a/unitylibs/core/workflow/workflow.js b/unitylibs/core/workflow/workflow.js
index 3d9ae73..f447114 100644
--- a/unitylibs/core/workflow/workflow.js
+++ b/unitylibs/core/workflow/workflow.js
@@ -6,6 +6,7 @@ import {
unityConfig,
defineDeviceByScreenSize,
getConfig,
+ priorityLoad,
} from '../../scripts/utils.js';
export function getImgSrc(pic) {
@@ -89,6 +90,7 @@ function getWorkFlowInformation(el) {
changebg: { endpoint: 'providers/PhotoshopChangeBackground' },
slider: {},
},
+ 'workflow-acrobat': {},
};
[...el.classList].forEach((cn) => { if (cn.match('workflow-')) wfName = cn; });
if (!wfName || !workflowCfg[wfName]) return [];
@@ -107,25 +109,208 @@ async function initWorkflow(cfg) {
}, { once: true });
}
-export default async function init(el, project = 'unity', unityLibs = '/unitylibs') {
+class WfInitiator {
+ constructor() {
+ this.el = null;
+ this.targetBlock = {};
+ this.unityLibs = '/unityLibs';
+ this.interactiveArea = null;
+ this.project = 'unity';
+ this.targetConfig = {};
+ this.operations = {};
+ this.actionMap = {};
+ }
+
+ async priorityLibFetch(renderWidget, workflowName) {
+ const priorityList = [
+ `${getUnityLibs()}/core/workflow/${workflowName}/action-binder.js`,
+ ];
+ if (renderWidget) {
+ priorityList.push(
+ `${getUnityLibs()}/core/workflow/${workflowName}/widget.css`,
+ `${getUnityLibs()}/core/workflow/${workflowName}/widget.js`,
+ );
+ }
+ await priorityLoad(priorityList);
+ }
+
+ async init(el, project = 'unity', unityLibs = '/unitylibs', langRegion, langCode) {
+ setUnityLibs(unityLibs, project);
+ this.el = el;
+ this.unityLibs = unityLibs;
+ this.project = project;
+ this.enabledFeatures = [];
+ this.workflowCfg = this.getWorkFlowInformation();
+ this.workflowCfg.langRegion = langRegion;
+ this.workflowCfg.langCode = langCode;
+ [this.targetBlock, this.interactiveArea, this.targetConfig] = await this.getTarget();
+ this.getEnabledFeatures();
+ this.callbackMap = {};
+ this.workflowCfg.targetCfg = this.targetConfig;
+ await this.priorityLibFetch(this.targetConfig.renderWidget, this.workflowCfg.name);
+ if (this.targetConfig.renderWidget) {
+ loadStyle(`${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/widget.css`);
+ const { default: UnityWidget } = await import(`${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/widget.js`);
+ this.actionMap = await new UnityWidget(
+ this.interactiveArea,
+ this.el,
+ this.workflowCfg,
+ ).initWidget();
+ } else {
+ this.actionMap = this.targetConfig.actionMap;
+ }
+ this.limits = this.targetConfig.limits;
+ const { default: ActionBinder } = await import(`${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/action-binder.js`);
+ await new ActionBinder(
+ this.el,
+ this.workflowCfg,
+ this.targetBlock,
+ this.interactiveArea,
+ this.actionMap,
+ this.limits,
+ ).initActionListeners();
+ }
+
+ checkRenderStatus(block, selector, res, rej, etime, rtime) {
+ if (etime > 20000) { rej(); return; }
+ if (block.querySelector(selector)) res();
+ else setTimeout(() => this.checkRenderStatus(block, selector, res, rej, etime + rtime), rtime);
+ }
+
+ intEnbReendered(block, selector) {
+ return new Promise((res, rej) => {
+ try {
+ this.checkRenderStatus(block, selector, res, rej, 0, 100);
+ } catch (err) { rej(); }
+ });
+ }
+
+ async getTarget() {
+ const res = await fetch(`${getUnityLibs()}/core/workflow/${this.workflowCfg.name}/target-config.json`);
+ const targetConfig = await res.json();
+ const prevElem = this.el.previousElementSibling;
+ const supportedBlocks = Object.keys(targetConfig);
+ let targetCfg = null;
+ for (let k = 0; k < supportedBlocks.length; k += 1) {
+ const classes = supportedBlocks[k].split('.');
+ let hasAllClasses = true;
+ for (let c of classes) {
+ const hasClass = prevElem.classList.contains(c);
+ const hasChild = prevElem.querySelector(`.${c}`);
+ if (!(hasClass || hasChild)) {
+ hasAllClasses = false;
+ break;
+ }
+ }
+ if (hasAllClasses) {
+ targetCfg = targetConfig[supportedBlocks[k]];
+ break;
+ }
+ }
+ if (!targetCfg) return [null, null, null];
+ await this.intEnbReendered(prevElem, targetCfg.selector);
+ let ta = null;
+ ta = this.createInteractiveArea(prevElem, targetCfg.selector, targetCfg);
+ prevElem.classList.add('unity-enabled');
+ return [prevElem, ta, targetCfg];
+ }
+
+ getImgSrc(pic) {
+ const viewport = defineDeviceByScreenSize();
+ let source = '';
+ if (viewport === 'MOBILE') source = pic.querySelector('source[type="image/webp"]:not([media])');
+ else source = pic.querySelector('source[type="image/webp"][media]');
+ return source ? source.srcset : pic.querySelector('img').src;
+ }
+
+ createInteractiveArea(block, selector, targetCfg) {
+ const iArea = createTag('div', { class: 'interactive-area' });
+ const asset = block.querySelector(selector);
+ if (asset.nodeName === 'PICTURE') {
+ asset.querySelector('img').src = this.getImgSrc(asset);
+ [...asset.querySelectorAll('source')].forEach((s) => s.remove());
+ const newPic = asset.cloneNode(true);
+ this.el.querySelector(':scope > div > div').prepend(newPic);
+ }
+ if (!targetCfg.renderWidget) return null;
+ asset.insertAdjacentElement('beforebegin', iArea);
+ iArea.append(asset);
+ if (this.el.classList.contains('light')) iArea.classList.add('light');
+ else iArea.classList.add('dark');
+ return iArea;
+ }
+
+ getWorkFlowInformation() {
+ let wfName = '';
+ const workflowCfg = {
+ 'workflow-photoshop': {
+ productName: 'Photoshop',
+ sfList: new Set(['removebg', 'changebg', 'slider']),
+ },
+ 'workflow-acrobat': {
+ productName: 'acrobat',
+ sfList: new Set(['fillsign']),
+ }
+ };
+ [...this.el.classList].forEach((cn) => { if (cn.match('workflow-')) wfName = cn; });
+ if (!wfName || !workflowCfg[wfName]) return [];
+ return {
+ name: wfName,
+ productName: workflowCfg[wfName].productName,
+ supportedFeatures: workflowCfg[wfName].sfList,
+ enabledFeatures: [],
+ featureCfg: [],
+ errors: {},
+ };
+ }
+
+ getEnabledFeatures() {
+ const { supportedFeatures } = this.workflowCfg;
+ const configuredFeatures = this.el.querySelectorAll(':scope > div > div > ul > li > span.icon');
+ configuredFeatures.forEach((cf) => {
+ const cfName = [...cf.classList].find((cn) => cn.match('icon-'));
+ if (!cfName) return;
+ const fn = cfName.split('-')[1];
+ if (supportedFeatures.has(fn)) {
+ this.workflowCfg.enabledFeatures.push(fn);
+ this.workflowCfg.featureCfg.push(cf.closest('li'));
+ } else if (fn.includes('error')) {
+ this.workflowCfg.errors[fn] = cf.closest('li').innerText;
+ }
+ });
+ }
+}
+
+
+export default async function init(el, project = 'unity', unityLibs = '/unitylibs', unityVersion = 'v1', langRegion = 'us', langCode = 'en') {
+ const uv = new URLSearchParams(window.location.search).get('unityversion') || unityVersion;
const { imsClientId } = getConfig();
if (imsClientId) unityConfig.apiKey = imsClientId;
setUnityLibs(unityLibs, project);
- const [targetBlock, unityWidget] = await getTargetArea(el);
- if (!targetBlock) return;
- const [wfName, wfDetail] = getWorkFlowInformation(el);
- if (!wfName || !wfDetail) return;
- const enabledFeatures = getEnabledFeatures(el, wfDetail);
- if (!enabledFeatures) return;
- const wfConfig = {
- unityEl: el,
- targetEl: targetBlock,
- unityWidget,
- wfName,
- wfDetail,
- enabledFeatures,
- uploadState: { },
- ...unityConfig,
- };
- await initWorkflow(wfConfig);
+ switch (uv) {
+ case 'v1':
+ const [targetBlock, unityWidget] = await getTargetArea(el);
+ if (!targetBlock) return;
+ const [wfName, wfDetail] = getWorkFlowInformation(el);
+ if (!wfName || !wfDetail) return;
+ const enabledFeatures = getEnabledFeatures(el, wfDetail);
+ if (!enabledFeatures) return;
+ const wfConfig = {
+ unityEl: el,
+ targetEl: targetBlock,
+ unityWidget,
+ wfName,
+ wfDetail,
+ enabledFeatures,
+ uploadState: { },
+ ...unityConfig,
+ };
+ await initWorkflow(wfConfig);
+ break;
+ case 'v2':
+ await new WfInitiator().init(el, project, unityLibs, langRegion, langCode);
+ break;
+ default:
+ break;
+ }
}
diff --git a/unitylibs/scripts/errors.js b/unitylibs/scripts/errors.js
new file mode 100644
index 0000000..148da01
--- /dev/null
+++ b/unitylibs/scripts/errors.js
@@ -0,0 +1,45 @@
+import { getConfig } from './utils.js';
+
+function createErrorMap(errorList) {
+ const errorMap = {};
+ if (Array.isArray(errorList)) {
+ errorList.forEach((error) => {
+ if (error['title.single.code']) {
+ errorMap[error['title.single.code']] = error['title.single.message'];
+ }
+ if (error['title.multi.code']) {
+ errorMap[error['title.multi.code']] = error['title.multi.message'];
+ }
+ if (error['body.single.code']) {
+ errorMap[error['body.single.code']] = error['body.single.message'];
+ }
+ if (error['body.multi.code']) {
+ errorMap[error['body.multi.code']] = error['body.multi.message'];
+ }
+ });
+ }
+ return errorMap;
+}
+
+async function loadErrorMessages(verb) {
+ const { locale } = getConfig();
+ const baseUrl = 'https://main--unity--adobecom.hlx.live';
+ const errorFile = locale.prefix && locale.prefix !== '/'
+ ? `${baseUrl}/${locale.prefix}/configs/errors/${verb}.json`
+ : `${baseUrl}/unity/configs/errors/${verb}.json`;
+ const errorRes = await fetch(errorFile);
+ if (!errorRes.ok) {
+ throw new Error('Failed to fetch error messages.');
+ }
+ const errorJson = await errorRes.json();
+ window.uem = createErrorMap(errorJson?.data);
+}
+
+export default async function getError(verb, code) {
+ try {
+ if (!window.uem) await loadErrorMessages(verb);
+ return window.uem?.[code];
+ } catch (e) {
+ return '';
+ }
+}
diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js
index 6e102e2..643bb23 100644
--- a/unitylibs/scripts/utils.js
+++ b/unitylibs/scripts/utils.js
@@ -29,8 +29,12 @@ export function decorateArea(area = document) {}
const miloLibs = setLibs('/libs');
-const { createTag, getConfig, loadStyle } = await import(`${miloLibs}/utils/utils.js`);
-export { createTag, loadStyle, getConfig };
+const {
+ createTag, getConfig, loadStyle, loadLink, loadScript, localizeLink, loadArea,
+} = await import(`${miloLibs}/utils/utils.js`);
+export {
+ createTag, loadStyle, getConfig, loadLink, loadScript, localizeLink, loadArea,
+};
const { decorateDefaultLinkAnalytics } = await import(`${miloLibs}/martech/attributes.js`);
export { decorateDefaultLinkAnalytics };
@@ -71,6 +75,22 @@ export async function loadSvg(src) {
}
}
+export async function loadSvgs(svgs) {
+ const promiseArr = [];
+ [...svgs].forEach((svg) => {
+ promiseArr.push(
+ fetch(svg.src)
+ .then((res) => {
+ if (res.ok) return res.text();
+ else throw new Error('Could not fetch SVG');
+ })
+ .then((txt) => { svg.parentElement.innerHTML = txt; })
+ .catch((e) => { svg.remove(); }),
+ );
+ });
+ await Promise.all(promiseArr);
+}
+
export function loadImg(img) {
return new Promise((res) => {
img.loading = 'eager';
@@ -104,6 +124,22 @@ export async function createActionBtn(btnCfg, btnClass, iconAsImg = false, swapO
return actionBtn;
}
+export async function priorityLoad(parr) {
+ const promiseArr = [];
+ parr.forEach((p) => {
+ if (p.endsWith('.js')) {
+ const pr = loadScript(p, 'module', { mode: 'async' });
+ promiseArr.push(pr);
+ } else if (p.endsWith('.css')) {
+ const pr = new Promise((res) => { loadLink(p, { rel: 'stylesheet', callback: res }); });
+ promiseArr.push(pr);
+ } else {
+ promiseArr.push(fetch(p));
+ }
+ });
+ await Promise.all(promiseArr);
+}
+
async function createErrorToast() {
const [alertImg, closeImg] = await Promise.all([
fetch(`${getUnityLibs()}/img/icons/alert.svg`).then((res) => res.text()),
@@ -179,6 +215,9 @@ export const unityConfig = (() => {
apiKey: 'leo',
refreshWidgetEvent: 'unity:refresh-widget',
interactiveSwitchEvent: 'unity:interactive-switch',
+ trackAnalyticsEvent: 'unity:track-analytics',
+ errorToastEvent: 'unity:show-error-toast',
+ surfaceId: 'unity',
};
const cfg = {
prod: {