@financial-times/n-myft-ui 28.1.0 → 28.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.
@@ -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
  },
@@ -14265,9 +14265,9 @@
14265
14265
  }
14266
14266
  },
14267
14267
  "node_modules/next-myft-client": {
14268
- "version": "10.1.0",
14269
- "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.1.0.tgz",
14270
- "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==",
14271
14271
  "hasInstallScript": true,
14272
14272
  "dependencies": {
14273
14273
  "black-hole-stream": "0.0.1",
@@ -34770,9 +34770,9 @@
34770
34770
  }
34771
34771
  },
34772
34772
  "next-myft-client": {
34773
- "version": "10.1.0",
34774
- "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.1.0.tgz",
34775
- "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==",
34776
34776
  "requires": {
34777
34777
  "black-hole-stream": "0.0.1",
34778
34778
  "fetchres": "^1.7.2",
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
 
@@ -184,6 +200,63 @@ function openCreateListAndAddArticleOverlay (contentId) {
184
200
  });
185
201
  }
186
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;
257
+ });
258
+ }
259
+
187
260
  function initialEventListeners () {
188
261
 
189
262
  document.body.addEventListener('myft.user.saved.content.add', event => {
@@ -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,
@@ -7,9 +7,10 @@ const csrfToken = getToken();
7
7
 
8
8
  let lists = [];
9
9
  let haveLoadedLists = false;
10
+ let createListOverlay;
10
11
 
11
12
  export default async function openSaveArticleToListVariant (name, contentId) {
12
- function createList (list) {
13
+ function createList (list, cb) {
13
14
  if(!list) {
14
15
  if (!lists.length) attachDescription();
15
16
  return contentElement.addEventListener('click', openFormHandler, { once: true });
@@ -18,14 +19,14 @@ export default async function openSaveArticleToListVariant (name, contentId) {
18
19
  myFtClient.add('user', null, 'created', 'list', uuid(), { name: list, token: csrfToken })
19
20
  .then(detail => {
20
21
  myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((createdList) => {
21
- lists.push({ name: list, uuid: createdList.actorId, checked: true });
22
+ lists.unshift({ name: list, uuid: createdList.actorId, checked: true });
22
23
  const listElement = ListsElement(lists, addToList, removeFromList);
23
24
  const overlayContent = document.querySelector('.o-overlay__content');
24
25
  overlayContent.insertAdjacentElement('afterbegin', listElement);
25
26
  const announceListContainer = document.querySelector('.myft-ui-create-list-variant-announcement');
26
27
  announceListContainer.textContent = `${list} created`;
27
- triggerCreateListEvent(contentId);
28
28
  contentElement.addEventListener('click', openFormHandler, { once: true });
29
+ cb(contentId, createdList.actorId);
29
30
  });
30
31
  })
31
32
  .catch(() => {
@@ -34,33 +35,25 @@ export default async function openSaveArticleToListVariant (name, contentId) {
34
35
  });
35
36
  }
36
37
 
37
- function addToList (list) {
38
+ function addToList (list, cb) {
38
39
  if(!list) {
39
40
  return;
40
41
  }
41
42
 
42
- myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then(() => {
43
- const indexToUpdate = lists.indexOf(list);
44
- lists[indexToUpdate] = { ...lists[indexToUpdate], checked: true };
45
- const listElement = ListsElement(lists, addToList, removeFromList);
46
- const overlayContent = document.querySelector('.o-overlay__content');
47
- overlayContent.insertAdjacentElement('afterbegin', listElement);
48
- triggerAddToListEvent(contentId);
43
+ myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((addedList) => {
44
+ cb();
45
+ triggerAddToListEvent(contentId, addedList.actorId);
49
46
  });
50
47
  }
51
48
 
52
- function removeFromList (list) {
49
+ function removeFromList (list, cb) {
53
50
  if(!list) {
54
51
  return;
55
52
  }
56
53
 
57
- myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then(() => {
58
- const indexToUpdate = lists.indexOf(list);
59
- lists[indexToUpdate] = { ...lists[indexToUpdate], checked: false };
60
- const listElement = ListsElement(lists, addToList, removeFromList);
61
- const overlayContent = document.querySelector('.o-overlay__content');
62
- overlayContent.insertAdjacentElement('afterbegin', listElement);
63
- triggerRemoveFromListEvent(contentId);
54
+ myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((removedList) => {
55
+ cb();
56
+ triggerRemoveFromListEvent(contentId, removedList.actorId);
64
57
  });
65
58
  }
66
59
 
@@ -78,7 +71,7 @@ export default async function openSaveArticleToListVariant (name, contentId) {
78
71
  const headingElement = HeadingElement();
79
72
  let [contentElement, removeDescription, attachDescription] = ContentElement(!lists.length);
80
73
 
81
- const createListOverlay = new Overlay(name, {
74
+ createListOverlay = new Overlay(name, {
82
75
  html: contentElement,
83
76
  heading: { title: headingElement.outerHTML },
84
77
  modal: false,
@@ -88,7 +81,11 @@ export default async function openSaveArticleToListVariant (name, contentId) {
88
81
 
89
82
  function outsideClickHandler (e) {
90
83
  const overlayContent = document.querySelector('.o-overlay__content');
91
- if(createListOverlay.visible && (!overlayContent || !overlayContent.contains(e.target))) {
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)) {
92
89
  createListOverlay.close();
93
90
  }
94
91
  }
@@ -114,17 +111,17 @@ export default async function openSaveArticleToListVariant (name, contentId) {
114
111
  const scrollHandler = getScrollHandler(createListOverlay.wrapper);
115
112
 
116
113
  createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
117
- positionOverlay(data.currentTarget);
118
-
119
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);
128
125
 
129
126
  window.addEventListener('scroll', scrollHandler);
130
127
 
@@ -135,6 +132,8 @@ export default async function openSaveArticleToListVariant (name, contentId) {
135
132
  window.removeEventListener('scroll', scrollHandler);
136
133
 
137
134
  window.removeEventListener('oViewport.resize', resizeHandler);
135
+
136
+ document.querySelector('.article-content').removeEventListener('click', outsideClickHandler);
138
137
  });
139
138
  }
140
139
 
@@ -164,7 +163,11 @@ function FormElement (createList) {
164
163
  event.preventDefault();
165
164
  event.stopPropagation();
166
165
  const inputListName = formElement.querySelector('input[name="list-name"]');
167
- createList(inputListName.value);
166
+ createList(inputListName.value, ((contentId, listId) => {
167
+ triggerCreateListEvent(contentId, listId);
168
+ triggerAddToListEvent(contentId, listId);
169
+ positionOverlay(createListOverlay.wrapper);
170
+ }));
168
171
  inputListName.value = '';
169
172
  formElement.remove();
170
173
  }
@@ -179,7 +182,7 @@ function ContentElement (hasDescription) {
179
182
 
180
183
  const content = `
181
184
  <div class="myft-ui-create-list-variant-footer">
182
- <button class="myft-ui-create-list-variant-add">Add to a new list</button>
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>
183
186
  ${hasDescription ? `
184
187
  ${description}
185
188
  ` : ''}
@@ -205,7 +208,7 @@ function ContentElement (hasDescription) {
205
208
 
206
209
  function HeadingElement () {
207
210
  const heading = `
208
- <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>
209
212
  `;
210
213
 
211
214
  return stringToHTMLElement(heading);
@@ -237,8 +240,9 @@ function ListsElement (lists, addToList, removeFromList) {
237
240
 
238
241
  function ListCheckboxElement (addToList, removeFromList) {
239
242
  return function (list) {
243
+
240
244
  const listCheckbox = `<label>
241
- <input type="checkbox" name="default" value="${list.name}" ${list.checked ? 'checked' : ''}>
245
+ <input type="checkbox" name="default" value="${list.uuid}" ${list.checked ? 'checked' : ''}>
242
246
  <span class="o-forms-input__label">
243
247
  <span class="o-normalise-visually-hidden">
244
248
  ${list.checked ? 'Remove article from ' : 'Add article to ' }
@@ -254,7 +258,15 @@ function ListCheckboxElement (addToList, removeFromList) {
254
258
 
255
259
  function handleCheck (event) {
256
260
  event.preventDefault();
257
- 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);
258
270
  }
259
271
 
260
272
  input.addEventListener('click', handleCheck);
@@ -287,10 +299,11 @@ function positionOverlay (target) {
287
299
 
288
300
  if (isMobile()) {
289
301
  const shareNavComponent = document.querySelector('.share-nav__horizontal');
302
+ const topHalfOffset = target.clientHeight + 10;
290
303
  target.style['position'] = 'absolute';
291
304
  target.style['margin-left'] = 0;
292
305
  target.style['margin-top'] = 0;
293
- target.style['top'] = calculateLargerScreenHalf(shareNavComponent) === 'ABOVE' ? '-120px' : '50px';
306
+ target.style['top'] = calculateLargerScreenHalf(shareNavComponent) === 'ABOVE' ? `-${topHalfOffset}px` : '50px';
294
307
  } else {
295
308
  target.style['position'] = 'absolute';
296
309
  target.style['margin-left'] = '45px';
@@ -318,20 +331,21 @@ function calculateLargerScreenHalf (target) {
318
331
  return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
319
332
  }
320
333
 
321
- async function getLists () {
322
- return myFtClient.getAll('created', 'list')
323
- .then(lists => lists.filter(list => !list.isRedirect))
324
- .then(lists => {
325
- return lists.map(list => ({ name: list.name, uuid: list.uuid, checked: false }));
326
- });
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
+ }));
327
340
  }
328
341
 
329
- function triggerAddToListEvent (contentId) {
342
+ function triggerAddToListEvent (contentId, listId) {
330
343
  return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
331
344
  detail: {
332
- category: 'professorLists',
333
- action: 'add-to-list',
345
+ category: 'list',
346
+ action: 'add-success',
334
347
  article_id: contentId,
348
+ list_id: listId,
335
349
  teamName: 'customer-products-us-growth',
336
350
  amplitudeExploratory: true
337
351
  },
@@ -339,12 +353,13 @@ function triggerAddToListEvent (contentId) {
339
353
  }));
340
354
  }
341
355
 
342
- function triggerRemoveFromListEvent (contentId) {
356
+ function triggerRemoveFromListEvent (contentId, listId) {
343
357
  return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
344
358
  detail: {
345
- category: 'professorLists',
346
- action: 'remove-from-list',
359
+ category: 'list',
360
+ action: 'remove-success',
347
361
  article_id: contentId,
362
+ list_id: listId,
348
363
  teamName: 'customer-products-us-growth',
349
364
  amplitudeExploratory: true
350
365
  },
@@ -352,24 +367,16 @@ function triggerRemoveFromListEvent (contentId) {
352
367
  }));
353
368
  }
354
369
 
355
- function triggerCreateListEvent (contentId) {
370
+ function triggerCreateListEvent (contentId, listId) {
356
371
  document.body.dispatchEvent(new CustomEvent('oTracking.event', {
357
372
  detail: {
358
- category: 'professorLists',
359
- action: 'create-list',
373
+ category: 'list',
374
+ action: 'create-success',
360
375
  article_id: contentId,
376
+ list_id: listId,
361
377
  teamName: 'customer-products-us-growth',
362
378
  amplitudeExploratory: true
363
379
  },
364
380
  bubbles: true
365
381
  }));
366
-
367
- return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
368
- detail: {
369
- category: 'myFT',
370
- action: 'create-list-success',
371
- article_id: contentId
372
- },
373
- bubbles: true
374
- }));
375
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/n-myft-ui",
3
- "version": "28.1.0",
3
+ "version": "28.2.0",
4
4
  "description": "Client side component for interaction with myft",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -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",