@financial-times/n-myft-ui 28.1.0 → 28.2.2

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
  },
@@ -65,6 +65,7 @@
65
65
  "karma-sinon": "^1.0.5",
66
66
  "karma-sinon-chai": "2.0.2",
67
67
  "karma-sourcemap-loader": "^0.3.7",
68
+ "karma-viewport": "^1.0.9",
68
69
  "karma-webpack": "^4.0.2",
69
70
  "lintspaces-cli": "^0.7.0",
70
71
  "lolex": "5.1.1",
@@ -3383,6 +3384,84 @@
3383
3384
  "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
3384
3385
  "dev": true
3385
3386
  },
3387
+ "node_modules/@types/karma": {
3388
+ "version": "6.3.3",
3389
+ "resolved": "https://registry.npmjs.org/@types/karma/-/karma-6.3.3.tgz",
3390
+ "integrity": "sha512-nRMec4mTCt+tkpRqh5/pAxmnjzEgAaalIq7mdfLFH88gSRC8+bxejLiSjHMMT/vHIhJHqg4GPIGCnCFbwvDRww==",
3391
+ "dev": true,
3392
+ "dependencies": {
3393
+ "@types/node": "*",
3394
+ "log4js": "^6.4.1"
3395
+ }
3396
+ },
3397
+ "node_modules/@types/karma/node_modules/date-format": {
3398
+ "version": "4.0.13",
3399
+ "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.13.tgz",
3400
+ "integrity": "sha512-bnYCwf8Emc3pTD8pXnre+wfnjGtfi5ncMDKy7+cWZXbmRAsdWkOQHrfC1yz/KiwP5thDp2kCHWYWKBX4HP1hoQ==",
3401
+ "dev": true,
3402
+ "engines": {
3403
+ "node": ">=4.0"
3404
+ }
3405
+ },
3406
+ "node_modules/@types/karma/node_modules/flatted": {
3407
+ "version": "3.2.6",
3408
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz",
3409
+ "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==",
3410
+ "dev": true
3411
+ },
3412
+ "node_modules/@types/karma/node_modules/fs-extra": {
3413
+ "version": "8.1.0",
3414
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
3415
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
3416
+ "dev": true,
3417
+ "dependencies": {
3418
+ "graceful-fs": "^4.2.0",
3419
+ "jsonfile": "^4.0.0",
3420
+ "universalify": "^0.1.0"
3421
+ },
3422
+ "engines": {
3423
+ "node": ">=6 <7 || >=8"
3424
+ }
3425
+ },
3426
+ "node_modules/@types/karma/node_modules/jsonfile": {
3427
+ "version": "4.0.0",
3428
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
3429
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
3430
+ "dev": true,
3431
+ "optionalDependencies": {
3432
+ "graceful-fs": "^4.1.6"
3433
+ }
3434
+ },
3435
+ "node_modules/@types/karma/node_modules/log4js": {
3436
+ "version": "6.6.1",
3437
+ "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.6.1.tgz",
3438
+ "integrity": "sha512-J8VYFH2UQq/xucdNu71io4Fo+purYYudyErgBbswWKO0MC6QVOERRomt5su/z6d3RJSmLyTGmXl3Q/XjKCf+/A==",
3439
+ "dev": true,
3440
+ "dependencies": {
3441
+ "date-format": "^4.0.13",
3442
+ "debug": "^4.3.4",
3443
+ "flatted": "^3.2.6",
3444
+ "rfdc": "^1.3.0",
3445
+ "streamroller": "^3.1.2"
3446
+ },
3447
+ "engines": {
3448
+ "node": ">=8.0"
3449
+ }
3450
+ },
3451
+ "node_modules/@types/karma/node_modules/streamroller": {
3452
+ "version": "3.1.2",
3453
+ "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.2.tgz",
3454
+ "integrity": "sha512-wZswqzbgGGsXYIrBYhOE0yP+nQ6XRk7xDcYwuQAGTYXdyAUmvgVFE0YU1g5pvQT0m7GBaQfYcSnlHbapuK0H0A==",
3455
+ "dev": true,
3456
+ "dependencies": {
3457
+ "date-format": "^4.0.13",
3458
+ "debug": "^4.3.4",
3459
+ "fs-extra": "^8.1.0"
3460
+ },
3461
+ "engines": {
3462
+ "node": ">=8.0"
3463
+ }
3464
+ },
3386
3465
  "node_modules/@types/mdast": {
3387
3466
  "version": "3.0.10",
3388
3467
  "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@@ -12267,6 +12346,15 @@
12267
12346
  "graceful-fs": "^4.1.6"
12268
12347
  }
12269
12348
  },
12349
+ "node_modules/jsonschema": {
12350
+ "version": "1.4.1",
12351
+ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz",
12352
+ "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==",
12353
+ "dev": true,
12354
+ "engines": {
12355
+ "node": "*"
12356
+ }
12357
+ },
12270
12358
  "node_modules/jsprim": {
12271
12359
  "version": "1.4.2",
12272
12360
  "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -12441,6 +12529,16 @@
12441
12529
  "graceful-fs": "^4.1.2"
12442
12530
  }
12443
12531
  },
12532
+ "node_modules/karma-viewport": {
12533
+ "version": "1.0.9",
12534
+ "resolved": "https://registry.npmjs.org/karma-viewport/-/karma-viewport-1.0.9.tgz",
12535
+ "integrity": "sha512-E1xVe66vBQtI66TGOtZMzV5nf6BW5tW4TQVUqPK+oakVLdsG/ZUG688tGK0lL1q0t7nfQD1dwLD8Z9Guu/RVdg==",
12536
+ "dev": true,
12537
+ "dependencies": {
12538
+ "@types/karma": "^6.3.3",
12539
+ "jsonschema": "^1.4.0"
12540
+ }
12541
+ },
12444
12542
  "node_modules/karma-webpack": {
12445
12543
  "version": "4.0.2",
12446
12544
  "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.2.tgz",
@@ -14265,9 +14363,9 @@
14265
14363
  }
14266
14364
  },
14267
14365
  "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==",
14366
+ "version": "10.3.0",
14367
+ "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.3.0.tgz",
14368
+ "integrity": "sha512-dAaIs6PhvYGczEsOHGNrI3FKKFDOcm6rSv06SaL9emltqlHqUXt7esun8m4N3D2iL2FzVXKLeMd9/gv/GDryOg==",
14271
14369
  "hasInstallScript": true,
