@financial-times/n-myft-ui 27.1.0 → 27.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.
@@ -3,7 +3,8 @@
3
3
  data-content-id="{{contentId}}"
4
4
  data-myft-ui="saved"
5
5
  action="/myft/save/{{contentId}}"
6
- data-js-action="/__myft/api/core/saved/content/{{contentId}}?method=put">
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
8
  {{> n-myft-ui/components/csrf-token/input}}
8
9
  <div
9
10
  class="n-myft-ui__announcement o-normalise-visually-hidden"
package/myft/main.scss CHANGED
@@ -173,3 +173,148 @@ $spacing-unit: 20px;
173
173
  }
174
174
 
175
175
  }
176
+
177
+ .share-nav {
178
+ &.data-overlap-initialised {
179
+ .o-overlay {
180
+ transition: opacity 0.15s ease-in;
181
+ opacity: 0;
182
+ z-index: -1;
183
+ }
184
+ }
185
+
186
+ .myft-ui-create-list-variant {
187
+ border-radius: 10px;
188
+ border: 1px solid oColorsByName('black-5');
189
+ background: oColorsByName('white-80');
190
+
191
+ .o-overlay__heading {
192
+ border-radius: 10px 10px 0 0;
193
+ background: oColorsByName('white-60');
194
+ @include oTypographySans($scale: 2);
195
+ color: oColorsByName('black-80');
196
+ }
197
+
198
+ .o-overlay__content {
199
+ @include oTypographySans($scale: 0);
200
+ color: oColorsByName('black-80');
201
+ padding: 0;
202
+ }
203
+
204
+ .o-overlay__title {
205
+ margin: 8px 14px 0 8px;
206
+ }
207
+
208
+ &-container {
209
+ display: block;
210
+ width: 340px;
211
+ top: 115.5px;
212
+ left: 50px;
213
+ }
214
+
215
+ &-add {
216
+ border: 0;
217
+ background: none;
218
+ @include oTypographySans($scale: 1, $weight: 'semibold');
219
+ color: oColorsByName('black-80');
220
+
221
+ padding-left: 0;
222
+ margin-left: -8px;
223
+
224
+ &:hover {
225
+ text-decoration: underline;
226
+ }
227
+
228
+ &::before {
229
+ content: '';
230
+ @include oIconsContent(
231
+ 'plus',
232
+ oColorsByName('black-80'),
233
+ 28,
234
+ $iconset-version: 1
235
+ );
236
+ vertical-align: middle;
237
+ margin-top: -2px;
238
+ }
239
+ }
240
+
241
+ &-add-description {
242
+ margin: 4px 0;
243
+ }
244
+
245
+ &-heading {
246
+ &::before {
247
+ content: '';
248
+ @include oIconsContent(
249
+ 'tick',
250
+ oColorsByName('teal'),
251
+ 32,
252
+ $iconset-version: 1
253
+ );
254
+ vertical-align: middle;
255
+ margin-top: -2px;
256
+ }
257
+ }
258
+
259
+ &-footer {
260
+ border-top: 1px solid oColorsByName('black-5');
261
+ padding: 16px;
262
+ }
263
+
264
+ &-icon {
265
+ &::before {
266
+ content: "";
267
+ display: inline-block;
268
+ background-repeat: no-repeat;
269
+ background-size: contain;
270
+ background-position: 50%;
271
+ background-color: transparent;
272
+ background-image: url(https://www.ft.com/__origami/service/image/v2/images/raw/ftlogo-v1:brand-myft?source=next-article);
273
+ width: 42px;
274
+ height: 42px;
275
+ vertical-align: middle;
276
+ margin-top: -2px;
277
+ }
278
+
279
+ &-visually-hidden {
280
+ clip: rect(0 0 0 0);
281
+ clip-path: inset(50%);
282
+ height: 1px;
283
+ overflow: hidden;
284
+ position: absolute;
285
+ white-space: nowrap;
286
+ width: 1px;
287
+ }
288
+ }
289
+
290
+ &-form {
291
+ display: flex;
292
+ width: calc(100% - 32px);
293
+ justify-content: space-between;
294
+ height: 40px;
295
+ gap: 8px;
296
+ padding: 0 16px 16px;
297
+
298
+ & > * {
299
+ flex: 1 1 auto;
300
+ }
301
+
302
+ .o-forms-input {
303
+ margin-top: 0;
304
+ }
305
+ }
306
+
307
+ &-lists {
308
+ padding: 16px 16px 0;
309
+ @include oTypographySans($scale: 1);
310
+ &-text {
311
+ @include oTypographySans($weight: 'semibold');
312
+ color: oColorsByName('black-80');
313
+ margin-bottom: 16px;
314
+ }
315
+ &-container {
316
+ margin-top: 0;
317
+ }
318
+ }
319
+ }
320
+ }
package/myft/ui/lists.js CHANGED
@@ -6,6 +6,7 @@ import nNotification from 'n-notification';
6
6
  import { uuid } from 'n-ui-foundations';
7
7
  import getToken from './lib/get-csrf-token';
8
8
  import oForms from 'o-forms';
9
+ import openSaveArticleToListVariant from './save-article-to-list-variant';
9
10
 
10
11
  const delegate = new Delegate(document.body);
11
12
  const csrfToken = getToken();
@@ -161,6 +162,10 @@ function showArticleSavedOverlay (contentId) {
161
162
  showListsOverlay('Article saved', `/myft/list?fragment=true&fromArticleSaved=true&contentId=${contentId}`, contentId);
162
163
  }
163
164
 
165
+ function showCreateListAndAddArticleOverlay (contentId, name = 'myft-ui-create-list-variant') {
166
+ return openSaveArticleToListVariant(name, contentId);
167
+ }
168
+
164
169
  function handleArticleSaved (contentId) {
165
170
  return myFtClient.getAll('created', 'list')
166
171
  .then(createdLists => createdLists.filter(list => !list.isRedirect))
@@ -171,10 +176,27 @@ function handleArticleSaved (contentId) {
171
176
  });
172
177
  }
