diff --git a/studio/src/constants.js b/studio/src/constants.js
index aa7d2e2e..5303e0ff 100644
--- a/studio/src/constants.js
+++ b/studio/src/constants.js
@@ -25,3 +25,5 @@ export const ANALYTICS_LINK_IDS = [
// TODO remove these?
export const EVENT_CHANGE = 'change';
export const EVENT_INPUT = 'input';
+
+export const EVENT_OST_SELECT = 'ost-select';
diff --git a/studio/src/editor-panel.js b/studio/src/editor-panel.js
index 20eb333c..5f67a59d 100644
--- a/studio/src/editor-panel.js
+++ b/studio/src/editor-panel.js
@@ -166,6 +166,7 @@ export default class EditorPanel extends LitElement {
handleKeyDown(event) {
if (event.code === 'Escape') this.close();
+ if (!event.ctrlKey) return;
if (event.code === 'ArrowLeft' && event.shiftKey)
this.updatePosition('left');
if (event.code === 'ArrowRight' && event.shiftKey)
diff --git a/studio/src/maslib.js b/studio/src/maslib.js
index 1b96dd8d..3e05e32c 100644
--- a/studio/src/maslib.js
+++ b/studio/src/maslib.js
@@ -11,7 +11,7 @@ const miloLibs = getMiloLibs();
const injectMasLib = () => {
const script = document.createElement('script');
- script.setAttribute('src', `${miloLibs}/libs/deps/mas/mas.js`);
+ script.setAttribute('src', `${miloLibs}/libs/features/mas/dist/mas.js`);
script.setAttribute('type', 'module');
document.head.prepend(script);
};
diff --git a/studio/src/rte/ost.js b/studio/src/rte/ost.js
index 42c1f469..8d07ecdb 100644
--- a/studio/src/rte/ost.js
+++ b/studio/src/rte/ost.js
@@ -1,9 +1,14 @@
import { html } from 'lit';
-import { CHECKOUT_CTA_TEXTS } from '../constants.js';
+import { CHECKOUT_CTA_TEXTS, EVENT_OST_SELECT } from '../constants.js';
-let ostRoot;
+let ostRoot = document.getElementById('ost');
let closeFunction;
+if (!ostRoot) {
+ ostRoot = document.createElement('div');
+ document.body.appendChild(ostRoot);
+}
+
export const ostDefaults = {
aosApiKey: 'wcms-commerce-ims-user-prod',
checkoutClientId: 'creative',
@@ -17,7 +22,7 @@ export const ostDefaults = {
displayRecurrence: true,
displayPerUnit: false,
displayTax: false,
- displayOldPrice: false,
+ displayOldPrice: true,
forceTaxExclusive: true,
},
wcsApiKey: 'wcms-commerce-ims-ro-user-cc',
@@ -86,6 +91,7 @@ const OST_OPTION_ATTRIBUTE_MAPPING = {
displayTax: 'data-display-tax',
forceTaxExclusive: 'data-tax-exclusive',
isPerpetual: 'data-perpetual',
+ storedPromoOverride: 'data-promotion-code',
wcsOsi: 'data-wcs-osi',
workflow: 'data-checkout-workflow',
workflowStep: 'data-checkout-workflow-step',
@@ -99,7 +105,7 @@ export const OST_OPTION_ATTRIBUTE_MAPPING_REVERSE = Object.fromEntries(
);
const OST_OPTION_DEFAULTS = {
- displayOldPrice: false,
+ displayOldPrice: true,
displayPerUnit: false,
displayRecurrence: true,
displayTax: false,
@@ -145,7 +151,7 @@ export function onSelect(offerSelectorId, type, offer, options, promoOverride) {
}
ostRoot.dispatchEvent(
- new CustomEvent('use', {
+ new CustomEvent(EVENT_OST_SELECT, {
detail: attributes,
bubbles: true,
}),
@@ -163,10 +169,6 @@ export function getOffferSelectorTool() {
}
export function openOfferSelectorTool(offerElement) {
- if (!ostRoot) {
- ostRoot = document.createElement('div');
- document.body.appendChild(ostRoot);
- }
let searchOfferSelectorId;
const aosAccessToken =
localStorage.getItem('masAccessToken') ?? window.adobeid.authorize();
@@ -196,12 +198,15 @@ export function openOfferSelectorTool(offerElement) {
}
});
- ['promotionCode', 'checkoutType', 'workflowStep', 'country'].forEach(
- (key) => {
- const value = offerSelectorPlaceholderOptions[key];
- if (value) searchParameters.append(key, value);
- },
- );
+ [
+ 'storedPromoOverride',
+ 'checkoutType',
+ 'workflowStep',
+ 'country',
+ ].forEach((key) => {
+ const value = offerSelectorPlaceholderOptions[key];
+ if (value) searchParameters.append(key, value);
+ });
}
ostRoot.style.display = 'block';
closeFunction = window.ost.openOfferSelectorTool({
diff --git a/studio/src/rte/rte-field.js b/studio/src/rte/rte-field.js
index 66692de0..471fb1e6 100644
--- a/studio/src/rte/rte-field.js
+++ b/studio/src/rte/rte-field.js
@@ -13,6 +13,8 @@ import {
} from './ost.js';
import prosemirrorStyles from './prosemirror.css.js';
+import { EVENT_OST_SELECT } from '../constants.js';
+import throttle from '../utils/throttle.js';
const CUSTOM_ELEMENT_CHECKOUT_LINK = 'checkout-link';
const CUSTOM_ELEMENT_INLINE_PRICE = 'inline-price';
@@ -85,6 +87,8 @@ class RteField extends LitElement {
readOnly: { type: Boolean, attribute: 'readonly' },
showLinkEditor: { type: Boolean, state: true },
defaultLinkStyle: { type: String, attribute: 'default-link-style' },
+ maxLength: { type: Number, attribute: 'max-length' },
+ length: { type: Number, state: true },
};
static get styles() {
@@ -110,6 +114,10 @@ class RteField extends LitElement {
background-color: var(--spectrum-global-color-gray-50);
}
+ .exceeded {
+ color: var(--spectrum-global-color-red-700);
+ }
+
rte-link-editor {
display: contents;
}
@@ -120,6 +128,10 @@ class RteField extends LitElement {
align-content: center;
}
+ .price-unit-type:before {
+ content: ' ';
+ }
+
a.accent,
a.primary-outline,
a.secondary,
@@ -207,6 +219,8 @@ class RteField extends LitElement {
this.showLinkEditor = false;
this.inline = false;
this.link = false;
+ this.maxLength = 70;
+ this.length = 0;
this.#boundHandlers = {
escKey: this.#handleEscKey.bind(this),
ostEvent: this.#handleOstEvent.bind(this),
@@ -214,6 +228,7 @@ class RteField extends LitElement {
blur: this.#handleBlur.bind(this),
focus: this.#handleFocus.bind(this),
doubleClickOn: this.#handleDoubleClickOn.bind(this),
+ updateLength: throttle(this.#updateLength.bind(this), 100),
};
}
@@ -227,7 +242,14 @@ class RteField extends LitElement {
document.addEventListener('keydown', this.#boundHandlers.escKey, {
capture: true,
});
- document.addEventListener('use', this.#boundHandlers.ostEvent);
+ document.addEventListener(
+ EVENT_OST_SELECT,
+ this.#boundHandlers.ostEvent,
+ );
+ this.updateLengthInterval = setInterval(
+ this.#boundHandlers.updateLength,
+ 1000,
+ );
}
disconnectedCallback() {
@@ -235,8 +257,12 @@ class RteField extends LitElement {
document.removeEventListener('keydown', this.#boundHandlers.escKey, {
capture: true,
});
- document.removeEventListener('use', this.#boundHandlers.ostEvent);
+ document.removeEventListener(
+ EVENT_OST_SELECT,
+ this.#boundHandlers.ostEvent,
+ );
this.editorView?.destroy();
+ clearInterval(this.updateLengthInterval);
}
#initEditorSchema() {
@@ -468,6 +494,7 @@ class RteField extends LitElement {
this.editorView.updateState(newState);
if (newState.doc) {
+ this.#boundHandlers.updateLength();
const value = this.#serializeContent(newState);
// skip change event during initialization
const isFirstChange = this.value === null;
@@ -629,7 +656,7 @@ class RteField extends LitElement {
: state.schema.nodes.link; // Fixed to use 'link' node type
const mergedAttributes = {
- ...(selection.node?.attrs ?? {}),
+ class: selection.node?.attrs.class,
...attributes,
};
@@ -669,6 +696,10 @@ class RteField extends LitElement {
!selection.node.attrs['data-wcs-osi'];
}
+ #updateLength() {
+ this.length = this.editorView.dom.innerText.length;
+ }
+
async openLinkEditor() {
const attrs = this.#getLinkAttrs();
this.showLinkEditor = true;
@@ -788,12 +819,18 @@ class RteField extends LitElement {
}
render() {
+ const lengthExceeded = this.length > this.maxLength;
return html`
+ ${this.length}/${this.maxLength} +
${this.linkEditor} `; } diff --git a/studio/src/utils/throttle.js b/studio/src/utils/throttle.js new file mode 100644 index 00000000..994101cb --- /dev/null +++ b/studio/src/utils/throttle.js @@ -0,0 +1,24 @@ +function throttle(func, limit) { + let lastFunc; + let lastRan; + return function (...args) { + const context = this; + if (!lastRan) { + func.apply(context, args); + lastRan = Date.now(); + } else { + clearTimeout(lastFunc); + lastFunc = setTimeout( + function () { + if (Date.now() - lastRan >= limit) { + func.apply(context, args); + lastRan = Date.now(); + } + }, + limit - (Date.now() - lastRan), + ); + } + }; +} + +export default throttle; diff --git a/studio/test/rte/ost.test.js b/studio/test/rte/ost.test.js new file mode 100644 index 00000000..ff966715 --- /dev/null +++ b/studio/test/rte/ost.test.js @@ -0,0 +1,93 @@ +import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; + +import { EVENT_OST_SELECT } from '../../src/constants.js'; + +describe('onSelect', () => { + let dispatchEventStub; + let ostRoot; + let onSelect; + + before(async () => { + ostRoot = document.createElement('div'); + ostRoot.id = 'ost'; + document.body.appendChild(ostRoot); + ({ onSelect } = await import('../../src/rte/ost.js')); + dispatchEventStub = sinon.stub(ostRoot, 'dispatchEvent'); + }); + + beforeEach(() => { + dispatchEventStub.reset(); + }); + + it('should dispatch an event with correct attributes for price', () => { + const offerSelectorId = 'test-id'; + const type = 'price'; + const offer = {}; + const options = { + displayOldPrice: false, + }; + const promoOverride = 'PROMO123'; + + onSelect(offerSelectorId, type, offer, options, promoOverride); + + const expectedAttributes = { + 'data-wcs-osi': offerSelectorId, + 'data-template': type, + is: 'inline-price', + 'data-display-old-price': false, + 'data-promotion-code': promoOverride, + }; + + expect(dispatchEventStub.calledOnce).to.be.true; + const event = dispatchEventStub.getCall(0).args[0]; + expect(event.type).to.equal(EVENT_OST_SELECT); + expect(event.detail).to.deep.equal(expectedAttributes); + }); + + it('should dispatch an event with correct attributes for checkout link', () => { + const offerSelectorId = 'test-id'; + const type = 'checkoutUrl'; + const offer = {}; + const options = { + ctaText: 'buy-now', + }; + const promoOverride = null; + + onSelect(offerSelectorId, type, offer, options, promoOverride); + + const expectedAttributes = { + 'data-wcs-osi': offerSelectorId, + 'data-template': type, + is: 'checkout-link', + text: 'Buy now', + 'data-analytics-id': 'buy-now', + }; + + expect(dispatchEventStub.calledOnce).to.be.true; + const event = dispatchEventStub.getCall(0).args[0]; + expect(event.type).to.equal(EVENT_OST_SELECT); + expect(event.detail).to.deep.equal(expectedAttributes); + }); + + it('should not include promo code if not provided for price', () => { + const offerSelectorId = 'test-id'; + const type = 'price'; + const offer = {}; + const options = {}; + const promoOverride = null; + + onSelect(offerSelectorId, type, offer, options, promoOverride); + + const expectedAttributes = { + 'data-wcs-osi': offerSelectorId, + 'data-template': type, + is: 'inline-price', + }; + + expect(dispatchEventStub.calledOnce).to.be.true; + const event = dispatchEventStub.getCall(0).args[0]; + expect(event.type).to.equal(EVENT_OST_SELECT); + expect(event.detail).to.deep.equal(expectedAttributes); + }); +}); diff --git a/studio/test/rte/rte-field.test.html b/studio/test/rte/rte-field.test.html index 064879d6..834fe871 100644 --- a/studio/test/rte/rte-field.test.html +++ b/studio/test/rte/rte-field.test.html @@ -38,7 +38,7 @@ import '../../src/swc.js'; import '../../src/rte/rte-field.js'; import '../../src/rte/rte-link-editor.js'; - import '@adobecom/milo/libs/deps/mas/mas.js'; + import '@adobecom/milo/libs/features/mas/dist/mas.js'; import { mockFetch } from '@adobecom/milo/libs/features/mas/test/mocks/fetch.js'; import { withWcs } from '@adobecom/milo/libs/features/mas/test/mocks/wcs.js'; @@ -276,6 +276,17 @@ ); }); + it('should display char counter in red when max length is exceeded', async function() { + const rte = await createFromTemplate('rte-maxlength', this.test.title); + const span = rte.shadowRoot.querySelector('#counter').firstElementChild; + await delay(100); + expect(span.classList.contains('exceeded')).to.be.false; + rte.setAttribute('max-length', 40); + await delay(100); + expect(span.classList.contains('exceeded')).to.be.true; + }); + + it.skip('dev', async function () { const rte = await createFromTemplate( 'rte-dev', @@ -399,6 +410,11 @@ > + +