14272
14370
  "dependencies": {
14273
14371
  "black-hole-stream": "0.0.1",
@@ -25822,6 +25920,74 @@
25822
25920
  "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
25823
25921
  "dev": true
25824
25922
  },
25923
+ "@types/karma": {
25924
+ "version": "6.3.3",
25925
+ "resolved": "https://registry.npmjs.org/@types/karma/-/karma-6.3.3.tgz",
25926
+ "integrity": "sha512-nRMec4mTCt+tkpRqh5/pAxmnjzEgAaalIq7mdfLFH88gSRC8+bxejLiSjHMMT/vHIhJHqg4GPIGCnCFbwvDRww==",
25927
+ "dev": true,
25928
+ "requires": {
25929
+ "@types/node": "*",
25930
+ "log4js": "^6.4.1"
25931
+ },
25932
+ "dependencies": {
25933
+ "date-format": {
25934
+ "version": "4.0.13",
25935
+ "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.13.tgz",
25936
+ "integrity": "sha512-bnYCwf8Emc3pTD8pXnre+wfnjGtfi5ncMDKy7+cWZXbmRAsdWkOQHrfC1yz/KiwP5thDp2kCHWYWKBX4HP1hoQ==",
25937
+ "dev": true
25938
+ },
25939
+ "flatted": {
25940
+ "version": "3.2.6",
25941
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz",
25942
+ "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==",
25943
+ "dev": true
25944
+ },
25945
+ "fs-extra": {
25946
+ "version": "8.1.0",
25947
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
25948
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
25949
+ "dev": true,
25950
+ "requires": {
25951
+ "graceful-fs": "^4.2.0",
25952
+ "jsonfile": "^4.0.0",
25953
+ "universalify": "^0.1.0"
25954
+ }
25955
+ },
25956
+ "jsonfile": {
25957
+ "version": "4.0.0",
25958
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
25959
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
25960
+ "dev": true,
25961
+ "requires": {
25962
+ "graceful-fs": "^4.1.6"
25963
+ }
25964
+ },
25965
+ "log4js": {
25966
+ "version": "6.6.1",
25967
+ "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.6.1.tgz",
25968
+ "integrity": "sha512-J8VYFH2UQq/xucdNu71io4Fo+purYYudyErgBbswWKO0MC6QVOERRomt5su/z6d3RJSmLyTGmXl3Q/XjKCf+/A==",
25969
+ "dev": true,
25970
+ "requires": {
25971
+ "date-format": "^4.0.13",
25972
+ "debug": "^4.3.4",
25973
+ "flatted": "^3.2.6",
25974
+ "rfdc": "^1.3.0",
25975
+ "streamroller": "^3.1.2"
25976
+ }
25977
+ },
25978
+ "streamroller": {
25979
+ "version": "3.1.2",
25980
+ "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.2.tgz",
25981
+ "integrity": "sha512-wZswqzbgGGsXYIrBYhOE0yP+nQ6XRk7xDcYwuQAGTYXdyAUmvgVFE0YU1g5pvQT0m7GBaQfYcSnlHbapuK0H0A==",
25982
+ "dev": true,
25983
+ "requires": {
25984
+ "date-format": "^4.0.13",
25985
+ "debug": "^4.3.4",
25986
+ "fs-extra": "^8.1.0"
25987
+ }
25988
+ }
25989
+ }
25990
+ },
25825
25991
  "@types/mdast": {
25826
25992
  "version": "3.0.10",
25827
25993
  "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@@ -33156,6 +33322,12 @@
33156
33322
  "universalify": "^0.1.2"
33157
33323
  }
33158
33324
  },
33325
+ "jsonschema": {
33326
+ "version": "1.4.1",
33327
+ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz",
33328
+ "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==",
33329
+ "dev": true
33330
+ },
33159
33331
  "jsprim": {
33160
33332
  "version": "1.4.2",
33161
33333
  "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -33415,6 +33587,16 @@
33415
33587
  "graceful-fs": "^4.1.2"
33416
33588
  }
33417
33589
  },
33590
+ "karma-viewport": {
33591
+ "version": "1.0.9",
33592
+ "resolved": "https://registry.npmjs.org/karma-viewport/-/karma-viewport-1.0.9.tgz",
33593
+ "integrity": "sha512-E1xVe66vBQtI66TGOtZMzV5nf6BW5tW4TQVUqPK+oakVLdsG/ZUG688tGK0lL1q0t7nfQD1dwLD8Z9Guu/RVdg==",
33594
+ "dev": true,
33595
+ "requires": {
33596
+ "@types/karma": "^6.3.3",
33597
+ "jsonschema": "^1.4.0"
33598
+ }
33599
+ },
33418
33600
  "karma-webpack": {
33419
33601
  "version": "4.0.2",
33420
33602
  "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.2.tgz",
@@ -34770,9 +34952,9 @@
34770
34952
  }
34771
34953
  },
34772
34954
  "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==",
