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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,7 @@
4
4
  data-myft-ui="saved"
5
5
  action="/myft/save/{{contentId}}"
6
6
  data-js-action="/__myft/api/core/saved/content/{{contentId}}?method=put"
7
- {{#if @root.flags.manageArticleLists}}data-myft-ui-save-new="manageArticleLists"{{/if}}
8
- {{#if @root.flags.manageArticleLists}}data-myft-ui-save-new-config="{{#if modal}}modal{{/if}}"{{/if}}>
7
+ data-myft-ui-save-config="{{#if nonModal}}nonModal{{/if}}">
9
8
  {{> n-myft-ui/components/csrf-token/input}}
10
9
  <div
11
10
  class="n-myft-ui__announcement o-normalise-visually-hidden"
package/myft/main.scss CHANGED
@@ -174,7 +174,7 @@ $spacing-unit: 20px;
174
174
 
175
175
  }
176
176
 
177
- .myft-ui-create-list-variant-message {
177
+ .myft-ui-create-list {
178
178
  border-radius: 10px;
179
179
  border: 1px solid oColorsByName('black-5');
180
180
  background: oColorsByName('white-80');
@@ -193,7 +193,7 @@ $spacing-unit: 20px;
193
193
  }
194
194
  }
195
195
 
196
- .myft-ui-create-list-variant {
196
+ .myft-ui-create-list {
197
197
  @include oButtons($opts: (
198
198
  'sizes': ('big'),
199
199
  'types': ('primary', 'secondary'),
@@ -0,0 +1,15 @@
1
+ export default function escapeText (text) {
2
+ if (typeof text !== 'string' || !text instanceof String) {
3
+ return '';
4
+ }
5
+
6
+ const tagsToReplace = {
7
+ '&': '&amp;',
8
+ '<': '&lt;',
9
+ '>': '&gt;'
10
+ };
11
+
12
+ return text.replace(/[&<>]/g, function (tag) {
13
+ return tagsToReplace[tag] || tag;
14
+ });
15
+ }
package/myft/ui/lists.js CHANGED
@@ -1,330 +1,514 @@
1
1
  import myFtClient from 'next-myft-client';
2
- import Delegate from 'ftdomdelegate';
2
+ import isMobile from './lib/is-mobile';
3
+ import escapeText from './lib/escape-text';
3
4
  import Overlay from '@financial-times/o-overlay';
4
- import * as myFtUiButtonStates from './lib/button-states';
5
- import nNotification from '@financial-times/n-notification';
6
5
  import { uuid } from 'n-ui-foundations';
7
6
  import getToken from './lib/get-csrf-token';
8
- import isMobile from './lib/is-mobile';
9
- import oForms from '@financial-times/o-forms';
10
- import openSaveArticleToListVariant from './save-article-to-list-variant';
11
7
  import stringToHTMLElement from './lib/convert-string-to-html-element';
12
8
 
13
- const delegate = new Delegate(document.body);
14
9
  const csrfToken = getToken();
15
10
 
11
+ let lists = [];
12
+ let haveLoadedLists = false;
13
+ let createListOverlay;
14
+
15
+ async function openSaveArticleToList (contentId, options = {}) {
16
+ const { name, nonModal = false } = options;
17
+ const modal = !nonModal;
18
+ function createList (newList, cb) {
19
+ if(!newList || !newList.name) {
20
+ return restoreContent();
21
+ }
22
+
23
+ myFtClient.add('user', null, 'created', 'list', uuid(), { name: newList.name, token: csrfToken, isPublic: newList.isPublic })
24
+ .then(detail => {
25
+ myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((data) => {
26
+ const createdList = { name: newList.name, uuid: data.actorId, checked: true, isPublic: !!newList.isPublic };
27
+ lists.unshift(createdList);
28
+ const announceListContainer = document.querySelector('.myft-ui-create-list-announcement');
29
+ announceListContainer.textContent = `${newList.name} created`;
30
+ cb(contentId, createdList);
31
+ });
32
+ })
33
+ .catch(() => {
34
+ return restoreContent();
35
+ });
36
+ }
37
+
38
+ function addToList (list, cb) {
39
+ if(!list) {
40
+ return;
41
+ }
42
+
43
+ myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((addedList) => {
44
+ cb();
45
+ triggerAddToListEvent(contentId, addedList.actorId);
46
+ });
47
+ }
48
+
49
+ function removeFromList (list, cb) {
50
+ if(!list) {
51
+ return;
52
+ }
53
+
54
+ myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((removedList) => {
55
+ cb();
56
+ triggerRemoveFromListEvent(contentId, removedList.actorId);
57
+ });
58
+ }
59
+
60
+ function restoreContent () {
61
+ if (!lists.length) attachDescription();
62
+ refreshListElement();
63
+ showListElement();
64
+ return restoreFormHandler();
65
+ }
66
+
67
+ if (!haveLoadedLists) {
68
+ lists = await getLists(contentId);
69
+ haveLoadedLists = true;
70
+ }
16
71
 
17
- function openOverlay (html, { name = 'myft-ui', title = '&nbsp;', shaded = false, trigger = false }) {
18
- // If an overlay already exists of the same name destroy it.
19
72
  const overlays = Overlay.getOverlays();
20
73
  const existingOverlay = overlays[name];
21
74
  if (existingOverlay) {
22
75
  existingOverlay.destroy();
23
76
  }
24
- // Create a new overlay.
25
- const overlay = new Overlay(name, {
26
- heading: { title, shaded },
27
- html,
77
+
78
+ const headingElement = HeadingElement();
79
+ let [contentElement, removeDescription, attachDescription, restoreFormHandler] = ContentElement(!lists.length, openFormHandler);
80
+ const [listElement, refreshListElement, hideListElement, showListElement] = ListsElement(lists, addToList, removeFromList);
81
+
82
+ createListOverlay = new Overlay(name, {
83
+ modal,
84
+ html: contentElement,
85
+ heading: { title: headingElement.outerHTML },
86
+ parentnode: isMobile() ? '.o-share--horizontal' : '.o-share--vertical',
87
+ class: 'myft-ui-create-list',
28
88
  });
29
89
 
30
- overlay.open();
90
+ function outsideClickHandler (e) {
91
+ const overlayContent = document.querySelector('.o-overlay__content');
92
+ const overlayContainer = document.querySelector('.o-overlay');
93
+ // we don't want to close the overlay if the click happened inside the
94
+ // overlay, except if the click happened on the overlay close button
95
+ const isTargetInsideOverlay = overlayContainer.contains(e.target) && !e.target.classList.contains('o-overlay__close');
96
+ if(createListOverlay.visible && (!overlayContent || !isTargetInsideOverlay)) {
97
+ createListOverlay.close();
98
+ }
99
+ }
100
+
101
+ function onFormCancel () {
102
+ showListElement();
103
+ restoreFormHandler();
104
+ }
105
+
106
+ function onFormListCreated () {
107
+ refreshListElement();
108
+ showListElement();
109
+ restoreFormHandler();
110
+ }
31
111
 
32
- if (trigger) {
33
- document.body.addEventListener('oOverlay.destroy', () => trigger.focus(), true);
112
+ function openFormHandler () {
113
+ hideListElement();
114
+ const formElement = FormElement(createList, attachDescription, onFormListCreated, onFormCancel, modal);
115
+ const overlayContent = document.querySelector('.o-overlay__content');
116
+ removeDescription();
117
+ overlayContent.insertAdjacentElement('beforeend', formElement);
34
118
  }
35
119
 
36
- return new Promise(resolve => {
37
- document.body.addEventListener('oOverlay.ready', () => resolve(overlay));
120
+ createListOverlay.open();
121
+
122
+ const scrollHandler = getScrollHandler(createListOverlay.wrapper);
123
+ const resizeHandler = getResizeHandler(createListOverlay.wrapper);
124
+
125
+ createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
126
+ const overlayContent = document.querySelector('.o-overlay__content');
127
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
128
+ if (!lists.length) {
129
+ hideListElement();
130
+ }
131
+
132
+ if (!modal) {
133
+ positionOverlay(data.currentTarget);
134
+
135
+ window.addEventListener('oViewport.resize', resizeHandler);
136
+ window.addEventListener('scroll', scrollHandler);
137
+ }
138
+
139
+ restoreFormHandler();
140
+
141
+ document.body.addEventListener('click', outsideClickHandler);
142
+
143
+
144
+ });
145
+
146
+ createListOverlay.wrapper.addEventListener('oOverlay.destroy', () => {
147
+ window.removeEventListener('scroll', scrollHandler);
148
+
149
+ window.removeEventListener('oViewport.resize', resizeHandler);
150
+
151
+ document.body.removeEventListener('click', outsideClickHandler);
38
152
  });
39
153
  }
40
154
 
41
- function updateAfterAddToList (listId, contentId, wasAdded) {
155
+ function getScrollHandler (target) {
156
+ return realignOverlay(window.scrollY, target);
157
+ }
158
+
159
+ function getResizeHandler (target) {
160
+ return function resizeHandler () {
161
+ positionOverlay(target);
162
+ };
163
+ }
164
+
165
+ function FormElement (createList, attachDescription, onListCreated, onCancel, modal=true) {
166
+ const formString = `
167
+ <form class="myft-ui-create-list-form">
168
+ <label class="myft-ui-create-list-form-name o-forms-field">
169
+ <span class="o-forms-input o-forms-input--text">
170
+ <input class="myft-ui-create-list-text" type="text" name="list-name">
171
+ <div class="myft-ui-create-list-label">List name</div>
172
+ </span>
173
+ </label>
174
+
175
+ <div class="myft-ui-create-list-form-public o-forms-field" role="group">
176
+ <span class="o-forms-input o-forms-input--toggle">
177
+ <label>
178
+ <input class="myft-ui-create-list-form-toggle" type="checkbox" name="is-public" value="public" checked data-trackable="private-link" text="private">
179
+ <span class="myft-ui-create-list-form-toggle-label o-forms-input__label">
180
+ <span class="o-forms-input__label__main">
181
+ Public
182
+ </span>
183
+ <span id="myft-ui-create-list-form-public-description" class="o-forms-input__label__prompt">
184
+ Your profession & list will be visible to others
185
+ </span>
186
+ </span>
187
+ </label>
188
+ </span>
189
+ </div>
190
+
191
+ <div class="myft-ui-create-list-form-buttons">
192
+ <button class="o-buttons o-buttons--primary o-buttons--inverse o-buttons--big" type="button" data-trackable="cancel-link" text="cancel">
193
+ Cancel
194
+ </button>
195
+ <button class="o-buttons o-buttons--big o-buttons--secondary" type="submit">
196
+ Add
197
+ </button>
198
+ </div>
199
+ </form>
200
+ `;
42
201
 
43
- myFtUiButtonStates.setStateOfButton('contained', contentId, wasAdded);
202
+ const formElement = stringToHTMLElement(formString);
203
+
204
+ function handleSubmit (event) {
205
+ event.preventDefault();
206
+ event.stopPropagation();
207
+ const inputListName = formElement.querySelector('input[name="list-name"]');
208
+ const inputIsPublic = formElement.querySelector('input[name="is-public"]');
44
209
 
45
- return myFtClient.personaliseUrl(`/myft/list/${listId}`)
46
- .then(personalisedUrl => {
47
- const message = `
48
- <a href="/myft" class="myft-ui__logo" data-trackable="myft-logo"><abbr title="myFT" class="myft-ui__icon"></abbr></a>
49
- ${wasAdded ? `Article added to your list.
50
- <a href="${personalisedUrl}" data-trackable="alerts">View list</a>` : 'Article removed from your list'}
51
- `;
210
+ const newList = {
211
+ name: inputListName.value,
212
+ isPublic: inputIsPublic ? inputIsPublic.checked : false
213
+ };
52
214
 
53
- if (!message) {
54
- return;
215
+ createList(newList, ((contentId, createdList) => {
216
+ triggerCreateListEvent(contentId, createdList.uuid);
217
+ triggerAddToListEvent(contentId, createdList.uuid);
218
+ if (!modal) {
219
+ positionOverlay(createListOverlay.wrapper);
55
220
  }
56
- nNotification.show({
57
- content: message,
58
- trackable: 'myft-feedback-notification'
59
- });
221
+ onListCreated();
222
+ }));
223
+ formElement.remove();
224
+ }
60
225
 
61
- document.body.dispatchEvent(new CustomEvent('oTracking.event', {
62
- detail: {
63
- category: 'list',
64
- action: 'copy-success',
65
- article_id: contentId,
66
- list_id: listId,
67
- content: {
68
- uuid: contentId
69
- },
70
- teamName: 'customer-products-us-growth',
71
- amplitudeExploratory: true
72
- },
73
- bubbles: true
74
- }));
75
- });
226
+ function handleCancelClick (event) {
227
+ event.preventDefault();
228
+ event.stopPropagation();
229
+ formElement.remove();
230
+ if (!lists.length) attachDescription();
231
+ onCancel();
232
+ }
233
+
234
+ formElement.querySelector('button[type="submit"]').addEventListener('click', handleSubmit);
235
+ formElement.querySelector('button[type="button"]').addEventListener('click', handleCancelClick);
236
+
237
+ addPublicToggleListener(formElement);
238
+
239
+ return formElement;
76
240
  }
77
241
 
242
+ function addPublicToggleListener (formElement) {
243
+ function onPublicToggleClick (event) {
244
+ event.target.setAttribute('data-trackable', event.target.checked ? 'private-link' : 'public-link');
245
+ event.target.setAttribute('text', event.target.checked ? 'private' : 'public');
246
+ }
78
247
 
79
- function setUpSaveToExistingListListeners (overlay, contentId) {
248
+ formElement.querySelector('input[name="is-public"]').addEventListener('click', onPublicToggleClick);
249
+ }
80
250
 
81
- const saveToExistingListButton = overlay.content.querySelector('.js-save-to-existing-list');
82
- const listSelect = overlay.content.querySelector('.js-list-select');
251
+ function ContentElement (hasDescription, onClick) {
252
+ const description = '<p class="myft-ui-create-list-add-description">Lists are a simple way to curate your content</p>';
83
253
 
84
- if (saveToExistingListButton) {
85
- saveToExistingListButton.addEventListener('click', event => {
86
- event.preventDefault();
254
+ const content = `
255
+ <div class="myft-ui-create-list-footer">
256
+ <button class="myft-ui-create-list-add myft-ui-create-list-add-collapsed" aria-expanded=false data-trackable="add-to-new-list" text="add to new list">Add to a new list</button>
257
+ ${hasDescription ? `
258
+ ${description}
259
+ ` : ''}
260
+ <span
261
+ class="myft-ui-create-list-announcement o-normalise-visually-hidden"
262
+ role="region"
263
+ aria-live="assertive"
264
+ />
265
+ </div>
266
+ `;
87
267
 
88
- if (!listSelect.value) {
89
- return;
90
- }
268
+ const contentElement = stringToHTMLElement(content);
91
269
 
92
- const listId = listSelect.options[listSelect.selectedIndex].value;
93
- myFtClient.add('list', listId, 'contained', 'content', contentId, { token: csrfToken })
94
- .then(detail => {
95
- updateAfterAddToList(detail.actorId, detail.subject, !!detail.results);
96
- overlay.close();
97
- });
98
- });
270
+ function removeDescription () {
271
+ const descriptionElement = contentElement.querySelector('.myft-ui-create-list-add-description');
272
+ if (descriptionElement) {
273
+ descriptionElement.remove();
274
+ }
275
+ }
276
+
277
+ function attachDescription () {
278
+ const descriptionElement = stringToHTMLElement(description);
279
+ contentElement.insertAdjacentElement('beforeend', descriptionElement);
280
+ }
281
+
282
+ function restoreFormHandler () {
283
+ contentElement.querySelector('.myft-ui-create-list-add').classList.add('myft-ui-create-list-add-collapsed');
284
+ contentElement.querySelector('.myft-ui-create-list-add').setAttribute('aria-expanded', false);
285
+ return contentElement.addEventListener('click', clickHandler, { once: true });
286
+ }
287
+
288
+ function clickHandler (event) {
289
+ contentElement.querySelector('.myft-ui-create-list-add').classList.remove('myft-ui-create-list-add-collapsed');
290
+ contentElement.querySelector('.myft-ui-create-list-add').setAttribute('aria-expanded', true);
291
+ onClick(event);
99
292
  }
293
+
294
+ return [contentElement, removeDescription, attachDescription, restoreFormHandler];
100
295
  }
101
296
 
102
- function setUpCreateListListeners (overlay, contentId) {
297
+ function HeadingElement () {
298
+ const heading = `
299
+ <span class="myft-ui-create-list-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-icon"><span class="myft-ui-create-list-icon-visually-hidden">my FT</span></span></span>
300
+ `;
103
301
 
104
- const createListButton = overlay.content.querySelector('.js-create-list');
105
- const nameInput = overlay.content.querySelector('.js-name');
302
+ return stringToHTMLElement(heading);
303
+ }
106
304
 
107
- if (!createListButton) {
108
- return;
305
+ function ListsElement (lists, addToList, removeFromList) {
306
+ const currentList = document.querySelector('.myft-ui-create-list-lists');
307
+ if (currentList) {
308
+ currentList.remove();
109
309
  }
110
310
 
111
- createListButton.addEventListener('click', event => {
112
- event.preventDefault();
311
+ const listCheckboxElement = ListCheckboxElement(addToList, removeFromList);
113
312
 
114
- if (!nameInput.value) {
115
- return;
116
- }
313
+ const listsTemplate = `
314
+ <div class="myft-ui-create-list-lists o-forms-field o-forms-field--optional" role="group">
315
+ <span class="myft-ui-create-list-lists-text">Add to list</span>
316
+ <span class="myft-ui-create-list-lists-container o-forms-input o-forms-input--checkbox">
317
+ </span>
318
+ </div>
319
+ `;
320
+ const listsElement = stringToHTMLElement(listsTemplate);
117
321
 
118
- const createListData = {
119
- token: csrfToken,
120
- name: nameInput.value
121
- };
322
+ const listsElementContainer = listsElement.querySelector('.myft-ui-create-list-lists-container');
122
323
 
123
- myFtClient.add('user', null, 'created', 'list', uuid(), createListData)
124
- .then(detail => {
125
- if (contentId) {
126
- return myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken });
127
- } else {
128
- return detail;
129
- }
324
+ function refresh () {
325
+ listsElementContainer.replaceChildren(...lists.map(list => listCheckboxElement(list)));
326
+ }
130
327
 
131
- })
132
- .then(detail => {
133
- if (contentId) {
134
- updateAfterAddToList(detail.actorId, contentId, !!detail.results);
135
- }
136
- overlay.close();
137
- })
138
- .catch(err => {
139
- nNotification.show({
140
- content: contentId ? 'Error adding article to new list' : 'Error creating new list',
141
- type: 'error'
142
- });
143
- throw err;
144
- });
145
- });
328
+ function hide () {
329
+ listsElement.style.display = 'none';
330
+ }
331
+
332
+ function show () {
333
+ listsElement.style.display = 'flex';
334
+ }
335
+
336
+ refresh();
146
337
 
338
+ return [listsElement, refresh, hide, show];
147
339
  }
148
340
 
341
+ function ListCheckboxElement (addToList, removeFromList) {
342
+ return function (list) {
343
+
344
+ const listCheckbox = `<label>
345
+ <input type="checkbox" name="default" value="${list.uuid}" ${list.checked ? 'checked' : ''}>
346
+ <span class="o-forms-input__label">
347
+ <span class="o-normalise-visually-hidden">
348
+ ${list.checked ? 'Remove article from ' : 'Add article to ' }
349
+ </span>
350
+ ${escapeText(list.name)}
351
+ </span>
352
+ </label>
353
+ `;
354
+
355
+ const listCheckboxElement = stringToHTMLElement(listCheckbox);
356
+
357
+ const [ input ] = listCheckboxElement.children;
358
+
359
+ function handleCheck (event) {
360
+ event.preventDefault();
361
+ const isChecked = event.target.checked;
149
362
 
150
- function showListsOverlay (overlayTitle, formHtmlUrl, contentId, trigger) {
151
- myFtClient.personaliseUrl(formHtmlUrl)
152
- .then(url => fetch(url, {
153
- credentials: 'same-origin'
154
- }))
155
- .then(res => {
156
- if (res.ok) {
157
- return res.text();
158
- } else {
159
- throw new Error(`Unexpected response: ${res.statusText}`);
363
+ function onListUpdated () {
364
+ const indexToUpdate = lists.indexOf(list);
365
+ lists[indexToUpdate] = { ...lists[indexToUpdate], checked: isChecked };
366
+ listCheckboxElement.querySelector('input').checked = isChecked;
160
367
  }
161
- })
162
- .then(html => openOverlay(html, { name: 'myft-lists', title: overlayTitle, trigger }))
163
- .then(overlay => {
164
- oForms.init(overlay.content);
165
- setUpSaveToExistingListListeners(overlay, contentId);
166
- setUpCreateListListeners(overlay, contentId);
167
- })
168
- .catch(err => {
169
- nNotification.show({
170
- content: 'Sorry, something went wrong',
171
- type: 'error'
172
- });
173
- throw err;
174
- });
175
368
 
176
- }
369
+ return isChecked ? addToList(list, onListUpdated) : removeFromList(list, onListUpdated);
370
+ }
177
371
 
178
- function showCopyToListOverlay (contentId, excludeList, trigger) {
179
- showListsOverlay('Copy to list', `/myft/list?fragment=true&copy=true&contentId=${contentId}&excludeList=${excludeList}`, contentId, trigger);
180
- }
372
+ input.addEventListener('click', handleCheck);
181
373
 
182
- function showCreateListOverlay (trigger) {
183
- showListsOverlay('Create list', '/myft/list?fragment=true', null, trigger);
374
+ return listCheckboxElement;
375
+ };
184
376
  }
185
377
 
186
- function showArticleSavedOverlay (contentId) {
187
- showListsOverlay('Article saved', `/myft/list?fragment=true&fromArticleSaved=true&contentId=${contentId}`, contentId);
188
- }
378
+ function realignOverlay (originalScrollPosition, target) {
379
+ return function () {
380
+ const currentScrollPosition = window.scrollY;
189
381
 
190
- function showCreateListAndAddArticleOverlay (contentId, config) {
191
- const options = {
192
- name: 'myft-ui-create-list-variant',
193
- ...config
382
+ if(Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
383
+ return;
384
+ }
385
+
386
+ if (currentScrollPosition) {
387
+ originalScrollPosition = currentScrollPosition;;
388
+ }
389
+
390
+ positionOverlay(target);
194
391
  };
392
+ }
195
393
 
196
- return openSaveArticleToListVariant(contentId, options);
394
+ function positionOverlay (target) {
395
+ target.style['min-width'] = '340px';
396
+ target.style['width'] = '100%';
397
+ target.style['margin-top'] = 0;
398
+ target.style['left'] = 0;
399
+ target.style['top'] = 0;
400
+
401
+ if (isMobile()) {
402
+ const shareNavComponent = document.querySelector('.share-nav__horizontal');
403
+ const topHalfOffset = target.clientHeight + 10;
404
+ target.style['position'] = 'absolute';
405
+ target.style['margin-left'] = 0;
406
+ target.style['top'] = calculateLargerScreenHalf(shareNavComponent) === 'ABOVE' ? `-${topHalfOffset}px` : '50px';
407
+ } else {
408
+ target.style['position'] = 'absolute';
409
+ target.style['margin-left'] = '45px';
410
+ }
197
411
  }
198
412
 
199
- function handleArticleSaved (contentId) {
200
- return myFtClient.getAll('created', 'list')
201
- .then(createdLists => createdLists.filter(list => !list.isRedirect))
202
- .then(createdLists => {
203
- if (createdLists.length) {
204
- showArticleSavedOverlay(contentId);
205
- }
206
- });
413
+ function calculateLargerScreenHalf (target) {
414
+ if (!target) {
415
+ return 'BELOW';
416
+ }
417
+
418
+ const vh = Math.min(document.documentElement.clientHeight || 0, window.innerHeight || 0);
419
+
420
+ const targetBox = target.getBoundingClientRect();
421
+ const spaceAbove = targetBox.top;
422
+ const spaceBelow = vh - targetBox.bottom;
423
+
424
+ return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
207
425
  }
208
426
 
209
- function openCreateListAndAddArticleOverlay (contentId, config) {
210
- return myFtClient.getAll('created', 'list')
211
- .then(createdLists => createdLists.filter(list => !list.isRedirect))
212
- .then(() => {
213
- return showCreateListAndAddArticleOverlay(contentId, config);
214
- });
427
+ async function getLists (contentId) {
428
+ return myFtClient.getListsContent()
429
+ .then(results => results.items.map(list => {
430
+ const isChecked = Array.isArray(list.content) && list.content.some(content => content.uuid === contentId);
431
+ return { name: list.name, uuid: list.uuid, checked: isChecked, content: list.content, isPublic: list.isPublic };
432
+ }));
215
433
  }
216
434
 
217
- function handleRemoveToggleSubmit (event) {
218
- event.preventDefault();
435
+ function triggerAddToListEvent (contentId, listId) {
436
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
437
+ detail: {
438
+ category: 'list',
439
+ action: 'add-success',
440
+ article_id: contentId,
441
+ list_id: listId,
442
+ teamName: 'customer-products-us-growth',
443
+ amplitudeExploratory: true
444
+ },
445
+ bubbles: true
446
+ }));
447
+ }
219
448
 
220
- const formEl = event.target;
221
- const submitBtnEl = formEl.querySelector('button[type="submit"]');
449
+ function triggerRemoveFromListEvent (contentId, listId) {
450
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
451
+ detail: {
452
+ category: 'list',
453
+ action: 'remove-success',
454
+ article_id: contentId,
455
+ list_id: listId,
456
+ teamName: 'customer-products-us-growth',
457
+ amplitudeExploratory: true
458
+ },
459
+ bubbles: true
460
+ }));
461
+ }
222
462
 
223
- if (submitBtnEl.hasAttribute('disabled')) {
224
- return;
225
- }
463
+ function triggerCreateListEvent (contentId, listId) {
464
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
465
+ detail: {
466
+ category: 'list',
467
+ action: 'create-success',
468
+ article_id: contentId,
469
+ list_id: listId,
470
+ teamName: 'customer-products-us-growth',
471
+ amplitudeExploratory: true
472
+ },
473
+ bubbles: true
474
+ }));
475
+ }
226
476
 
227
- const isSubmitBtnPressed = submitBtnEl.getAttribute('aria-pressed') === 'true';
228
- const action = isSubmitBtnPressed ? 'remove' : 'add';
229
- const contentId = formEl.dataset.contentId;
230
- const listId = formEl.dataset.actorId;
231
- const csrfToken = formEl.elements.token;
232
-
233
- if (!csrfToken || !csrfToken.value) {
234
- document.body.dispatchEvent(new CustomEvent('oErrors.log', {
235
- bubbles: true,
236
- detail: {
237
- error: new Error('myFT form submitted without a CSRF token'),
238
- info: {
239
- action,
240
- actorType: 'list',
241
- actorId: listId,
242
- relationshipName: 'contained',
243
- subjectType: 'content',
244
- subjectId: contentId,
245
- },
246
- },
247
- }));
248
- }
477
+ function showCreateListAndAddArticleOverlay (contentId, config) {
478
+ const options = {
479
+ name: 'myft-ui-create-list',
480
+ ...config
481
+ };
249
482
 
250
- submitBtnEl.setAttribute('disabled', '');
483
+ return openSaveArticleToList(contentId, options);
484
+ }
251
485
 
252
- myFtClient[action]('list', listId, 'contained', 'content', contentId, { token: csrfToken.value })
486
+ function openCreateListAndAddArticleOverlay (contentId, config) {
487
+ return myFtClient.getAll('created', 'list')
253
488
  .then(() => {
254
- myFtUiButtonStates.toggleButton(submitBtnEl, !isSubmitBtnPressed);
255
-
256
- document.body.dispatchEvent(new CustomEvent('oTracking.event', {
257
- detail: {
258
- category: 'list',
259
- action: action === 'add' ? 'add-success' : 'remove-success',
260
- article_id: contentId,
261
- list_id: listId,
262
- content: {
263
- uuid: contentId
264
- },
265
- teamName: 'customer-products-us-growth',
266
- amplitudeExploratory: true
267
- },
268
- bubbles: true
269
- }));
270
- })
271
- .catch(error => {
272
- setTimeout(() => submitBtnEl.removeAttribute('disabled'));
273
- throw error;
489
+ return showCreateListAndAddArticleOverlay(contentId, config);
274
490
  });
275
491
  }
276
492
 
277
493
  function initialEventListeners () {
278
494
 
279
495
  document.body.addEventListener('myft.user.saved.content.add', event => {
496
+ event.stopPropagation();
280
497
  const contentId = event.detail.subject;
281
-
282
- // Checks if the createListAndSaveArticle variant is active
283
- // and will show the variant overlay if the user has no lists,
284
- // otherwise it will show the classic overlay
285
- const newListDesign = event.currentTarget.querySelector('[data-myft-ui-save-new="manageArticleLists"]');
286
- if (newListDesign) {
287
- const configKeys = newListDesign.dataset.myftUiSaveNewConfig.split(',');
288
- const config = configKeys.reduce((configObj, key) => (key ? { ...configObj, [key]: true} : configObj), {});
289
-
290
- // Temporary events on the public toggle feature.
291
- // These will be used to build a sanity check dashboard, and will be removed after we get clean-up this test.
292
- document.body.dispatchEvent(new CustomEvent('oTracking.event', {
293
- detail: {
294
- category: 'lists',
295
- action: 'savedArticle',
296
- article_id: contentId,
297
- teamName: 'customer-products-us-growth',
298
- amplitudeExploratory: true
299
- },
300
- bubbles: true
301
- }));
302
-
303
- return openCreateListAndAddArticleOverlay(contentId, config);
498
+ const configSet = event.currentTarget.querySelector('[data-myft-ui-save-config]');
499
+ if (!configSet) {
500
+ return openCreateListAndAddArticleOverlay(contentId);
304
501
  }
305
-
306
- handleArticleSaved(contentId);
502
+ const configKeys = configSet.dataset.myftUiSaveConfig.split(',');
503
+ const config = configKeys.reduce((configObj, key) => (key ? { ...configObj, [key]: true} : configObj), {});
504
+ return openCreateListAndAddArticleOverlay(contentId, config);
307
505
  });
308
506
 
309
507
  document.body.addEventListener('myft.user.saved.content.remove', event => {
508
+ event.stopPropagation();
310
509
  const contentId = event.detail.subject;
311
-
312
- const newListDesign = event.currentTarget.querySelector('[data-myft-ui-save-new="manageArticleLists"]');
313
- if (newListDesign) {
314
- return showUnsavedNotification(contentId);
315
- }
316
- });
317
-
318
- delegate.on('click', '[data-myft-ui="copy-to-list"]', event => {
319
- event.preventDefault();
320
- showCopyToListOverlay(event.target.getAttribute('data-content-id'), event.target.getAttribute('data-actor-id'), event.target);
510
+ return showUnsavedNotification(contentId);
321
511
  });
322
- delegate.on('click', '[data-myft-ui="create-list"]', event => {
323
- event.preventDefault();
324
- showCreateListOverlay(event.target);
325
- });
326
-
327
- delegate.on('submit', '[data-myft-ui="contained"]', handleRemoveToggleSubmit);
328
512
  }
329
513
 
330
514
  function showUnsavedNotification () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/n-myft-ui",
3
- "version": "30.1.1",
3
+ "version": "30.2.0",
4
4
  "description": "Client side component for interaction with myft",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -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
- }