@financial-times/n-myft-ui 28.0.6 → 28.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,7 +15,7 @@
15
15
  "form-serialize": "^0.7.2",
16
16
  "ftdomdelegate": "^4.0.6",
17
17
  "js-cookie": "^2.2.1",
18
- "next-myft-client": "^10.1.0",
18
+ "next-myft-client": "^10.3.0",
19
19
  "next-session-client": "^4.0.0",
20
20
  "superstore-sync": "^2.1.1"
21
21
  },
@@ -97,7 +97,7 @@
97
97
  "@financial-times/o-normalise": "^3.0.0",
98
98
  "@financial-times/o-overlay": "^4.0.0",
99
99
  "@financial-times/o-spacing": "^3.0.0",
100
- "@financial-times/o-tooltip": "^5.0.0",
100
+ "@financial-times/o-tooltip": "^5.2.4",
101
101
  "@financial-times/o-topper": "^5.2.3",
102
102
  "n-ui-foundations": "^9.0.0"
103
103
  }
@@ -3031,9 +3031,9 @@
3031
3031
  }
3032
3032
  },
3033
3033
  "node_modules/@financial-times/o-tooltip": {
3034
- "version": "5.2.2",
3035
- "resolved": "https://registry.npmjs.org/@financial-times/o-tooltip/-/o-tooltip-5.2.2.tgz",
3036
- "integrity": "sha512-JUklwHvNmEFXfqjc6Ft7jV5Rc3cTfqBbcnexMCOvWTr8BE3Xidbg6/VumLKhmS+MTmG4NFl5EgP4Kt2hLrAa1Q==",
3034
+ "version": "5.2.4",
3035
+ "resolved": "https://registry.npmjs.org/@financial-times/o-tooltip/-/o-tooltip-5.2.4.tgz",
3036
+ "integrity": "sha512-GXC320/zwTNvacG4PCHSwGBPb+3eWMw7pePUAw41iJ70Wjwy+H9aGbbQikWOiBCiSb6BohwImWOJ6RR0g1dReg==",
3037
3037
  "peer": true,
3038
3038
  "dependencies": {
3039
3039
  "ftdomdelegate": "^4.0.6"
@@ -3042,6 +3042,7 @@
3042
3042
  "npm": "^7 || ^8"
3043
3043
  },
3044
3044
  "peerDependencies": {
3045
+ "@financial-times/o-brand": "^4.2.1",
3045
3046
  "@financial-times/o-grid": "^6.0.0",
3046
3047
  "@financial-times/o-icons": "^7.0.1",
3047
3048
  "@financial-times/o-normalise": "^3.0.0",
@@ -14264,9 +14265,9 @@
14264
14265
  }
14265
14266
  },