34955
+ "version": "10.3.0",
34956
+ "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.3.0.tgz",
34957
+ "integrity": "sha512-dAaIs6PhvYGczEsOHGNrI3FKKFDOcm6rSv06SaL9emltqlHqUXt7esun8m4N3D2iL2FzVXKLeMd9/gv/GDryOg==",
34776
34958
  "requires": {
34777
34959
  "black-hole-stream": "0.0.1",
34778
34960
  "fetchres": "^1.7.2",
@@ -4,7 +4,8 @@
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}}>
7
+ {{#if @root.flags.manageArticleLists}}data-myft-ui-save-new="manageArticleLists"{{/if}}
8
+ {{#if @root.flags.manageArticleLists}}data-myft-ui-save-new-config="{{#if @root.flags.myftListPublicPrivateToggle}}showPublicToggle{{/if}}"{{/if}}>
8
9
  {{> n-myft-ui/components/csrf-token/input}}
9
10
  <div
10
11
  class="n-myft-ui__announcement o-normalise-visually-hidden"
package/karma.conf.js CHANGED
@@ -10,7 +10,7 @@ module.exports = function (karma) {
10
10
 
11
11
  // frameworks to use
12
12
  // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13
- frameworks: ['mocha', 'chai', 'sinon', 'sinon-chai'],
13
+ frameworks: ['mocha', 'chai', 'sinon', 'sinon-chai', 'viewport'],
14
14
 
15
15
  // list of files / patterns to load in the browser
16
16
  files: [
@@ -110,7 +110,8 @@ module.exports = function (karma) {
110
110
  require('karma-webpack'),
111
111
  require('karma-chrome-launcher'),
112
112
  require('karma-browserstack-launcher'),
113
- require('karma-html-reporter')
113
+ require('karma-html-reporter'),
114
+ require('karma-viewport')
114
115
  ],
115
116
  client: {
116
117
  mocha: {
package/myft/main.scss CHANGED
@@ -183,6 +183,26 @@ $spacing-unit: 20px;
183
183
  }
184
184
  }
185
185
 
186
+ .myft-ui-create-list-variant-message {
187
+ border-radius: 10px;
188
+ border: 1px solid oColorsByName('black-5');
189
+ background: oColorsByName('white-80');
190
+
191
+ &-content {
192
+ display: flex;
193
+ flex-direction: column;
194
+
195
+ h3 {
196
+ margin: 0;
197
+ }
198
+ }
199
+
200
+ &-buttons {
201
+ text-align: center;
202
+ }
203
+ }
204
+
205
+
186
206
  .myft-ui-create-list-variant {
187
207
  border-radius: 10px;
188
208
  border: 1px solid oColorsByName('black-5');
@@ -239,7 +259,7 @@ $spacing-unit: 20px;
239
259
  }
240
260
 
241
261
  &-add-description {
242
- margin: 4px 0;
262
+ margin: oSpacingByName('s1') 0;
243
263
  }
244
264
 
245
265
  &-heading {
@@ -258,7 +278,7 @@ $spacing-unit: 20px;
258
278
 
259
279
  &-footer {
260
280
  border-top: 1px solid oColorsByName('black-5');
261
- padding: 16px;
281
+ padding: oSpacingByName('s4');
262
282
  }
263
283
 
264
284
  &-icon {
@@ -288,33 +308,89 @@ $spacing-unit: 20px;
288
308
  }
289
309
 
290
310
  &-form {
311
+ $field-spacing: 's4';
291
312
  display: flex;
313
+ flex-direction: column;
292
314
  width: calc(100% - 32px);
293
- justify-content: space-between;
294
- height: 40px;
295
- gap: 8px;
296
- padding: 0 16px 16px;
315
+ gap: oSpacingByName($field-spacing);
316
+ padding: 0 oSpacingByName($field-spacing) oSpacingByName('s3');
297
317
 
298
318
  & > * {
299
319
  flex: 1 1 auto;
320
+ margin-bottom: 0;
300
321
  }
301
322
 
302
323
  .o-forms-input {
303
324
  margin-top: 0;
304
325
  }
326
+
327
+ &-toggle {
328
+ position: absolute;
329
+ }
330
+
331
+ &-toggle-label::after {
332
+ background-color: oColorsByName('white');
333
+ }
334
+
335
+ &-buttons {
336
+ display: flex;
337
+ justify-content: flex-end;
338
+ @include oTypographySans($scale: 2);
339
+ }
340
+
341
+ &-public {
342
+ max-width: 300px;
343
+ padding: 0 3px;
344
+ }
305
345
  }
306
346
 
307
347
  &-lists {
308
- padding: 16px 16px 0;
348
+ padding: oSpacingByName('s4') oSpacingByName('s4') 0;
309
349
  @include oTypographySans($scale: 1);
310
350
  &-text {
311
351
  @include oTypographySans($weight: 'semibold');
312
352
  color: oColorsByName('black-80');
313
- margin-bottom: 16px;
353
+ margin-bottom: oSpacingByName('s3');
314
354
  }
315
355
  &-container {
316
356
  margin-top: 0;
357
+ max-height: 92px;
358
+ padding: 4px 2px;
359
+ overflow-y: auto;
360
+ @include oGridRespondTo($from: M) {
361
+ max-height: 126px;
362
+ }
317
363
  }
318
364
  }
319
365
  }
366
+
367
+ .myft-notification {
368
+ background: oColorsByName('white-80');
369
+ box-sizing: border-box;
370
+ display: flex;
371
+ align-items: center;
372
+ justify-content: center;
373
+ position: absolute;
374
+ border-radius: 5px;
375
+ font-family: MetricWeb, sans-serif;
376
+ font-size: 18px;
377
+ }
378
+
379
+ .share-nav__vertical {
380
+ .myft-notification {
381
+ top: 175px;
382
+ width: 340px;
383
+ height: 44px;
384
+ left: 50px;
385
+ }
386
+ }
387
+
388
+ .share-nav__horizontal {
389
+ .myft-notification {
390
+ top: -52px;
391
+ width: 340px;
392
+ height: 44px;
393
+ z-index: 10;
394
+ }
395
+ }
320
396
  }
@@ -0,0 +1,5 @@
1
+ export default function stringToHTMLElement (string) {
2
+ const template = document.createElement('template');
3
+ template.innerHTML = string.trim();
4
+ return template.content.firstChild;
5
+ }
@@ -0,0 +1,7 @@
1
+ const MOBILE_BREAKPOINT = 980;
2
+
3
+ export default function isMobile () {
4
+ const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
5
+
6
+ return vw <= MOBILE_BREAKPOINT;
7
+ }
@@ -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
@@ -5,8 +5,10 @@ import * as myFtUiButtonStates from './lib/button-states';
5
5
  import nNotification from '@financial-times/n-notification';
6
6
  import { uuid } from 'n-ui-foundations';
7
7
  import getToken from './lib/get-csrf-token';
8
+ import isMobile from './lib/is-mobile';
8
9
  import oForms from '@financial-times/o-forms';
9
10
  import openSaveArticleToListVariant from './save-article-to-list-variant';
11
+ import stringToHTMLElement from './lib/convert-string-to-html-element';
10
12
 
11
13
  const delegate = new Delegate(document.body);
12
14
  const csrfToken = getToken();
@@ -51,6 +53,18 @@ function updateAfterAddToList (listId, contentId, wasAdded) {
51
53
  content: message,
52
54
  trackable: 'myft-feedback-notification'
53
55
  });
56
+
57
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
58
+ detail: {
59
+ category: 'list',
60
+ action: 'copy-success',
61
+ article_id: contentId,
62
+ list_id: listId,
63
+ teamName: 'customer-products-us-growth',
64
+ amplitudeExploratory: true
65
+ },
66
+ bubbles: true
67
+ }));
54
68
  });
55
69
  }
56
70
 
