@financial-times/n-myft-ui 28.1.0 → 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
  },
@@ -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",