@financial-times/n-myft-ui 30.1.0 → 30.2.0

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/n-myft-ui",
3
- "version": "30.1.0",
3
+ "version": "30.2.0",
4
4
  "description": "Client side component for interaction with myft",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -96,7 +96,6 @@
96
96
  "@financial-times/o-overlay": "^4.0.0",
97
97
  "@financial-times/o-spacing": "^3.0.0",
98
98
  "@financial-times/o-tooltip": "^5.2.4",
99
- "@financial-times/o-topper": "^5.2.3",
100
99
  "n-ui-foundations": "^9.0.0"
101
100
  },
102
101
  "dependencies": {
@@ -0,0 +1,22 @@
1
+ const expect = require('chai').expect;
2
+ const escapeText = require('../../myft/ui/lib/escape-text');
3
+
4
+ describe('escapeText', function () {
5
+ it('should escape an anchor tag', () => {
6
+ const input = '<a onclick="console.log(\'muahaha\')"></a>';
7
+ const expectedOutput = '&lt;a onclick="console.log(\'muahaha\')"&gt;&lt;/a&gt;';
8
+ expect(escapeText(input)).to.equal(expectedOutput);
9
+ });
10
+
11
+ it('should escape ampersands', () => {
12
+ const input = '&lt;a onclick="console.log(\'muahaha\')"&gt;&lt;/a&gt;';
13
+ const expectedOutput = '&amp;lt;a onclick="console.log(\'muahaha\')"&amp;gt;&amp;lt;/a&amp;gt;';
14
+ expect(escapeText(input)).to.equal(expectedOutput);
15
+ });
16
+
17
+ it('should return empty string for inputs that are not strings', () => {
18
+ const input = true;
19
+ const expectedOutput = '';
20
+ expect(escapeText(input)).to.equal(expectedOutput);
21
+ });
22
+ });
@@ -1,512 +0,0 @@
1
- import Overlay from '@financial-times/o-overlay';
2
- import myFtClient from 'next-myft-client';
3
- import { uuid } from 'n-ui-foundations';
4
- import getToken from './lib/get-csrf-token';
5
- import isMobile from './lib/is-mobile';
6
- import stringToHTMLElement from './lib/convert-string-to-html-element';
7
-
8
- const csrfToken = getToken();
9
-
10
- let lists = [];
11
- let haveLoadedLists = false;
12
- let createListOverlay;
13
- let scrolledOnOpen;
14
- let listOverlayBottom;
15
-
16
- export default async function openSaveArticleToListVariant (contentId, options = {}) {
17
- const { name, modal = false } = options;
18
-
19
- function createList (newList, cb) {
20
- if(!newList || !newList.name) {
21
- return restoreContent();
22
- }
23
-
24
- myFtClient.add('user', null, 'created', 'list', uuid(), { name: newList.name, token: csrfToken, isPublic: newList.isPublic })
25
- .then(detail => {
26
- myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((data) => {
27
- const createdList = { name: newList.name, uuid: data.actorId, checked: true, isPublic: !!newList.isPublic };
28
- lists.unshift(createdList);
29
- const announceListContainer = document.querySelector('.myft-ui-create-list-variant-announcement');
30
- announceListContainer.textContent = `${newList.name} created`;
31
- cb(contentId, createdList);
32
- });
33
- })
34
- .catch(() => {
35
- return restoreContent();
36
- });
37
- }
38
-
39
- function addToList (list, cb) {
40
- if(!list) {
41
- return;
42
- }
43
-
44
- myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((addedList) => {
45
- cb();
46
- triggerAddToListEvent(contentId, addedList.actorId);
47
- });
48
- }
49
-
50
- function removeFromList (list, cb) {
51
- if(!list) {
52
- return;
53
- }
54
-
55
- myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((removedList) => {
56
- cb();
57
- triggerRemoveFromListEvent(contentId, removedList.actorId);
58
- });
59
- }
60
-
61
- function restoreContent () {
62
- if (!lists.length) attachDescription();
63
- refreshListElement();
64
- showListElement();
65
- return restoreFormHandler();
66
- }
67
-
68
- if (!haveLoadedLists) {
69
- lists = await getLists(contentId);
70
- haveLoadedLists = true;
71
- }
72
-
73
- const overlays = Overlay.getOverlays();
74
- const existingOverlay = overlays[name];
75
- if (existingOverlay) {
76
- existingOverlay.destroy();
77
- }
78
-
79
- const headingElement = HeadingElement();
80
- let [contentElement, removeDescription, attachDescription, restoreFormHandler] = ContentElement(!lists.length, openFormHandler);
81
- const [listElement, refreshListElement, hideListElement, showListElement] = ListsElement(lists, addToList, removeFromList);
82
-
83
- createListOverlay = new Overlay(name, {
84
- modal,
85
- html: contentElement,
86
- heading: { title: headingElement.outerHTML },
87
- parentnode: isMobile() ? '.o-share--horizontal' : '.o-share--vertical',
88
- class: 'myft-ui-create-list-variant',
89
- });
90
-
91
- function outsideClickHandler (e) {
92
- const overlayContent = document.querySelector('.o-overlay__content');
93
- const overlayContainer = document.querySelector('.o-overlay');
94
- // we don't want to close the overlay if the click happened inside the
95
- // overlay, except if the click happened on the overlay close button
96
- const isTargetInsideOverlay = overlayContainer.contains(e.target) && !e.target.classList.contains('o-overlay__close');
97
- if(createListOverlay.visible && (!overlayContent || !isTargetInsideOverlay)) {
98
- createListOverlay.close();
99
- }
100
- }
101
-
102
- function onFormCancel () {
103
- showListElement();
104
- restoreFormHandler();
105
- }
106
-
107
- function onFormListCreated () {
108
- refreshListElement();
109
- showListElement();
110
- restoreFormHandler();
111
- }
112
-
113
- function openFormHandler () {
114
- hideListElement();
115
- const formElement = FormElement(createList, attachDescription, onFormListCreated, onFormCancel, modal);
116
- const overlayContent = document.querySelector('.o-overlay__content');
117
- removeDescription();
118
- overlayContent.insertAdjacentElement('beforeend', formElement);
119
- }
120
-
121
- createListOverlay.open();
122
- scrolledOnOpen = window.scrollY;
123
-
124
- const scrollHandler = getScrollHandler(createListOverlay.wrapper);
125
- const resizeHandler = getResizeHandler(createListOverlay.wrapper);
126
-
127
- createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
128
- const overlayContent = document.querySelector('.o-overlay__content');
129
- overlayContent.insertAdjacentElement('afterbegin', listElement);
130
- if (!lists.length) {
131
- hideListElement();
132
- }
133
-
134
- if (!modal) {
135
- positionOverlay(data.currentTarget);
136
-
137
- window.addEventListener('oViewport.resize', resizeHandler);
138
- window.addEventListener('scroll', scrollHandler);
139
- }
140
-
141
- listOverlayBottom = document.querySelector('.myft-ui-create-list-variant').getBoundingClientRect().bottom;
142
-
143
- restoreFormHandler();
144
-
145
- document.body.addEventListener('click', outsideClickHandler);
146
-
147
-
148
- });
149
-
150
- createListOverlay.wrapper.addEventListener('oOverlay.destroy', () => {
151
- window.removeEventListener('scroll', scrollHandler);
152
-
153
- window.removeEventListener('oViewport.resize', resizeHandler);
154
-
155
- document.body.removeEventListener('click', outsideClickHandler);
156
- });
157
- }
158
-
159
- function getScrollHandler (target) {
160
- return realignOverlay(window.scrollY, target);
161
- }
162
-
163
- function getResizeHandler (target) {
164
- return function resizeHandler () {
165
- positionOverlay(target);
166
- };
167
- }
168
-
169
- function FormElement (createList, attachDescription, onListCreated, onCancel, modal=false) {
170
- const formString = `
171
- <form class="myft-ui-create-list-variant-form">
172
- <label class="myft-ui-create-list-variant-form-name o-forms-field">
173
- <span class="o-forms-input o-forms-input--text">
174
- <input class="myft-ui-create-list-variant-text" type="text" name="list-name">
175
- <div class="myft-ui-create-list-variant-label">List name</div>
176
- </span>
177
- </label>
178
-
179
- <div class="myft-ui-create-list-variant-form-public o-forms-field" role="group">
180
- <span class="o-forms-input o-forms-input--toggle">
181
- <label>
182
- <input class="myft-ui-create-list-variant-form-toggle" type="checkbox" name="is-public" value="public" checked data-trackable="private-link" text="private">
183
- <span class="myft-ui-create-list-variant-form-toggle-label o-forms-input__label">
184
- <span class="o-forms-input__label__main">
185
- Public
186
- </span>
187
- <span id="myft-ui-create-list-variant-form-public-description" class="o-forms-input__label__prompt">
188
- Your profession & list will be visible to others
189
- </span>
190
- </span>
191
- </label>
192
- </span>
193
- </div>
194
-
195
- <div class="myft-ui-create-list-variant-form-buttons">
196
- <button class="o-buttons o-buttons--primary o-buttons--inverse o-buttons--big" type="button" data-trackable="cancel-link" text="cancel">
197
- Cancel
198
- </button>
199
- <button class="o-buttons o-buttons--big o-buttons--secondary" type="submit">
200
- Add
201
- </button>
202
- </div>
203
- </form>
204
- `;
205
-
206
- const formElement = stringToHTMLElement(formString);
207
-
208
- function handleSubmit (event) {
209
- event.preventDefault();
210
- event.stopPropagation();
211
- const inputListName = formElement.querySelector('input[name="list-name"]');
212
- const inputIsPublic = formElement.querySelector('input[name="is-public"]');
213
-
214
- const newList = {
215
- name: inputListName.value,
216
- isPublic: inputIsPublic ? inputIsPublic.checked : false
217
- };
218
-
219
- createList(newList, ((contentId, createdList) => {
220
- triggerCreateListEvent(contentId, createdList.uuid);
221
- triggerAddToListEvent(contentId, createdList.uuid);
222
- if (!modal) {
223
- positionOverlay(createListOverlay.wrapper);
224
- }
225
- onListCreated();
226
- }));
227
- formElement.remove();
228
- }
229
-
230
- function handleCancelClick (event) {
231
- event.preventDefault();
232
- event.stopPropagation();
233
- formElement.remove();
234
- if (!lists.length) attachDescription();
235
- onCancel();
236
- }
237
-
238
- formElement.querySelector('button[type="submit"]').addEventListener('click', handleSubmit);
239
- formElement.querySelector('button[type="button"]').addEventListener('click', handleCancelClick);
240
-
241
- addPublicToggleListener(formElement);
242
-
243
- return formElement;
244
- }
245
-
246
- function addPublicToggleListener (formElement) {
247
- function onPublicToggleClick (event) {
248
- event.target.setAttribute('data-trackable', event.target.checked ? 'private-link' : 'public-link');
249
- event.target.setAttribute('text', event.target.checked ? 'private' : 'public');
250
- }
251
-
252
- formElement.querySelector('input[name="is-public"]').addEventListener('click', onPublicToggleClick);
253
- }
254
-
255
- function ContentElement (hasDescription, onClick) {
256
- const description = '<p class="myft-ui-create-list-variant-add-description">Lists are a simple way to curate your content</p>';
257
-
258
- const content = `
259
- <div class="myft-ui-create-list-variant-footer">
260
- <button class="myft-ui-create-list-variant-add myft-ui-create-list-variant-add-collapsed" aria-expanded=false data-trackable="add-to-new-list" text="add to new list">Add to a new list</button>
261
- ${hasDescription ? `
262
- ${description}
263
- ` : ''}
264
- <span
265
- class="myft-ui-create-list-variant-announcement o-normalise-visually-hidden"
266
- role="region"
267
- aria-live="assertive"
268
- />
269
- </div>
270
- `;
271
-
272
- const contentElement = stringToHTMLElement(content);
273
-
274
- contentElement.querySelector('.myft-ui-create-list-variant-add').addEventListener('click', checkScrollToAdd);
275
- contentElement.querySelector('.myft-ui-create-list-variant-add').addEventListener('click', triggerAddToNewListEvent);
276
-
277
- function removeDescription () {
278
- const descriptionElement = contentElement.querySelector('.myft-ui-create-list-variant-add-description');
279
- if (descriptionElement) {
280
- descriptionElement.remove();
281
- }
282
- }
283
-
284
- function attachDescription () {
285
- const descriptionElement = stringToHTMLElement(description);
286
- contentElement.insertAdjacentElement('beforeend', descriptionElement);
287
- }
288
-
289
- function restoreFormHandler () {
290
- contentElement.querySelector('.myft-ui-create-list-variant-add').classList.add('myft-ui-create-list-variant-add-collapsed');
291
- contentElement.querySelector('.myft-ui-create-list-variant-add').setAttribute('aria-expanded', false);
292
- return contentElement.addEventListener('click', clickHandler, { once: true });
293
- }
294
-
295
- function clickHandler (event) {
296
- contentElement.querySelector('.myft-ui-create-list-variant-add').classList.remove('myft-ui-create-list-variant-add-collapsed');
297
- contentElement.querySelector('.myft-ui-create-list-variant-add').setAttribute('aria-expanded', true);
298
- onClick(event);
299
- }
300
-
301
- return [contentElement, removeDescription, attachDescription, restoreFormHandler];
302
- }
303
-
304
- function HeadingElement () {
305
- const heading = `
306
- <span class="myft-ui-create-list-variant-heading">Added to <a href="https://www.ft.com/myft/saved-articles" data-trackable="saved-articles">saved articles</a> in <span class="myft-ui-create-list-variant-icon"><span class="myft-ui-create-list-variant-icon-visually-hidden">my FT</span></span></span>
307
- `;
308
-
309
- return stringToHTMLElement(heading);
310
- }
311
-
312
- function ListsElement (lists, addToList, removeFromList) {
313
- const currentList = document.querySelector('.myft-ui-create-list-variant-lists');
314
- if (currentList) {
315
- currentList.remove();
316
- }
317
-
318
- const listCheckboxElement = ListCheckboxElement(addToList, removeFromList);
319
-
320
- const listsTemplate = `
321
- <div class="myft-ui-create-list-variant-lists o-forms-field o-forms-field--optional" role="group">
322
- <span class="myft-ui-create-list-variant-lists-text">Add to list</span>
323
- <span class="myft-ui-create-list-variant-lists-container o-forms-input o-forms-input--checkbox">
324
- </span>
325
- </div>
326
- `;
327
- const listsElement = stringToHTMLElement(listsTemplate);
328
-
329
- const listsElementContainer = listsElement.querySelector('.myft-ui-create-list-variant-lists-container');
330
-
331
- function refresh () {
332
- listsElementContainer.replaceChildren(...lists.map(list => listCheckboxElement(list)));
333
- }
334
-
335
- function hide () {
336
- listsElement.style.display = 'none';
337
- }
338
-
339
- function show () {
340
- listsElement.style.display = 'flex';
341
- }
342
-
343
- refresh();
344
-
345
- return [listsElement, refresh, hide, show];
346
- }
347
-
348
- function ListCheckboxElement (addToList, removeFromList) {
349
- return function (list) {
350
-
351
- const listCheckbox = `<label>
352
- <input type="checkbox" name="default" value="${list.uuid}" ${list.checked ? 'checked' : ''}>
353
- <span class="o-forms-input__label">
354
- <span class="o-normalise-visually-hidden">
355
- ${list.checked ? 'Remove article from ' : 'Add article to ' }
356
- </span>
357
- ${list.name}
358
- </span>
359
- </label>
360
- `;
361
-
362
- const listCheckboxElement = stringToHTMLElement(listCheckbox);
363
-
364
- const [ input ] = listCheckboxElement.children;
365
-
366
- function handleCheck (event) {
367
- event.preventDefault();
368
- const isChecked = event.target.checked;
369
-
370
- function onListUpdated () {
371
- const indexToUpdate = lists.indexOf(list);
372
- lists[indexToUpdate] = { ...lists[indexToUpdate], checked: isChecked };
373
- listCheckboxElement.querySelector('input').checked = isChecked;
374
- }
375
-
376
- return isChecked ? addToList(list, onListUpdated) : removeFromList(list, onListUpdated);
377
- }
378
-
379
- input.addEventListener('click', handleCheck);
380
-
381
- return listCheckboxElement;
382
- };
383
- }
384
-
385
- function realignOverlay (originalScrollPosition, target) {
386
- return function () {
387
- const currentScrollPosition = window.scrollY;
388
-
389
- if(Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
390
- return;
391
- }
392
-
393
- if (currentScrollPosition) {
394
- originalScrollPosition = currentScrollPosition;;
395
- }
396
-
397
- positionOverlay(target);
398
- };
399
- }
400
-
401
- function positionOverlay (target) {
402
- target.style['min-width'] = '340px';
403
- target.style['width'] = '100%';
404
- target.style['margin-top'] = 0;
405
- target.style['left'] = 0;
406
- target.style['top'] = 0;
407
-
408
- if (isMobile()) {
409
- const shareNavComponent = document.querySelector('.share-nav__horizontal');
410
- const topHalfOffset = target.clientHeight + 10;
411
- target.style['position'] = 'absolute';
412
- target.style['margin-left'] = 0;
413
- target.style['top'] = calculateLargerScreenHalf(shareNavComponent) === 'ABOVE' ? `-${topHalfOffset}px` : '50px';
414
- } else {
415
- target.style['position'] = 'absolute';
416
- target.style['margin-left'] = '45px';
417
- }
418
- }
419
-
420
- function calculateLargerScreenHalf (target) {
421
- if (!target) {
422
- return 'BELOW';
423
- }
424
-
425
- const vh = Math.min(document.documentElement.clientHeight || 0, window.innerHeight || 0);
426
-
427
- const targetBox = target.getBoundingClientRect();
428
- const spaceAbove = targetBox.top;
429
- const spaceBelow = vh - targetBox.bottom;
430
-
431
- return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
432
- }
433
-
434
- async function getLists (contentId) {
435
- return myFtClient.getListsContent()
436
- .then(results => results.items.map(list => {
437
- const isChecked = Array.isArray(list.content) && list.content.some(content => content.uuid === contentId);
438
- return { name: list.name, uuid: list.uuid, checked: isChecked, content: list.content, isPublic: list.isPublic };
439
- }));
440
- }
441
-
442
- function triggerAddToListEvent (contentId, listId) {
443
- return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
444
- detail: {
445
- category: 'list',
446
- action: 'add-success',
447
- article_id: contentId,
448
- list_id: listId,
449
- teamName: 'customer-products-us-growth',
450
- amplitudeExploratory: true
451
- },
452
- bubbles: true
453
- }));
454
- }
455
-
456
- function triggerRemoveFromListEvent (contentId, listId) {
457
- return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
458
- detail: {
459
- category: 'list',
460
- action: 'remove-success',
461
- article_id: contentId,
462
- list_id: listId,
463
- teamName: 'customer-products-us-growth',
464
- amplitudeExploratory: true
465
- },
466
- bubbles: true
467
- }));
468
- }
469
-
470
- function triggerCreateListEvent (contentId, listId) {
471
- document.body.dispatchEvent(new CustomEvent('oTracking.event', {
472
- detail: {
473
- category: 'list',
474
- action: 'create-success',
475
- article_id: contentId,
476
- list_id: listId,
477
- teamName: 'customer-products-us-growth',
478
- amplitudeExploratory: true
479
- },
480
- bubbles: true
481
- }));
482
- }
483
-
484
- // Temporary event on the public toggle feature.
485
- // These will be used to build a sanity check dashboard, and will be removed after we get clean-up this test.
486
- function triggerAddToNewListEvent () {
487
- document.body.dispatchEvent(new CustomEvent('oTracking.event', {
488
- detail: {
489
- category: 'publicToggle',
490
- action: 'addToNewList',
491
- teamName: 'customer-products-us-growth',
492
- amplitudeExploratory: true
493
- },
494
- bubbles: true
495
- }));
496
- }
497
-
498
- //Temporary event to determine whether users need to scroll to add to a list
499
- function checkScrollToAdd () {
500
- //if the bottom of the overlay was not showing and scrolling has occurred since it was opened
501
- if(listOverlayBottom > window.innerHeight && window.scrollY > scrolledOnOpen) {
502
- document.body.dispatchEvent(new CustomEvent('oTracking.event', {
503
- detail: {
504
- category: 'publicToggle',
505
- action: 'scrollToAdd',
506
- teamName: 'customer-products-us-growth',
507
- amplitudeExploratory: true
508
- },
509
- bubbles: true
510
- }));
511
- }
512
- }