@@ -83,6 +97,10 @@ function setUpCreateListListeners (overlay, contentId) {
83
97
  const createListButton = overlay.content.querySelector('.js-create-list');
84
98
  const nameInput = overlay.content.querySelector('.js-name');
85
99
 
100
+ if (!createListButton) {
101
+ return;
102
+ }
103
+
86
104
  createListButton.addEventListener('click', event => {
87
105
  event.preventDefault();
88
106
 
@@ -162,8 +180,13 @@ function showArticleSavedOverlay (contentId) {
162
180
  showListsOverlay('Article saved', `/myft/list?fragment=true&fromArticleSaved=true&contentId=${contentId}`, contentId);
163
181
  }
164
182
 
165
- function showCreateListAndAddArticleOverlay (contentId, name = 'myft-ui-create-list-variant') {
166
- return openSaveArticleToListVariant(name, contentId);
183
+ function showCreateListAndAddArticleOverlay (contentId, config) {
184
+ const options = {
185
+ name: 'myft-ui-create-list-variant',
186
+ ...config
187
+ };
188
+
189
+ return openSaveArticleToListVariant(contentId, options);
167
190
  }
168
191
 
169
192
  function handleArticleSaved (contentId) {
@@ -176,11 +199,68 @@ function handleArticleSaved (contentId) {
176
199
  });
177
200
  }
178
201
 
179
- function openCreateListAndAddArticleOverlay (contentId) {
202
+ function openCreateListAndAddArticleOverlay (contentId, config) {
180
203
  return myFtClient.getAll('created', 'list')
181
204
  .then(createdLists => createdLists.filter(list => !list.isRedirect))
182
205
  .then(() => {
183
- return showCreateListAndAddArticleOverlay(contentId);
206
+ return showCreateListAndAddArticleOverlay(contentId, config);
207
+ });
208
+ }
209
+
210
+ function handleRemoveToggleSubmit (event) {
211
+ event.preventDefault();
212
+
213
+ const formEl = event.target;
214
+ const submitBtnEl = formEl.querySelector('button[type="submit"]');
215
+
216
+ if (submitBtnEl.hasAttribute('disabled')) {
217
+ return;
218
+ }
219
+
220
+ const isSubmitBtnPressed = submitBtnEl.getAttribute('aria-pressed') === 'true';
221
+ const action = isSubmitBtnPressed ? 'remove' : 'add';
222
+ const contentId = formEl.dataset.contentId;
223
+ const listId = formEl.dataset.actorId;
224
+ const csrfToken = formEl.elements.token;
225
+
226
+ if (!csrfToken || !csrfToken.value) {
227
+ document.body.dispatchEvent(new CustomEvent('oErrors.log', {
228
+ bubbles: true,
229
+ detail: {
230
+ error: new Error('myFT form submitted without a CSRF token'),
231
+ info: {
232
+ action,
233
+ actorType: 'list',
234
+ actorId: listId,
235
+ relationshipName: 'contained',
236
+ subjectType: 'content',
237
+ subjectId: contentId,
238
+ },
239
+ },
240
+ }));
241
+ }
242
+
243
+ submitBtnEl.setAttribute('disabled', '');
244
+
245
+ myFtClient[action]('list', listId, 'contained', 'content', contentId, { token: csrfToken.value })
246
+ .then(() => {
247
+ myFtUiButtonStates.toggleButton(submitBtnEl, !isSubmitBtnPressed);
248
+
249
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
250
+ detail: {
251
+ category: 'list',
252
+ action: action === 'add' ? 'add-success' : 'remove-success',
253
+ article_id: contentId,
254
+ list_id: listId,
255
+ teamName: 'customer-products-us-growth',
256
+ amplitudeExploratory: true
257
+ },
258
+ bubbles: true
259
+ }));
260
+ })
261
+ .catch(error => {
262
+ setTimeout(() => submitBtnEl.removeAttribute('disabled'));
263
+ throw error;
184
264
  });
185
265
  }
186
266
 
@@ -192,14 +272,39 @@ function initialEventListeners () {
192
272
  // Checks if the createListAndSaveArticle variant is active
193
273
  // and will show the variant overlay if the user has no lists,
194
274
  // otherwise it will show the classic overlay
195
- const createNewListDesign = event.currentTarget.querySelector('[data-myft-ui-save-new="manageArticleLists"]');
196
- if (createNewListDesign) {
197
- return openCreateListAndAddArticleOverlay(contentId);
275
+ const newListDesign = event.currentTarget.querySelector('[data-myft-ui-save-new="manageArticleLists"]');
276
+ if (newListDesign) {
277
+ const configKeys = newListDesign.dataset.myftUiSaveNewConfig.split(',');
278
+ const config = configKeys.reduce((configObj, key) => (key ? { ...configObj, [key]: true} : configObj), {});
279
+
280
+ // Temporary events on the public toggle feature.
281
+ // These will be used to build a sanity check dashboard, and will be removed after we get clean-up this test.
282
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
283
+ detail: {
284
+ category: 'publicToggle',
285
+ action: 'savedArticle',
286
+ article_id: contentId,
287
+ teamName: 'customer-products-us-growth',
288
+ amplitudeExploratory: true
289
+ },
290
+ bubbles: true
291
+ }));
292
+
293
+ return openCreateListAndAddArticleOverlay(contentId, config);
198
294
  }
199
295
 
200
296
  handleArticleSaved(contentId);
201
297
  });
202
298
 
299
+ document.body.addEventListener('myft.user.saved.content.remove', event => {
300
+ const contentId = event.detail.subject;
301
+
302
+ const newListDesign = event.currentTarget.querySelector('[data-myft-ui-save-new="manageArticleLists"]');
303
+ if (newListDesign) {
304
+ return showUnsavedNotification(contentId);
305
+ }
306
+ });
307
+
203
308
  delegate.on('click', '[data-myft-ui="copy-to-list"]', event => {
204
309
  event.preventDefault();
205
310
  showCopyToListOverlay(event.target.getAttribute('data-content-id'), event.target.getAttribute('data-actor-id'));
@@ -208,6 +313,36 @@ function initialEventListeners () {
208
313
  ev.preventDefault();
209
314
  showCreateListOverlay();
210
315
  });
316
+
317
+ delegate.on('submit', '[data-myft-ui="contained"]', handleRemoveToggleSubmit);
318
+ }
319
+
320
+ function showUnsavedNotification () {
321
+ const parentSelector = isMobile() ? '.o-share--horizontal' : '.o-share--vertical';
322
+ const parentNode = document.querySelector(parentSelector);
323
+
324
+ // We're not supporting multiple notifications for now
325
+ // If a notification is present, we'll silently avoid showing another
326
+ if (document.querySelector('.myft-notification') || !parentNode) {
327
+ return;
328
+ }
329
+
330
+ const content = `
331
+ <p role="alert">Removed from <a href="https://www.ft.com/myft/saved-articles">saved articles</a> in myFT</p>
332
+ `;
333
+
334
+ const contentNode = stringToHTMLElement(content);
335
+
336
+ const container = document.createElement('div');
337
+ container.className = 'myft-notification';
338
+ container.appendChild(contentNode);
339
+
340
+ parentNode.appendChild(container);
341
+
342
+ setTimeout(
343
+ () => parentNode.removeChild(container),
344
+ 5 * 1000
345
+ );
211
346
  }
