Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-162020: Add char counter to RTE field #126

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions studio/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
1 change: 1 addition & 0 deletions studio/src/editor-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion studio/src/maslib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
35 changes: 20 additions & 15 deletions studio/src/rte/ost.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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,
Expand Down Expand Up @@ -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,
}),
Expand All @@ -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();
Expand Down Expand Up @@ -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({
Expand Down
43 changes: 40 additions & 3 deletions studio/src/rte/rte-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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() {
Expand All @@ -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;
}
Expand All @@ -120,6 +128,10 @@ class RteField extends LitElement {
align-content: center;
}

.price-unit-type:before {
content: ' ';
}

a.accent,
a.primary-outline,
a.secondary,
Expand Down Expand Up @@ -207,13 +219,16 @@ 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),
linkSave: this.#handleLinkSave.bind(this),
blur: this.#handleBlur.bind(this),
focus: this.#handleFocus.bind(this),
doubleClickOn: this.#handleDoubleClickOn.bind(this),
updateLength: throttle(this.#updateLength.bind(this), 100),
};
}

Expand All @@ -227,16 +242,27 @@ 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() {
super.disconnectedCallback();
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() {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -788,12 +819,18 @@ class RteField extends LitElement {
}

render() {
const lengthExceeded = this.length > this.maxLength;
return html`
<sp-action-group size="m" aria-label="RTE toolbar actions">
${this.#formatButtons} ${this.#linkEditorButton}
${this.#unlinkEditorButton} ${this.#offerSelectorToolButton}
</sp-action-group>
<div id="editor"></div>
<p id="counter">
<span class="${lengthExceeded ? 'exceeded' : ''}"
>${this.length}</span
>/${this.maxLength}
</p>
${this.linkEditor}
`;
}
Expand Down
24 changes: 24 additions & 0 deletions studio/src/utils/throttle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function throttle(func, limit) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small introduction comment?

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;
93 changes: 93 additions & 0 deletions studio/test/rte/ost.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
18 changes: 17 additions & 1 deletion studio/test/rte/rte-field.test.html
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -399,6 +410,11 @@
>
</rte-field>
</template>
<template id="rte-maxlength">
<rte-field>
this is a text that exceeds the 40 characters limit
</rte-field>
</template>
<template id="rte-dev">
<div>
<rte-field link>
Expand Down
Loading