173
178
 
179
+ function openCreateListAndAddArticleOverlay (contentId) {
180
+ return myFtClient.getAll('created', 'list')
181
+ .then(createdLists => createdLists.filter(list => !list.isRedirect))
182
+ .then(createdLists => {
183
+ return !createdLists.length ? showCreateListAndAddArticleOverlay(contentId) : showArticleSavedOverlay(contentId);
184
+ });
185
+ }
186
+
174
187
  function initialEventListeners () {
175
188
 
176
189
  document.body.addEventListener('myft.user.saved.content.add', event => {
177
190
  const contentId = event.detail.subject;
191
+
192
+ // Checks if the createListAndSaveArticle variant is active
193
+ // and will show the variant overlay if the user has no lists,
194
+ // otherwise it will show the classic overlay
195
+ const createListVariant = event.currentTarget.querySelector('[data-myft-ui-variant="createListAndSaveArticleVariant"]');
196
+ if (createListVariant) {
197
+ return openCreateListAndAddArticleOverlay(contentId);
198
+ }
199
+
178
200
  handleArticleSaved(contentId);
179
201
  });
180
202
 
@@ -0,0 +1,376 @@
1
+ import Overlay from '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 oTooltip from 'o-tooltip';
6
+
7
+ const csrfToken = getToken();
8
+
9
+ let lists;
10
+
11
+ export default async function showSaveArticleToListVariant (name, contentId) {
12
+ try {
13
+ await openSaveArticleToListVariant (name, contentId);
14
+ } catch(error) {
15
+ handleError(error);
16
+ }
17
+ }
18
+
19
+ async function openSaveArticleToListVariant (name, contentId) {
20
+ function createList (list) {
21
+ if(!list) {
22
+ return;
23
+ }
24
+
25
+ myFtClient.add('user', null, 'created', 'list', uuid(), { name: list, token: csrfToken })
26
+ .then(detail => {
27
+ myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((createdList) => {
28
+ lists.push({ name: list, uuid: createdList.actorId, checked: true });
29
+ const listElement = ListsElement(lists, addToList, removeFromList);
30
+ const overlayContent = document.querySelector('.o-overlay__content');
31
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
32
+ const announceListContainer = document.querySelector('.myft-ui-create-list-variant-announcement');
33
+ announceListContainer.textContent = `${list} created`;
34
+ triggerCreateListEvent(contentId);
35
+ });
36
+ });
37
+ }
38
+
39
+ function addToList (list) {
40
+ if(!list) {
41
+ return;
42
+ }
43
+
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);
51
+ });
52
+ }
53
+
54
+ function removeFromList (list) {
55
+ if(!list) {
56
+ return;
57
+ }
58
+
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);
66
+ });
67
+ }
68
+
69
+ if (!lists) {
70
+ lists = await getLists(contentId);
71
+ }
72
+
73
+ const overlays = Overlay.getOverlays();
74
+ const existingOverlay = overlays[name];
75
+ if (existingOverlay) {
76
+ existingOverlay.destroy();
77
+ }
78
+
79
+ const contentElement = ContentElement();
80
+ const headingElement = HeadingElement();
81
+
82
+ const createListOverlay = new Overlay(name, {
83
+ html: contentElement,
84
+ heading: { title: headingElement.outerHTML },
85
+ modal: false,
86
+ parentnode: isMobile() ? '.o-share--horizontal' : '.o-share--vertical',
87
+ class: 'myft-ui-create-list-variant',
88
+ });
89
+
90
+ const realignListener = realignOverlay(window.scrollY);
91
+
92
+ 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);
100
+ }
101
+ }
102
+
103
+ function openFormHandler () {
104
+ try {
105
+ const formElement = FormElement(createList);
106
+ const overlayContent = document.querySelector('.o-overlay__content');
107
+ overlayContent.insertAdjacentElement('beforeend', formElement);
108
+ formElement.elements[0].focus();
109
+ } catch(error) {
110
+ handleError(error);
111
+ }
112
+ }
113
+
114
+ createListOverlay.open();
115
+ createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
116
+ realignListener(data.currentTarget);
117
+
118
+ if (lists && lists.length) {
119
+ const listElement = ListsElement(lists, addToList, removeFromList);
120
+ const overlayContent = document.querySelector('.o-overlay__content');
121
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
122
+ }
123
+
124
+ contentElement.addEventListener('click', openFormHandler);
125
+
126
+ document.querySelector('.article-content').addEventListener('click', outsideClickHandler);
127
+ });
128
+
129
+ createListOverlay.wrapper.addEventListener('oOverlay.destroy', () => {
130
+ const tooltipTemplate = document.createElement('div');
131
+ const opts = {
132
+ target: 'o-header-top-link-myft',
133
+ content: 'Go to saved articles in myFT to find your lists',
134
+ showOnConstruction: true,
135
+ closeAfter: 5,
136
+ position: 'below'
137
+ };
138
+
139
+ new oTooltip(tooltipTemplate, opts);
140
+
141
+ contentElement.removeEventListener('click', openFormHandler);
142
+
143
+ document.querySelector('.article-content').removeEventListener('click', outsideClickHandler);
144
+ });
145
+
146
+ window.addEventListener('scroll', function () {
147
+ realignListener(createListOverlay.wrapper, window.scrollY);
148
+ });
149
+
150
+ window.addEventListener('oViewport.resize', () => {
151
+ realignListener(createListOverlay.wrapper);
152
+ });
153
+ }
154
+
155
+ function stringToHTMLElement (string) {
156
+ const template = document.createElement('template');
157
+ template.innerHTML = string.trim();
158
+ return template.content.firstChild;
159
+ }
160
+
161
+ function FormElement (createList) {
162
+ const formString = `
163
+ <form class="myft-ui-create-list-variant-form">
164
+ <label class="o-forms-field">
165
+ <span class="o-forms-input o-forms-input--text">
166
+ <input type="text" name="list-name" aria-label="List name">
167
+ </span>
168
+ </label>
169
+ <button class="o-buttons o-buttons--secondary" type="submit">
170
+ Save
171
+ </button>
172
+ </form>
173
+ `;
174
+
175
+ const formElement = stringToHTMLElement(formString);
176
+
177
+ function handleSubmit (event) {
178
+ try {
179
+ event.preventDefault();
180
+ event.stopPropagation();
181
+ const inputListName = formElement.querySelector('input[name="list-name"]');
182
+ createList(inputListName.value);
183
+ inputListName.value = '';
184
+ formElement.remove();
185
+ } catch(error) {
186
+ handleError(error);
187
+ }
188
+ }
189
+
190
+ formElement.querySelector('button[type="submit"]').addEventListener('click', handleSubmit);
191
+
192
+ return formElement;
193
+ }
194
+
195
+ function ContentElement () {
196
+ let content = `
197
+ <div class="myft-ui-create-list-variant-footer">
198
+ <button class="myft-ui-create-list-variant-add">Add to a new list</button>
199
+ ${!lists.length ? '<p class="myft-ui-create-list-variant-add-description">Lists are a simple way to curate your content</p>' : ''}
200
+ </div>
201
+ `;
202
+
203
+ return stringToHTMLElement(content);
204
+ }
205
+
206
+ function HeadingElement () {
207
+ 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>
209
+ `;
210
+
211
+ return stringToHTMLElement(heading);
212
+ }
213
+
214
+ function ListsElement (lists, addToList, removeFromList) {
215
+ const currentList = document.querySelector('.myft-ui-create-list-variant-lists');
216
+ if (currentList) {
217
+ currentList.remove();
218
+ }
219
+
220
+ const listCheckboxElement = ListCheckboxElement(addToList, removeFromList);
221
+
222
+ const listsTemplate = `
223
+ <div class="myft-ui-create-list-variant-lists o-forms-field o-forms-field--optional" role="group">
224
+ <span class="myft-ui-create-list-variant-lists-text">Add to a list</span>
225
+ <span class="myft-ui-create-list-variant-lists-container o-forms-input o-forms-input--checkbox">
226
+ </span>
227
+ </div>
228
+ `;
229
+ const listsElement = stringToHTMLElement(listsTemplate);
230
+
231
+ const listsElementContainer = listsElement.querySelector('.myft-ui-create-list-variant-lists-container');
232
+
233
+ lists.map(list => listsElementContainer.insertAdjacentElement('beforeend', listCheckboxElement(list)));
234
+
235
+ return listsElement;
236
+ }
237
+
238
+ function ListCheckboxElement (addToList, removeFromList) {
239
+ return function (list) {
240
+ const listCheckbox = `<label>
241
+ <input type="checkbox" name="default" value="${list.name}" ${list.checked ? 'checked' : ''}>
242
+ <span class="o-forms-input__label">
243
+ <span class="o-normalise-visually-hidden">
244
+ ${list.checked ? 'Remove article from ' : 'Add article to ' }
245
+ </span>
246
+ ${list.name}
247
+ </span>
248
+ </label>
249
+ `;
250
+
251
+ const listCheckboxElement = stringToHTMLElement(listCheckbox);
252
+
253
+ const [ input ] = listCheckboxElement.children;
254
+
255
+ function handleCheck (event) {
256
+ event.preventDefault();
257
+ return event.target.checked ? addToList(list) : removeFromList(list);
258
+ }
259
+
260
+ input.addEventListener('click', handleCheck);
261
+
262
+ return listCheckboxElement;
263
+ };
264
+ }
265
+
266
+ function realignOverlay (originalScrollPosition) {
267
+ return function (target, currentScrollPosition) {
268
+ try {
269
+ if(currentScrollPosition && Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
270
+ return;
271
+ }
272
+
273
+ originalScrollPosition = currentScrollPosition;
274
+
275
+ target.style['min-width'] = '340px';
276
+ target.style['width'] = '100%';
277
+ target.style['margin-top'] = '-50px';
278
+ target.style['left'] = 0;
279
+
280
+ if (isMobile()) {
281
+ target.style['position'] = 'absolute';
282
+ target.style['margin-left'] = 0;
283
+ target.style['margin-top'] = 0;
284
+ target.style['top'] = calculateLargerScreenHalf(target) === 'ABOVE' ? '-120px' : '50px';
285
+ } else {
286
+ target.style['position'] = 'absolute';
287
+ target.style['margin-left'] = '45px';
288
+ target.style['top'] = '220px';
289
+ }
290
+ } catch (error) {
291
+ handleError(error);
292
+ }
293
+ };
294
+ }
295
+
296
+ function isMobile () {
297
+ const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
298
+
299
+ return vw <= 980;
300
+ }
301
+
302
+ function calculateLargerScreenHalf (target) {
303
+ const vh = Math.min(document.documentElement.clientHeight || 0, window.innerHeight || 0);
304
+
305
+ const targetBox = target.getBoundingClientRect();
306
+ const spaceAbove = targetBox.top;
307
+ const spaceBelow = vh - targetBox.bottom;
308
+
309
+ return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
310
+ }
311
+
312
+ async function getLists () {
313
+ return myFtClient.getAll('created', 'list')
314
+ .then(lists => lists.filter(list => !list.isRedirect))
315
+ .then(lists => {
316
+ return lists.map(list => ({ name: list.name, uuid: list.uuid, checked: false }));
317
+ });
318
+ }
319
+
320
+ function triggerAddToListEvent (contentId) {
321
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
322
+ detail: {
323
+ category: 'professorLists',
324
+ action: 'add-to-list',
325
+ article_id: contentId,
326
+ teamName: 'customer-products-us-growth',
327
+ amplitudeExploratory: true
328
+ },
329
+ bubbles: true
330
+ }));
331
+ }
332
+
333
+ function triggerRemoveFromListEvent (contentId) {
334
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
335
+ detail: {
336
+ category: 'professorLists',
337
+ action: 'remove-from-list',
338
+ article_id: contentId,
339
+ teamName: 'customer-products-us-growth',
340
+ amplitudeExploratory: true
341
+ },
342
+ bubbles: true
343
+ }));
344
+ }
345
+
346
+ function triggerCreateListEvent (contentId) {
347
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
348
+ detail: {
349
+ category: 'professorLists',
350
+ action: 'create-list',
351
+ article_id: contentId,
352
+ teamName: 'customer-products-us-growth',
353
+ amplitudeExploratory: true
354
+ },
355
+ bubbles: true
356
+ }));
357
+
358
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
359
+ detail: {
360
+ category: 'myFT',
361
+ action: 'create-list-success',
362
+ article_id: contentId
363
+ },
364
+ bubbles: true
365
+ }));
366
+ }
367
+
368
+ function handleError (error) {
369
+ document.body.dispatchEvent(new CustomEvent('oErrors.log', {
370
+ bubbles: true,
371
+ detail: {
372
+ error,
373
+ info: { component: 'professorLists' },
374
+ }
375
+ }));
376
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/n-myft-ui",
3
- "version": "27.1.0",
3
+ "version": "27.2.0",
4
4
  "description": "Client side component for interaction with myft",
5
5
  "main": "server.js",
6
6
  "scripts": {