212
347
 
213
348
  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,
@@ -2,65 +2,63 @@ import Overlay from '@financial-times/o-overlay';
2
2
  import myFtClient from 'next-myft-client';
3
3
  import { uuid } from 'n-ui-foundations';
4
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';
5
7
 
6
8
  const csrfToken = getToken();
7
9
 
8
10
  let lists = [];
9
11
  let haveLoadedLists = false;
12
+ let createListOverlay;
10
13
 
11
- export default async function openSaveArticleToListVariant (name, contentId) {
12
- function createList (list) {
13
- if(!list) {
14
+ export default async function openSaveArticleToListVariant (contentId, options = {}) {
15
+ const { name, showPublicToggle = false } = options;
16
+
17
+ function createList (newList, cb) {
18
+ if(!newList || !newList.name) {
14
19
  if (!lists.length) attachDescription();
15
- return contentElement.addEventListener('click', openFormHandler, { once: true });
20
+ return restoreFormHandler();
16
21
  }
17
22
 
18
- myFtClient.add('user', null, 'created', 'list', uuid(), { name: list, token: csrfToken })
23
+ myFtClient.add('user', null, 'created', 'list', uuid(), { name: newList.name, token: csrfToken })
19
24
  .then(detail => {
20
- myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((createdList) => {
21
- lists.push({ name: list, uuid: createdList.actorId, checked: true });
25
+ myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((data) => {
26
+ const createdList = { name: newList.name, uuid: data.actorId, checked: true, isShareable: !!newList.isShareable };
27
+ lists.unshift(createdList);
22
28
  const listElement = ListsElement(lists, addToList, removeFromList);
23
29
  const overlayContent = document.querySelector('.o-overlay__content');
24
30
  overlayContent.insertAdjacentElement('afterbegin', listElement);
25
31
  const announceListContainer = document.querySelector('.myft-ui-create-list-variant-announcement');
26
- announceListContainer.textContent = `${list} created`;
27
- triggerCreateListEvent(contentId);
28
- contentElement.addEventListener('click', openFormHandler, { once: true });
32
+ announceListContainer.textContent = `${newList.name} created`;
33
+ restoreFormHandler();
34
+ cb(contentId, createdList);
29
35
  });
30
36
  })
31
37
  .catch(() => {
32
38
  if (!lists.length) attachDescription();
33
- return contentElement.addEventListener('click', openFormHandler, { once: true });
39
+ return restoreFormHandler();
34
40
  });
35
41
  }
36
42
 
37
- function addToList (list) {
43
+ function addToList (list, cb) {
38
44
  if(!list) {
39
45
  return;
40
46
  }
41
47
 
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);
48
+ myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((addedList) => {
49
+ cb();
50
+ triggerAddToListEvent(contentId, addedList.actorId);
49
51
  });
50
52
  }
51
53
 
52
- function removeFromList (list) {
54
+ function removeFromList (list, cb) {
53
55
  if(!list) {
54
56
  return;
55
57
  }
56
58
 
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);
59
+ myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((removedList) => {
60
+ cb();
61
+ triggerRemoveFromListEvent(contentId, removedList.actorId);
64
62
  });
65
63
  }
66
64
 
@@ -76,9 +74,9 @@ export default async function openSaveArticleToListVariant (name, contentId) {
76
74
  }
77
75
 
78
76
  const headingElement = HeadingElement();
79
- let [contentElement, removeDescription, attachDescription] = ContentElement(!lists.length);
77
+ let [contentElement, removeDescription, attachDescription, restoreFormHandler] = ContentElement(!lists.length, openFormHandler);
80
78
 
81
- const createListOverlay = new Overlay(name, {
79
+ createListOverlay = new Overlay(name, {
82
80
  html: contentElement,
83
81
  heading: { title: headingElement.outerHTML },
84
82
  modal: false,
@@ -88,43 +86,40 @@ export default async function openSaveArticleToListVariant (name, contentId) {
88
86
 
89
87
  function outsideClickHandler (e) {
90
88
  const overlayContent = document.querySelector('.o-overlay__content');
91
- if(createListOverlay.visible && (!overlayContent || !overlayContent.contains(e.target))) {
89
+ const overlayContainer = document.querySelector('.o-overlay');
90
+ // we don't want to close the overlay if the click happened inside the
91
+ // overlay, except if the click happened on the overlay close button
92
+ const isTargetInsideOverlay = overlayContainer.contains(e.target) && !e.target.classList.contains('o-overlay__close');
93
+ if(createListOverlay.visible && (!overlayContent || !isTargetInsideOverlay)) {
92
94
  createListOverlay.close();
93
95
  }
94
96
  }
95
97
 
96
98
  function openFormHandler () {
97
- const formElement = FormElement(createList);
99
+ const formElement = FormElement(createList, showPublicToggle, restoreFormHandler, attachDescription);
98
100
  const overlayContent = document.querySelector('.o-overlay__content');
99
101
  removeDescription();
100
102
  overlayContent.insertAdjacentElement('beforeend', formElement);
101
103
  formElement.elements[0].focus();
102
104
  }
103
105
 
104
- function getScrollHandler (target) {
105
- return realignOverlay(window.scrollY, target);
106
- }
107
-
108
- function resizeHandler () {
109
- positionOverlay(createListOverlay.wrapper);
110
- }
111
-
112
106
  createListOverlay.open();
113
107
 
114
108
  const scrollHandler = getScrollHandler(createListOverlay.wrapper);
109
+ const resizeHandler = getResizeHandler(createListOverlay.wrapper);
115
110
 
116
111
  createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
117
- positionOverlay(data.currentTarget);
118
-
119
112
  if (lists.length) {
120
113
  const listElement = ListsElement(lists, addToList, removeFromList);
121
114
  const overlayContent = document.querySelector('.o-overlay__content');
122
115
  overlayContent.insertAdjacentElement('afterbegin', listElement);
123
116
  }
124
117
 
125
- contentElement.addEventListener('click', openFormHandler, { once: true });
118
+ positionOverlay(data.currentTarget);
119
+
120
+ restoreFormHandler();
126
121
 
127
- document.querySelector('.article-content').addEventListener('click', outsideClickHandler, { once: true });
122
+ document.querySelector('.article-content').addEventListener('click', outsideClickHandler);
128
123
 
129
124
  window.addEventListener('scroll', scrollHandler);
130
125
 
@@ -135,26 +130,95 @@ export default async function openSaveArticleToListVariant (name, contentId) {
135
130
  window.removeEventListener('scroll', scrollHandler);
136
131
 
137
132
  window.removeEventListener('oViewport.resize', resizeHandler);
133
+
134
+ document.querySelector('.article-content').removeEventListener('click', outsideClickHandler);
138
135
  });
139
136
  }
140
137
 
141
- function stringToHTMLElement (string) {
142
- const template = document.createElement('template');
143
- template.innerHTML = string.trim();
144
- return template.content.firstChild;
138
+ function showMessageOverlay () {
139
+ function onContinue () {
140
+ messageOverlay.destroy();
141
+ createListOverlay.show();
142
+ triggerAcknowledgeMessageEvent();
143
+ }
144
+
145
+ const messageElement = MessageElement(onContinue);
146
+
147
+ const messageOverlay = new Overlay('myft-ui-create-list-variant-message', {
148
+ html: messageElement,
149
+ modal: false,
150
+ parentnode: isMobile() ? '.o-share--horizontal' : '.o-share--vertical',
151
+ class: 'myft-ui-create-list-variant-message',
152
+ });
153
+
154
+ const scrollHandler = getScrollHandler(messageOverlay.wrapper);
155
+ const resizeHandler = getResizeHandler(messageOverlay.wrapper);
156
+
157
+ messageOverlay.open();
158
+
159
+ messageOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
160
+ positionOverlay(data.currentTarget);
161
+
162
+ window.addEventListener('scroll', scrollHandler);
163
+
164
+ window.addEventListener('oViewport.resize', resizeHandler);
165
+ });
166
+
167
+ messageOverlay.wrapper.addEventListener('oOverlay.destroy', () => {
168
+ window.removeEventListener('scroll', scrollHandler);
169
+
170
+ window.removeEventListener('oViewport.resize', resizeHandler);
171
+ });
172
+
173
+ return messageOverlay;
174
+ }
175
+
176
+ function getScrollHandler (target) {
177
+ return realignOverlay(window.scrollY, target);
145
178
  }
146
179
 
147
- function FormElement (createList) {
180
+ function getResizeHandler (target) {
181
+ return function resizeHandler () {
182
+ positionOverlay(target);
183
+ };
184
+ }
185
+
186
+ function FormElement (createList, showPublicToggle, restoreFormHandler, attachDescription) {
148
187
  const formString = `
149
188
  <form class="myft-ui-create-list-variant-form">
150
- <label class="o-forms-field">
189
+ <label class="myft-ui-create-list-variant-form-name o-forms-field">
151
190
  <span class="o-forms-input o-forms-input--text">
152
- <input type="text" name="list-name" aria-label="List name">
191
+ <input class="myft-ui-create-list-variant-text" type="text" name="list-name" aria-label="List name">
153
192
  </span>
154
193
  </label>
155
- <button class="o-buttons o-buttons--secondary" type="submit">
156
- Save
157
- </button>
194
+
195
+ ${showPublicToggle ?
196
+ `<div class="myft-ui-create-list-variant-form-public o-forms-field" role="group">
197
+ <span class="o-forms-input o-forms-input--toggle">
198
+ <label>
199
+ <input class="myft-ui-create-list-variant-form-toggle" type="checkbox" name="is-shareable" value="public" checked data-trackable="private-link" text="private">
200
+ <span class="myft-ui-create-list-variant-form-toggle-label o-forms-input__label">
201
+ <span class="o-forms-input__label__main">
202
+ Public
203
+ </span>
204
+ <span id="myft-ui-create-list-variant-form-public-description" class="o-forms-input__label__prompt">
205
+ Your profession & list will be visible to others
206
+ </span>
207
+ </span>
208
+ </label>
209
+ </span>
210
+ </div>` :
211
+ ''
212
+ }
213
+
214
+ <div class="myft-ui-create-list-variant-form-buttons">
215
+ <button class="o-buttons o-buttons--primary o-buttons--inverse o-buttons--big" type="button" data-trackable="cancel-link" text="cancel">
216
+ Cancel
217
+ </button>
218
+ <button class="o-buttons o-buttons--big o-buttons--secondary" type="submit">
219
+ Add
220
+ </button>
221
+ </div>
158
222
  </form>
159
223
  `;
160
224
 
@@ -164,22 +228,54 @@ function FormElement (createList) {
164
228
  event.preventDefault();
165
229
  event.stopPropagation();
166
230
  const inputListName = formElement.querySelector('input[name="list-name"]');
167
- createList(inputListName.value);
168
- inputListName.value = '';
231
+ const inputIsShareable = formElement.querySelector('input[name="is-shareable"]');
232
+
233
+ const newList = {
234
+ name: inputListName.value,
235
+ isShareable: inputIsShareable ? inputIsShareable.checked : false
236
+ };
237
+
238
+ createList(newList, ((contentId, createdList) => {
239
+ triggerCreateListEvent(contentId, createdList.uuid);
240
+ triggerAddToListEvent(contentId, createdList.uuid);
241
+ positionOverlay(createListOverlay.wrapper);
242
+ triggerCancelEvent();
243
+
244
+ if (createdList.isShareable) {
245
+ createListOverlay.close();
246
+ showMessageOverlay();
247
+ }
248
+ }));
249
+ formElement.remove();
250
+ }
251
+
252
+ function handleCancelClick (event) {
253
+ event.preventDefault();
254
+ event.stopPropagation();
169
255
  formElement.remove();
256
+ if (!lists.length) attachDescription();
257
+ restoreFormHandler();
258
+ }
259
+
260
+ function onPublicToggleClick (event) {
261
+ event.target.setAttribute('data-trackable', event.target.checked ? 'private-link' : 'public-link');
262
+ event.target.setAttribute('text', event.target.checked ? 'private' : 'public');
263
+ triggerPublicToggleEvent(event.target.checked);
170
264
  }
171
265
 
172
266
  formElement.querySelector('button[type="submit"]').addEventListener('click', handleSubmit);
267
+ formElement.querySelector('button[type="button"]').addEventListener('click', handleCancelClick);
268
+ formElement.querySelector('input[name="is-shareable"]').addEventListener('click', onPublicToggleClick);
173
269
 
174
270
  return formElement;
175
271
  }
176
272
 
177
- function ContentElement (hasDescription) {
273
+ function ContentElement (hasDescription, onClick) {
178
274
  const description = '<p class="myft-ui-create-list-variant-add-description">Lists are a simple way to curate your content</p>';
179
275
 
180
276
  const content = `
181
277
  <div class="myft-ui-create-list-variant-footer">
182
- <button class="myft-ui-create-list-variant-add">Add to a new list</button>
278
+ <button class="myft-ui-create-list-variant-add" data-trackable="add-to-new-list" text="add to new list">Add to a new list</button>
183
279
  ${hasDescription ? `
184
280
  ${description}
185
281
  ` : ''}
@@ -188,6 +284,8 @@ function ContentElement (hasDescription) {
188
284
 
189
285
  const contentElement = stringToHTMLElement(content);
190
286
 
287
+ contentElement.querySelector('.myft-ui-create-list-variant-add').addEventListener('click', triggerAddToNewListEvent);
288
+
191
289
  function removeDescription () {
192
290
  const descriptionElement = contentElement.querySelector('.myft-ui-create-list-variant-add-description');
193
291
  if (descriptionElement) {
@@ -200,12 +298,16 @@ function ContentElement (hasDescription) {
200
298
  contentElement.insertAdjacentElement('beforeend', descriptionElement);
201
299
  }
202
300
 
203
- return [contentElement, removeDescription, attachDescription];
301
+ function restoreFormHandler () {
302
+ return contentElement.addEventListener('click', onClick, { once: true });
303
+ }
304
+
305
+ return [contentElement, removeDescription, attachDescription, restoreFormHandler];
204
306
  }
205
307
 
206
308
  function HeadingElement () {
207
309
  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>
310
+ <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
311
  `;
210
312
 
211
313
  return stringToHTMLElement(heading);
@@ -221,7 +323,7 @@ function ListsElement (lists, addToList, removeFromList) {
221
323
 
222
324
  const listsTemplate = `
223
325
  <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>
326
+ <span class="myft-ui-create-list-variant-lists-text">Add to list</span>
225
327
  <span class="myft-ui-create-list-variant-lists-container o-forms-input o-forms-input--checkbox">
226
328
  </span>
227
329
  </div>
@@ -237,8 +339,9 @@ function ListsElement (lists, addToList, removeFromList) {
237
339
 
238
340
  function ListCheckboxElement (addToList, removeFromList) {
239
341
  return function (list) {
342
+
240
343
  const listCheckbox = `<label>
241
- <input type="checkbox" name="default" value="${list.name}" ${list.checked ? 'checked' : ''}>
344
+ <input type="checkbox" name="default" value="${list.uuid}" ${list.checked ? 'checked' : ''}>
242
345
  <span class="o-forms-input__label">
243
346
  <span class="o-normalise-visually-hidden">
244
347
  ${list.checked ? 'Remove article from ' : 'Add article to ' }
@@ -254,7 +357,15 @@ function ListCheckboxElement (addToList, removeFromList) {
254
357
 
255
358
  function handleCheck (event) {
256
359
  event.preventDefault();
257
- return event.target.checked ? addToList(list) : removeFromList(list);
360
+ const isChecked = event.target.checked;
361
+
362
+ function onListUpdated () {
363
+ const indexToUpdate = lists.indexOf(list);
364
+ lists[indexToUpdate] = { ...lists[indexToUpdate], checked: isChecked };
365
+ listCheckboxElement.querySelector('input').checked = isChecked;
366
+ }
367
+
368
+ return isChecked ? addToList(list, onListUpdated) : removeFromList(list, onListUpdated);
258
369
  }
259
370
 
260
371
  input.addEventListener('click', handleCheck);
@@ -263,6 +374,28 @@ function ListCheckboxElement (addToList, removeFromList) {
263
374
  };
264
375
  }
265
376
 
377
+ function MessageElement (onContinue) {
378
+ const message = `
379
+ <div class="myft-ui-create-list-variant-message-content" >
380
+ <div class="myft-ui-create-list-variant-message-text" aria-live="polite">
381
+ <h3>Thank you for your interest in making a public list</h3>
382
+ <p>We're currently testing this feature. For now, your list remains private and isn't visible to others.</p>
383
+ </div>
384
+ <div class="myft-ui-create-list-variant-message-buttons">
385
+ <button class="o-buttons o-buttons--big o-buttons--secondary" data-trackable="continue-link" text="continue">
386
+ Continue
387
+ </button>
388
+ </div>
389
+ </div>
390
+ `;
391
+
392
+ const messageElement = stringToHTMLElement(message);
393
+
394
+ messageElement.querySelector('button').addEventListener('click', onContinue);
395
+
396
+ return messageElement;
397
+ }
398
+
266
399
  function realignOverlay (originalScrollPosition, target) {
267
400
  return function () {
268
401
  const currentScrollPosition = window.scrollY;
@@ -282,28 +415,22 @@ function realignOverlay (originalScrollPosition, target) {
282
415
  function positionOverlay (target) {
283
416
  target.style['min-width'] = '340px';
284
417
  target.style['width'] = '100%';
285
- target.style['margin-top'] = '-50px';
418
+ target.style['margin-top'] = 0;
286
419
  target.style['left'] = 0;
420
+ target.style['top'] = 0;
287
421
 
288
422
  if (isMobile()) {
289
423
  const shareNavComponent = document.querySelector('.share-nav__horizontal');
424
+ const topHalfOffset = target.clientHeight + 10;
290
425
  target.style['position'] = 'absolute';
291
426
  target.style['margin-left'] = 0;
292
- target.style['margin-top'] = 0;
293
- target.style['top'] = calculateLargerScreenHalf(shareNavComponent) === 'ABOVE' ? '-120px' : '50px';
427
+ target.style['top'] = calculateLargerScreenHalf(shareNavComponent) === 'ABOVE' ? `-${topHalfOffset}px` : '50px';
294
428
  } else {
295
429
  target.style['position'] = 'absolute';
296
430
  target.style['margin-left'] = '45px';
297
- target.style['top'] = '220px';
298
431
  }
299
432
  }
300
433
 
301
- function isMobile () {
302
- const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
303
-
304
- return vw <= 980;
305
- }
306
-
307
434
  function calculateLargerScreenHalf (target) {
308
435
  if (!target) {
309
436
  return 'BELOW';
@@ -318,20 +445,21 @@ function calculateLargerScreenHalf (target) {
318
445
  return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
319
446
  }
320
447
 
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
- });
448
+ async function getLists (contentId) {
449
+ return myFtClient.getListsContent()
450
+ .then(results => results.items.map(list => {
451
+ const isChecked = Array.isArray(list.content) && list.content.some(content => content.uuid === contentId);
452
+ return { name: list.name, uuid: list.uuid, checked: isChecked, content: list.content, isShareable: false };
453
+ }));
327
454
  }
328
455
 
329
- function triggerAddToListEvent (contentId) {
456
+ function triggerAddToListEvent (contentId, listId) {
330
457
  return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
331
458
  detail: {
332
- category: 'professorLists',
333
- action: 'add-to-list',
459
+ category: 'list',
460
+ action: 'add-success',
334
461
  article_id: contentId,
462
+ list_id: listId,
335
463
  teamName: 'customer-products-us-growth',
336
464
  amplitudeExploratory: true
337
465
  },
@@ -339,12 +467,13 @@ function triggerAddToListEvent (contentId) {
339
467
  }));
340
468
  }
341
469
 
342
- function triggerRemoveFromListEvent (contentId) {
470
+ function triggerRemoveFromListEvent (contentId, listId) {
343
471
  return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
344
472
  detail: {
345
- category: 'professorLists',
346
- action: 'remove-from-list',
473
+ category: 'list',
474
+ action: 'remove-success',
347
475
  article_id: contentId,
476
+ list_id: listId,
348
477
  teamName: 'customer-products-us-growth',
349
478
  amplitudeExploratory: true
350
479
  },
@@ -352,23 +481,71 @@ function triggerRemoveFromListEvent (contentId) {
352
481
  }));
353
482
  }
354
483
 
355
- function triggerCreateListEvent (contentId) {
484
+ function triggerCreateListEvent (contentId, listId) {
356
485
  document.body.dispatchEvent(new CustomEvent('oTracking.event', {
357
486
  detail: {
358
- category: 'professorLists',
359
- action: 'create-list',
487
+ category: 'list',
488
+ action: 'create-success',
360
489
  article_id: contentId,
490
+ list_id: listId,
361
491
  teamName: 'customer-products-us-growth',
362
492
  amplitudeExploratory: true
363
493
  },
364
494
  bubbles: true
365
495
  }));
496
+ }
366
497
 
367
- return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
498
+ // Temporary event on the public toggle feature.
499
+ // These will be used to build a sanity check dashboard, and will be removed after we get clean-up this test.
500
+ function triggerPublicToggleEvent (isPublic) {
501
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
502
+ detail: {
503
+ category: 'publicToggle',
504
+ action: `${isPublic ? 'setPublic' : 'setPrivate'}`,
505
+ teamName: 'customer-products-us-growth',
506
+ amplitudeExploratory: true
507
+ },
508
+ bubbles: true
509
+ }));
510
+ }
511
+
512
+ // Temporary event on the public toggle feature.
513
+ // These will be used to build a sanity check dashboard, and will be removed after we get clean-up this test.
514
+ function triggerAddToNewListEvent () {
515
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
516
+ detail: {
517
+ category: 'publicToggle',
518
+ action: 'addToNewList',
519
+ teamName: 'customer-products-us-growth',
520
+ amplitudeExploratory: true
521
+ },
522
+ bubbles: true
523
+ }));
524
+ }
525
+
526
+ // Temporary event on the public toggle feature.
527
+ // These will be used to build a sanity check dashboard, and will be removed after we get clean-up this test.
528
+ function triggerAcknowledgeMessageEvent () {
529
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
368
530
  detail: {
369
- category: 'myFT',
370
- action: 'create-list-success',
371
- article_id: contentId
531
+ category: 'publicToggle',
532
+ action: 'acknowledgeMessage',
533
+ teamName: 'customer-products-us-growth',
534
+ amplitudeExploratory: true
535
+ },
536
+ bubbles: true
537
+ }));
538
+ }
539
+
540
+ // Temporary event on the public toggle feature.
541
+ // These will be used to build a sanity check dashboard, and will be removed after we get clean-up this test.
542
+ function triggerCancelEvent () {
543
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
544
+ detail: {
545
+ category: 'publicToggle',
546
+ action: 'cancel ',
547
+ teamName: 'customer-products-us-growth',
548
+ amplitudeExploratory: true
372
549
  },
373
550
  bubbles: true
374
551
  }));
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.2",
4
4
  "description": "Client side component for interaction with myft",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -65,6 +65,7 @@
65
65
  "karma-sinon": "^1.0.5",
66
66
  "karma-sinon-chai": "2.0.2",
67
67
  "karma-sourcemap-loader": "^0.3.7",
68
+ "karma-viewport": "^1.0.9",
68
69
  "karma-webpack": "^4.0.2",
69
70
  "lintspaces-cli": "^0.7.0",
70
71
  "lolex": "5.1.1",
@@ -103,13 +104,13 @@
103
104
  "form-serialize": "^0.7.2",
104
105
  "ftdomdelegate": "^4.0.6",
105
106
  "js-cookie": "^2.2.1",
106
- "next-myft-client": "^10.1.0",
107
+ "next-myft-client": "^10.3.0",
107
108
  "next-session-client": "^4.0.0",
108
109
  "superstore-sync": "^2.1.1"
109
110
  },
110
111
  "volta": {
111
112
  "node": "16.14.2",
112
- "npm": "7.20.2"
113
+ "npm": "7.24.2"
113
114
  },
114
115
  "engines": {
115
116
  "node": "14.x || 16.x",
@@ -0,0 +1,20 @@
1
+ const expect = require('chai').expect;
2
+ const isMobile = require('../../myft/ui/lib/is-mobile');
3
+
4
+ /* global viewport */
5
+
6
+ describe('IsMobile', function () {
7
+ afterEach(() => {
8
+ viewport.reset();
9
+ });
10
+
11
+ it('detects a desktop device', () => {
12
+ viewport.set(981, 800);
13
+ expect(isMobile()).to.be.false;
14
+ });
15
+
16
+ it('detects a mobile device', () => {
17
+ viewport.set(979, 800);
18
+ expect(isMobile()).to.be.true;
19
+ });
20
+ });