14266
14267
  "node_modules/next-myft-client": {
14267
- "version": "10.1.0",
14268
- "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.1.0.tgz",
14269
- "integrity": "sha512-PqnNE1gxV00yVv6pA+1jWaySJkonQBUx2uWchPNGNcB20SzVmYleSWLeD3bdxdNOD5Tpy8DDl58CEQrKja7bqA==",
14268
+ "version": "10.3.0",
14269
+ "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.3.0.tgz",
14270
+ "integrity": "sha512-dAaIs6PhvYGczEsOHGNrI3FKKFDOcm6rSv06SaL9emltqlHqUXt7esun8m4N3D2iL2FzVXKLeMd9/gv/GDryOg==",
14270
14271
  "hasInstallScript": true,
14271
14272
  "dependencies": {
14272
14273
  "black-hole-stream": "0.0.1",
@@ -25571,9 +25572,9 @@
25571
25572
  "requires": {}
25572
25573
  },
25573
25574
  "@financial-times/o-tooltip": {
25574
- "version": "5.2.2",
25575
- "resolved": "https://registry.npmjs.org/@financial-times/o-tooltip/-/o-tooltip-5.2.2.tgz",
25576
- "integrity": "sha512-JUklwHvNmEFXfqjc6Ft7jV5Rc3cTfqBbcnexMCOvWTr8BE3Xidbg6/VumLKhmS+MTmG4NFl5EgP4Kt2hLrAa1Q==",
25575
+ "version": "5.2.4",
25576
+ "resolved": "https://registry.npmjs.org/@financial-times/o-tooltip/-/o-tooltip-5.2.4.tgz",
25577
+ "integrity": "sha512-GXC320/zwTNvacG4PCHSwGBPb+3eWMw7pePUAw41iJ70Wjwy+H9aGbbQikWOiBCiSb6BohwImWOJ6RR0g1dReg==",
25577
25578
  "peer": true,
25578
25579
  "requires": {
25579
25580
  "ftdomdelegate": "^4.0.6"
@@ -34769,9 +34770,9 @@
34769
34770
  }
34770
34771
  },
34771
34772
  "next-myft-client": {
34772
- "version": "10.1.0",
34773
- "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.1.0.tgz",
34774
- "integrity": "sha512-PqnNE1gxV00yVv6pA+1jWaySJkonQBUx2uWchPNGNcB20SzVmYleSWLeD3bdxdNOD5Tpy8DDl58CEQrKja7bqA==",
34773
+ "version": "10.3.0",
34774
+ "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.3.0.tgz",
34775
+ "integrity": "sha512-dAaIs6PhvYGczEsOHGNrI3FKKFDOcm6rSv06SaL9emltqlHqUXt7esun8m4N3D2iL2FzVXKLeMd9/gv/GDryOg==",
34775
34776
  "requires": {
34776
34777
  "black-hole-stream": "0.0.1",
34777
34778
  "fetchres": "^1.7.2",
@@ -4,7 +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
- {{#ifEquals @root.flags.professorLists 'variant'}}data-myft-ui-variant="createListAndSaveArticleVariant"{{/ifEquals}}>
7
+ {{#if @root.flags.manageArticleLists}}data-myft-ui-save-new="manageArticleLists"{{/if}}>
8
8
  {{> n-myft-ui/components/csrf-token/input}}
9
9
  <div
10
10
  class="n-myft-ui__announcement o-normalise-visually-hidden"
package/myft/main.scss CHANGED
@@ -314,6 +314,12 @@ $spacing-unit: 20px;
314
314
  }
315
315
  &-container {
316
316
  margin-top: 0;
317
+ max-height: 92px;
318
+ padding-bottom: 2px;
319
+ overflow-y: auto;
320
+ @include oGridRespondTo($from: M) {
321
+ max-height: 126px;
322
+ }
317
323
  }
318
324
  }
319
325
  }
@@ -11,12 +11,6 @@ const relationshipConfig = {
11
11
  subjectType: 'concept',
12
12
  uiSelector: '[data-myft-ui="follow"]'
13
13
  },
14
- contained: {
15
- actorType: 'list',
16
- idProperty: 'data-content-id',
17
- subjectType: 'content',
18
- uiSelector: '[data-myft-ui="contained"]'
19
- }
20
14
  };
21
15
 
22
16
  export default relationshipConfig;
@@ -39,12 +39,10 @@ const getExtraContext = (subjectType, subjectId) => {
39
39
  * @param {Object} postedData Event's extra data (required for checking if an instant alert was turned on or off)
40
40
  * @return {String} label for the action to send in the custom event
41
41
  */
42
- const getAction = (subjectType, action, postedData, resultData) => {
42
+ const getAction = (subjectType, action, postedData) => {
43
43
  if (action === 'update' && subjectType === 'concept') {
44
44
  const updateState = (postedData && postedData._rel && postedData._rel.instant && postedData._rel.instant === 'true') ? 'on' : 'off';
45
45
  return `instant-alert-${updateState}`;
46
- } else if (resultData && resultData.rel && resultData.rel.type && resultData.rel.type === 'contained') {
47
- return `${action}-to-list-success`;
48
46
  } else {
49
47
  return customDataSettings[subjectType][action];
50
48
  }
@@ -57,7 +55,7 @@ const getAction = (subjectType, action, postedData, resultData) => {
57
55
  export function custom (eventData) {
58
56
  if (Object.keys(customDataSettings).indexOf(eventData.subjectType) !== -1) {
59
57
  const options = Object.assign(
60
- {action: getAction(eventData.subjectType, eventData.action, eventData.postedData, eventData.resultData)},
58
+ {action: getAction(eventData.subjectType, eventData.action, eventData.postedData)},
61
59
  eventData.trackingInfo);
62
60
  const extraContext = getExtraContext(eventData.subjectType, eventData.subjectId);
63
61
  Object.assign(options, extraContext);
package/myft/ui/lists.js CHANGED
@@ -51,6 +51,18 @@ function updateAfterAddToList (listId, contentId, wasAdded) {
51
51
  content: message,
52
52
  trackable: 'myft-feedback-notification'
53
53
  });
54
+
55
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
56
+ detail: {
57
+ category: 'list',
58
+ action: 'copy-success',
59
+ article_id: contentId,
60
+ list_id: listId,
61
+ teamName: 'customer-products-us-growth',
62
+ amplitudeExploratory: true
63
+ },
64
+ bubbles: true
65
+ }));
54
66
  });
55
67
  }
56
68
 
@@ -83,6 +95,10 @@ function setUpCreateListListeners (overlay, contentId) {
83
95
  const createListButton = overlay.content.querySelector('.js-create-list');
84
96
  const nameInput = overlay.content.querySelector('.js-name');
85
97
 
98
+ if (!createListButton) {
99
+ return;
100
+ }
101
+
86
102
  createListButton.addEventListener('click', event => {
87
103
  event.preventDefault();
88
104
 
@@ -179,8 +195,65 @@ function handleArticleSaved (contentId) {
179
195
  function openCreateListAndAddArticleOverlay (contentId) {
180
196
  return myFtClient.getAll('created', 'list')
181
197
  .then(createdLists => createdLists.filter(list => !list.isRedirect))
182
- .then(createdLists => {
183
- return !createdLists.length ? showCreateListAndAddArticleOverlay(contentId) : showArticleSavedOverlay(contentId);
198
+ .then(() => {
199
+ return showCreateListAndAddArticleOverlay(contentId);
200
+ });
201
+ }
202
+
203
+ function handleRemoveToggleSubmit (event) {
204
+ event.preventDefault();
205
+
206
+ const formEl = event.target;
207
+ const submitBtnEl = formEl.querySelector('button[type="submit"]');
208
+
209
+ if (submitBtnEl.hasAttribute('disabled')) {
210
+ return;
211
+ }
212
+
213
+ const isSubmitBtnPressed = submitBtnEl.getAttribute('aria-pressed') === 'true';
214
+ const action = isSubmitBtnPressed ? 'remove' : 'add';
215
+ const contentId = formEl.dataset.contentId;
216
+ const listId = formEl.dataset.actorId;
217
+ const csrfToken = formEl.elements.token;
218
+
219
+ if (!csrfToken || !csrfToken.value) {
220
+ document.body.dispatchEvent(new CustomEvent('oErrors.log', {
221
+ bubbles: true,
222
+ detail: {
223
+ error: new Error('myFT form submitted without a CSRF token'),
224
+ info: {
225
+ action,
226
+ actorType: 'list',
227
+ actorId: listId,
228
+ relationshipName: 'contained',
229
+ subjectType: 'content',
230
+ subjectId: contentId,
231
+ },
232
+ },
233
+ }));
234
+ }
235
+
236
+ submitBtnEl.setAttribute('disabled', '');
237
+
238
+ myFtClient[action]('list', listId, 'contained', 'content', contentId, { token: csrfToken.value })
239
+ .then(() => {
240
+ myFtUiButtonStates.toggleButton(submitBtnEl, !isSubmitBtnPressed);
241
+
242
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
243
+ detail: {
244
+ category: 'list',
245
+ action: action === 'add' ? 'add-success' : 'remove-success',
246
+ article_id: contentId,
247
+ list_id: listId,
248
+ teamName: 'customer-products-us-growth',
249
+ amplitudeExploratory: true
250
+ },
251
+ bubbles: true
252
+ }));
253
+ })
254
+ .catch(error => {
255
+ setTimeout(() => submitBtnEl.removeAttribute('disabled'));
256
+ throw error;
184
257
  });
185
258
  }
186
259
 
@@ -192,8 +265,8 @@ function initialEventListeners () {
192
265
  // Checks if the createListAndSaveArticle variant is active
193
266
  // and will show the variant overlay if the user has no lists,
194
267
  // otherwise it will show the classic overlay
195
- const createListVariant = event.currentTarget.querySelector('[data-myft-ui-variant="createListAndSaveArticleVariant"]');
196
- if (createListVariant) {
268
+ const createNewListDesign = event.currentTarget.querySelector('[data-myft-ui-save-new="manageArticleLists"]');
269
+ if (createNewListDesign) {
197
270
  return openCreateListAndAddArticleOverlay(contentId);
198
271
  }
199
272
 
@@ -208,6 +281,8 @@ function initialEventListeners () {
208
281
  ev.preventDefault();
209
282
  showCreateListOverlay();
210
283
  });
284
+
285
+ delegate.on('submit', '[data-myft-ui="contained"]', handleRemoveToggleSubmit);
211
286
  }
212
287
 
213
288
  export function init () {
@@ -65,6 +65,7 @@ function signedInEventListeners () {
65
65
  const resultData = event.detail.results && event.detail.results[0];
66
66
  const isPressed = !!event.detail.results;
67
67
  buttonStates.setStateOfButton(relationshipName, event.detail.subject, isPressed, undefined, resultData, true);
68
+
68
69
  tracking.custom({
69
70
  subjectType,
70
71
  action,
@@ -5,69 +5,61 @@ import getToken from './lib/get-csrf-token';
5
5
 
6
6
  const csrfToken = getToken();
7
7
 
8
- let lists;
8
+ let lists = [];
9
+ let haveLoadedLists = false;
10
+ let createListOverlay;
9
11
 
10
- export default async function showSaveArticleToListVariant (name, contentId) {
11
- try {
12
- await openSaveArticleToListVariant (name, contentId);
13
- } catch(error) {
14
- handleError(error);
15
- }
16
- }
17
-
18
- async function openSaveArticleToListVariant (name, contentId) {
19
- function createList (list) {
12
+ export default async function openSaveArticleToListVariant (name, contentId) {
13
+ function createList (list, cb) {
20
14
  if(!list) {
21
- return;
15
+ if (!lists.length) attachDescription();
16
+ return contentElement.addEventListener('click', openFormHandler, { once: true });
22
17
  }
23
18
 
24
19
  myFtClient.add('user', null, 'created', 'list', uuid(), { name: list, token: csrfToken })
25
20
  .then(detail => {
26
21
  myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((createdList) => {
27
- lists.push({ name: list, uuid: createdList.actorId, checked: true });
22
+ lists.unshift({ name: list, uuid: createdList.actorId, checked: true });
28
23
  const listElement = ListsElement(lists, addToList, removeFromList);
29
24
  const overlayContent = document.querySelector('.o-overlay__content');
30
25
  overlayContent.insertAdjacentElement('afterbegin', listElement);
31
26
  const announceListContainer = document.querySelector('.myft-ui-create-list-variant-announcement');
32
27
  announceListContainer.textContent = `${list} created`;
33
- triggerCreateListEvent(contentId);
34
28
  contentElement.addEventListener('click', openFormHandler, { once: true });
29
+ cb(contentId, createdList.actorId);
35
30
  });
31
+ })
32
+ .catch(() => {
33
+ if (!lists.length) attachDescription();
34
+ return contentElement.addEventListener('click', openFormHandler, { once: true });
36
35
  });
37
36
  }
38
37
 
39
- function addToList (list) {
38
+ function addToList (list, cb) {
40
39
  if(!list) {
41
40
  return;
42
41
  }
43
42
 
44
- myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then(() => {
45
- const indexToUpdate = lists.indexOf(list);
46
- lists[indexToUpdate] = { ...lists[indexToUpdate], checked: true };
47
- const listElement = ListsElement(lists, addToList, removeFromList);
48
- const overlayContent = document.querySelector('.o-overlay__content');
49
- overlayContent.insertAdjacentElement('afterbegin', listElement);
50
- triggerAddToListEvent(contentId);
43
+ myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((addedList) => {
44
+ cb();
45
+ triggerAddToListEvent(contentId, addedList.actorId);
51
46
  });
52
47
  }
53
48
 
54
- function removeFromList (list) {
49
+ function removeFromList (list, cb) {
55
50
  if(!list) {
56
51
  return;
57
52
  }
58
53
 
59
- myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then(() => {
60
- const indexToUpdate = lists.indexOf(list);
61
- lists[indexToUpdate] = { ...lists[indexToUpdate], checked: false };
62
- const listElement = ListsElement(lists, addToList, removeFromList);
63
- const overlayContent = document.querySelector('.o-overlay__content');
64
- overlayContent.insertAdjacentElement('afterbegin', listElement);
65
- triggerRemoveFromListEvent(contentId);
54
+ myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((removedList) => {
55
+ cb();
56
+ triggerRemoveFromListEvent(contentId, removedList.actorId);
66
57
  });
67
58
  }
68
59
 
69
- if (!lists) {
60
+ if (!haveLoadedLists) {
70
61
  lists = await getLists(contentId);
62
+ haveLoadedLists = true;
71
63
  }
72
64
 
73
65
  const overlays = Overlay.getOverlays();
@@ -77,9 +69,9 @@ async function openSaveArticleToListVariant (name, contentId) {
77
69
  }
78
70
 
79
71
  const headingElement = HeadingElement();
80
- let [contentElement, removeDescription] = ContentElement(!lists.length);
72
+ let [contentElement, removeDescription, attachDescription] = ContentElement(!lists.length);
81
73
 
82
- const createListOverlay = new Overlay(name, {
74
+ createListOverlay = new Overlay(name, {
83
75
  html: contentElement,
84
76
  heading: { title: headingElement.outerHTML },
85
77
  modal: false,
@@ -87,50 +79,61 @@ async function openSaveArticleToListVariant (name, contentId) {
87
79
  class: 'myft-ui-create-list-variant',
88
80
  });
89
81
 
90
- const realignListener = realignOverlay(window.scrollY);
91
-
92
82
  function outsideClickHandler (e) {
93
- try {
94
- const overlayContent = document.querySelector('.o-overlay__content');
95
- if(!overlayContent || !overlayContent.contains(e.target)) {
96
- createListOverlay.close();
97
- }
98
- } catch(error) {
99
- handleError(error);
83
+ const overlayContent = document.querySelector('.o-overlay__content');
84
+ const overlayContainer = document.querySelector('.o-overlay');
85
+ // we don't want to close the overlay if the click happened inside the
86
+ // overlay, except if the click happened on the overlay close button
87
+ const isTargetInsideOverlay = overlayContainer.contains(e.target) && !e.target.classList.contains('o-overlay__close');
88
+ if(createListOverlay.visible && (!overlayContent || !isTargetInsideOverlay)) {
89
+ createListOverlay.close();
100
90
  }
101
91
  }
102
92
 
103
93
  function openFormHandler () {
104
- try {
105
- const formElement = FormElement(createList);
106
- const overlayContent = document.querySelector('.o-overlay__content');
107
- removeDescription();
108
- overlayContent.insertAdjacentElement('beforeend', formElement);
109
- formElement.elements[0].focus();
110
- } catch(error) {
111
- handleError(error);
112
- }
94
+ const formElement = FormElement(createList);
95
+ const overlayContent = document.querySelector('.o-overlay__content');
96
+ removeDescription();
97
+ overlayContent.insertAdjacentElement('beforeend', formElement);
98
+ formElement.elements[0].focus();
99
+ }
100
+
101
+ function getScrollHandler (target) {
102
+ return realignOverlay(window.scrollY, target);
103
+ }
104
+
105
+ function resizeHandler () {
106
+ positionOverlay(createListOverlay.wrapper);
113
107
  }
114
108
 
115
109
  createListOverlay.open();
116
- createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
117
- realignListener(data.currentTarget);
118
110
 
119
- if (lists && lists.length) {
111
+ const scrollHandler = getScrollHandler(createListOverlay.wrapper);
112
+
113
+ createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
114
+ if (lists.length) {
120
115
  const listElement = ListsElement(lists, addToList, removeFromList);
121
116
  const overlayContent = document.querySelector('.o-overlay__content');
122
117
  overlayContent.insertAdjacentElement('afterbegin', listElement);
123
118
  }
124
119
 
120
+ positionOverlay(data.currentTarget);
121
+
125
122
  contentElement.addEventListener('click', openFormHandler, { once: true });
126
123
 
127
- document.querySelector('.article-content').addEventListener('click', outsideClickHandler, { once: true });
124
+ document.querySelector('.article-content').addEventListener('click', outsideClickHandler);
125
+
126
+ window.addEventListener('scroll', scrollHandler);
127
+
128
+ window.addEventListener('oViewport.resize', resizeHandler);
128
129
  });
129
130
 
130
- window.addEventListener('scroll', realignListener(createListOverlay.wrapper, window.scrollY));
131
+ createListOverlay.wrapper.addEventListener('oOverlay.destroy', () => {
132
+ window.removeEventListener('scroll', scrollHandler);
133
+
134
+ window.removeEventListener('oViewport.resize', resizeHandler);
131
135
 
132
- window.addEventListener('oViewport.resize', () => {
133
- realignListener(createListOverlay.wrapper);
136
+ document.querySelector('.article-content').removeEventListener('click', outsideClickHandler);
134
137
  });
135
138
  }
136
139
 
@@ -157,16 +160,16 @@ function FormElement (createList) {
157
160
  const formElement = stringToHTMLElement(formString);
158
161
 
159
162
  function handleSubmit (event) {
160
- try {
161
- event.preventDefault();
162
- event.stopPropagation();
163
- const inputListName = formElement.querySelector('input[name="list-name"]');
164
- createList(inputListName.value);
165
- inputListName.value = '';
166
- formElement.remove();
167
- } catch(error) {
168
- handleError(error);
169
- }
163
+ event.preventDefault();
164
+ event.stopPropagation();
165
+ const inputListName = formElement.querySelector('input[name="list-name"]');
166
+ createList(inputListName.value, ((contentId, listId) => {
167
+ triggerCreateListEvent(contentId, listId);
168
+ triggerAddToListEvent(contentId, listId);
169
+ positionOverlay(createListOverlay.wrapper);
170
+ }));
171
+ inputListName.value = '';
172
+ formElement.remove();
170
173
  }
171
174
 
172
175
  formElement.querySelector('button[type="submit"]').addEventListener('click', handleSubmit);
@@ -174,12 +177,14 @@ function FormElement (createList) {
174
177
  return formElement;
175
178
  }
176
179
 
177
- function ContentElement (description) {
180
+ function ContentElement (hasDescription) {
181
+ const description = '<p class="myft-ui-create-list-variant-add-description">Lists are a simple way to curate your content</p>';
182
+
178
183
  const content = `
179
184
  <div class="myft-ui-create-list-variant-footer">
180
- <button class="myft-ui-create-list-variant-add">Add to a new list</button>
181
- ${description ? `
182
- <p class="myft-ui-create-list-variant-add-description">Lists are a simple way to curate your content</p>
185
+ <button class="myft-ui-create-list-variant-add" data-trackable="add-to-new-list" text="Add to a new list">Add to a new list</button>
186
+ ${hasDescription ? `
187
+ ${description}
183
188
  ` : ''}
184
189
  </div>
185
190
  `;
@@ -193,12 +198,17 @@ function ContentElement (description) {
193
198
  }
194
199
  }
195
200
 
196
- return [contentElement, removeDescription];
201
+ function attachDescription () {
202
+ const descriptionElement = stringToHTMLElement(description);
203
+ contentElement.insertAdjacentElement('beforeend', descriptionElement);
204
+ }
205
+
206
+ return [contentElement, removeDescription, attachDescription];
197
207
  }
198
208
 
199
209
  function HeadingElement () {
200
210
  const heading = `
201
- <span class="myft-ui-create-list-variant-heading">Added to <a href="https://www.ft.com/myft/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>
211
+ <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>
202
212
  `;
203
213
 
204
214
  return stringToHTMLElement(heading);
@@ -230,8 +240,9 @@ function ListsElement (lists, addToList, removeFromList) {
230
240
 
231
241
  function ListCheckboxElement (addToList, removeFromList) {
232
242
  return function (list) {
243
+
233
244
  const listCheckbox = `<label>
234
- <input type="checkbox" name="default" value="${list.name}" ${list.checked ? 'checked' : ''}>
245
+ <input type="checkbox" name="default" value="${list.uuid}" ${list.checked ? 'checked' : ''}>
235
246
  <span class="o-forms-input__label">
236
247
  <span class="o-normalise-visually-hidden">
237
248
  ${list.checked ? 'Remove article from ' : 'Add article to ' }
@@ -247,7 +258,15 @@ function ListCheckboxElement (addToList, removeFromList) {
247
258
 
248
259
  function handleCheck (event) {
249
260
  event.preventDefault();
250
- return event.target.checked ? addToList(list) : removeFromList(list);
261
+ const isChecked = event.target.checked;
262
+
263
+ function onListUpdated () {
264
+ const indexToUpdate = lists.indexOf(list);
265
+ lists[indexToUpdate] = { ...lists[indexToUpdate], checked: isChecked };
266
+ listCheckboxElement.querySelector('input').checked = isChecked;
267
+ }
268
+
269
+ return isChecked ? addToList(list, onListUpdated) : removeFromList(list, onListUpdated);
251
270
  }
252
271
 
253
272
  input.addEventListener('click', handleCheck);
@@ -256,36 +275,42 @@ function ListCheckboxElement (addToList, removeFromList) {
256
275
  };
257
276
  }
258
277
 
259
- function realignOverlay (originalScrollPosition) {
260
- return function (target, currentScrollPosition) {
261
- try {
262
- if(currentScrollPosition && Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
263
- return;
264
- }
278
+ function realignOverlay (originalScrollPosition, target) {
279
+ return function () {
280
+ const currentScrollPosition = window.scrollY;
265
281
 
266
- originalScrollPosition = currentScrollPosition;
267
-
268
- target.style['min-width'] = '340px';
269
- target.style['width'] = '100%';
270
- target.style['margin-top'] = '-50px';
271
- target.style['left'] = 0;
272
-
273
- if (isMobile()) {
274
- target.style['position'] = 'absolute';
275
- target.style['margin-left'] = 0;
276
- target.style['margin-top'] = 0;
277
- target.style['top'] = calculateLargerScreenHalf(target) === 'ABOVE' ? '-120px' : '50px';
278
- } else {
279
- target.style['position'] = 'absolute';
280
- target.style['margin-left'] = '45px';
281
- target.style['top'] = '220px';
282
- }
283
- } catch (error) {
284
- handleError(error);
282
+ if(Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
283
+ return;
284
+ }
285
+
286
+ if (currentScrollPosition) {
287
+ originalScrollPosition = currentScrollPosition;;
285
288
  }
289
+
290
+ positionOverlay(target);
286
291
  };
287
292
  }
288
293
 
294
+ function positionOverlay (target) {
295
+ target.style['min-width'] = '340px';
296
+ target.style['width'] = '100%';
297
+ target.style['margin-top'] = '-50px';
298
+ target.style['left'] = 0;
299
+
300
+ if (isMobile()) {
301
+ const shareNavComponent = document.querySelector('.share-nav__horizontal');
302
+ const topHalfOffset = target.clientHeight + 10;
303
+ target.style['position'] = 'absolute';
304
+ target.style['margin-left'] = 0;
305
+ target.style['margin-top'] = 0;
306
+ target.style['top'] = calculateLargerScreenHalf(shareNavComponent) === 'ABOVE' ? `-${topHalfOffset}px` : '50px';
307
+ } else {
308
+ target.style['position'] = 'absolute';
309
+ target.style['margin-left'] = '45px';
310
+ target.style['top'] = '220px';
311
+ }
312
+ }
313
+
289
314
  function isMobile () {
290
315
  const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
291
316
 
@@ -293,6 +318,10 @@ function isMobile () {
293
318
  }
294
319
 
295
320
  function calculateLargerScreenHalf (target) {
321
+ if (!target) {
322
+ return 'BELOW';
323
+ }
324
+
296
325
  const vh = Math.min(document.documentElement.clientHeight || 0, window.innerHeight || 0);
297
326
 
298
327
  const targetBox = target.getBoundingClientRect();
@@ -302,20 +331,21 @@ function calculateLargerScreenHalf (target) {
302
331
  return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
303
332
  }
304
333
 
305
- async function getLists () {
306
- return myFtClient.getAll('created', 'list')
307
- .then(lists => lists.filter(list => !list.isRedirect))
308
- .then(lists => {
309
- return lists.map(list => ({ name: list.name, uuid: list.uuid, checked: false }));
310
- });
334
+ async function getLists (contentId) {
335
+ return myFtClient.getListsContent()
336
+ .then(results => results.items.map(list => {
337
+ const isChecked = Array.isArray(list.content) && list.content.some(content => content.uuid === contentId);
338
+ return { name: list.name, uuid: list.uuid, checked: isChecked, content: list.content };
339
+ }));
311
340
  }
312
341
 
313
- function triggerAddToListEvent (contentId) {
342
+ function triggerAddToListEvent (contentId, listId) {
314
343
  return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
315
344
  detail: {
316
- category: 'professorLists',
317
- action: 'add-to-list',
345
+ category: 'list',
346
+ action: 'add-success',
318
347
  article_id: contentId,
348
+ list_id: listId,
319
349
  teamName: 'customer-products-us-growth',
320
350
  amplitudeExploratory: true
321
351
  },
@@ -323,12 +353,13 @@ function triggerAddToListEvent (contentId) {
323
353
  }));
324
354
  }
325
355
 
326
- function triggerRemoveFromListEvent (contentId) {
356
+ function triggerRemoveFromListEvent (contentId, listId) {
327
357
  return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
328
358
  detail: {
329
- category: 'professorLists',
330
- action: 'remove-from-list',
359
+ category: 'list',
360
+ action: 'remove-success',
331
361
  article_id: contentId,
362
+ list_id: listId,
332
363
  teamName: 'customer-products-us-growth',
333
364
  amplitudeExploratory: true
334
365
  },
@@ -336,34 +367,16 @@ function triggerRemoveFromListEvent (contentId) {
336
367
  }));
337
368
  }
338
369
 
339
- function triggerCreateListEvent (contentId) {
370
+ function triggerCreateListEvent (contentId, listId) {
340
371
  document.body.dispatchEvent(new CustomEvent('oTracking.event', {
341
372
  detail: {
342
- category: 'professorLists',
343
- action: 'create-list',
373
+ category: 'list',
374
+ action: 'create-success',
344
375
  article_id: contentId,
376
+ list_id: listId,
345
377
  teamName: 'customer-products-us-growth',
346
378
  amplitudeExploratory: true
347
379
  },
348
380
  bubbles: true
349
381
  }));
350
-
351
- return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
352
- detail: {
353
- category: 'myFT',
354
- action: 'create-list-success',
355
- article_id: contentId
356
- },
357
- bubbles: true
358
- }));
359
- }
360
-
361
- function handleError (error) {
362
- document.body.dispatchEvent(new CustomEvent('oErrors.log', {
363
- bubbles: true,
364
- detail: {
365
- error,
366
- info: { component: 'professorLists' },
367
- }
368
- }));
369
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/n-myft-ui",
3
- "version": "28.0.6",
3
+ "version": "28.2.0",
4
4
  "description": "Client side component for interaction with myft",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -93,7 +93,7 @@
93
93
  "@financial-times/o-normalise": "^3.0.0",
94
94
  "@financial-times/o-overlay": "^4.0.0",
95
95
  "@financial-times/o-spacing": "^3.0.0",
96
- "@financial-times/o-tooltip": "^5.0.0",
96
+ "@financial-times/o-tooltip": "^5.2.4",
97
97
  "@financial-times/o-topper": "^5.2.3",
98
98
  "n-ui-foundations": "^9.0.0"
99
99
  },
@@ -103,13 +103,13 @@
103
103
  "form-serialize": "^0.7.2",
104
104
  "ftdomdelegate": "^4.0.6",
105
105
  "js-cookie": "^2.2.1",
106
- "next-myft-client": "^10.1.0",
106
+ "next-myft-client": "^10.3.0",
107
107
  "next-session-client": "^4.0.0",
108
108
  "superstore-sync": "^2.1.1"
109
109
  },
110
110
  "volta": {
111
111
  "node": "16.14.2",
112
- "npm": "7.20.2"
112
+ "npm": "7.24.2"
113
113
  },
114
114
  "engines": {
115
115
  "node": "14.x || 16.x",