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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- }