@financial-times/n-myft-ui 30.1.1 → 30.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.
@@ -4,8 +4,7 @@
|
|
4
4
|
data-myft-ui="saved"
|
5
5
|
action="/myft/save/{{contentId}}"
|
6
6
|
data-js-action="/__myft/api/core/saved/content/{{contentId}}?method=put"
|
7
|
-
|
8
|
-
{{#if @root.flags.manageArticleLists}}data-myft-ui-save-new-config="{{#if modal}}modal{{/if}}"{{/if}}>
|
7
|
+
data-myft-ui-save-config="{{#if nonModal}}nonModal{{/if}}">
|
9
8
|
{{> n-myft-ui/components/csrf-token/input}}
|
10
9
|
<div
|
11
10
|
class="n-myft-ui__announcement o-normalise-visually-hidden"
|
package/myft/main.scss
CHANGED
@@ -174,7 +174,7 @@ $spacing-unit: 20px;
|
|
174
174
|
|
175
175
|
}
|
176
176
|
|
177
|
-
.myft-ui-create-list
|
177
|
+
.myft-ui-create-list {
|
178
178
|
border-radius: 10px;
|
179
179
|
border: 1px solid oColorsByName('black-5');
|
180
180
|
background: oColorsByName('white-80');
|
@@ -193,7 +193,7 @@ $spacing-unit: 20px;
|
|
193
193
|
}
|
194
194
|
}
|
195
195
|
|
196
|
-
.myft-ui-create-list
|
196
|
+
.myft-ui-create-list {
|
197
197
|
@include oButtons($opts: (
|
198
198
|
'sizes': ('big'),
|
199
199
|
'types': ('primary', 'secondary'),
|
@@ -0,0 +1,15 @@
|
|
1
|
+
export default function escapeText (text) {
|
2
|
+
if (typeof text !== 'string' || !text instanceof String) {
|
3
|
+
return '';
|
4
|
+
}
|
5
|
+
|
6
|
+
const tagsToReplace = {
|
7
|
+
'&': '&',
|
8
|
+
'<': '<',
|
9
|
+
'>': '>'
|
10
|
+
};
|
11
|
+
|
12
|
+
return text.replace(/[&<>]/g, function (tag) {
|
13
|
+
return tagsToReplace[tag] || tag;
|
14
|
+
});
|
15
|
+
}
|
package/myft/ui/lists.js
CHANGED
@@ -1,330 +1,514 @@
|
|
1
1
|
import myFtClient from 'next-myft-client';
|
2
|
-
import
|
2
|
+
import isMobile from './lib/is-mobile';
|
3
|
+
import escapeText from './lib/escape-text';
|
3
4
|
import Overlay from '@financial-times/o-overlay';
|
4
|
-
import * as myFtUiButtonStates from './lib/button-states';
|
5
|
-
import nNotification from '@financial-times/n-notification';
|
6
5
|
import { uuid } from 'n-ui-foundations';
|
7
6
|
import getToken from './lib/get-csrf-token';
|
8
|
-
import isMobile from './lib/is-mobile';
|
9
|
-
import oForms from '@financial-times/o-forms';
|
10
|
-
import openSaveArticleToListVariant from './save-article-to-list-variant';
|
11
7
|
import stringToHTMLElement from './lib/convert-string-to-html-element';
|
12
8
|
|
13
|
-
const delegate = new Delegate(document.body);
|
14
9
|
const csrfToken = getToken();
|
15
10
|
|
11
|
+
let lists = [];
|
12
|
+
let haveLoadedLists = false;
|
13
|
+
let createListOverlay;
|
14
|
+
|
15
|
+
async function openSaveArticleToList (contentId, options = {}) {
|
16
|
+
const { name, nonModal = false } = options;
|
17
|
+
const modal = !nonModal;
|
18
|
+
function createList (newList, cb) {
|
19
|
+
if(!newList || !newList.name) {
|
20
|
+
return restoreContent();
|
21
|
+
}
|
22
|
+
|
23
|
+
myFtClient.add('user', null, 'created', 'list', uuid(), { name: newList.name, token: csrfToken, isPublic: newList.isPublic })
|
24
|
+
.then(detail => {
|
25
|
+
myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((data) => {
|
26
|
+
const createdList = { name: newList.name, uuid: data.actorId, checked: true, isPublic: !!newList.isPublic };
|
27
|
+
lists.unshift(createdList);
|
28
|
+
const announceListContainer = document.querySelector('.myft-ui-create-list-announcement');
|
29
|
+
announceListContainer.textContent = `${newList.name} created`;
|
30
|
+
cb(contentId, createdList);
|
31
|
+
});
|
32
|
+
})
|
33
|
+
.catch(() => {
|
34
|
+
return restoreContent();
|
35
|
+
});
|
36
|
+
}
|
37
|
+
|
38
|
+
function addToList (list, cb) {
|
39
|
+
if(!list) {
|
40
|
+
return;
|
41
|
+
}
|
42
|
+
|
43
|
+
myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((addedList) => {
|
44
|
+
cb();
|
45
|
+
triggerAddToListEvent(contentId, addedList.actorId);
|
46
|
+
});
|
47
|
+
}
|
48
|
+
|
49
|
+
function removeFromList (list, cb) {
|
50
|
+
if(!list) {
|
51
|
+
return;
|
52
|
+
}
|
53
|
+
|
54
|
+
myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((removedList) => {
|
55
|
+
cb();
|
56
|
+
triggerRemoveFromListEvent(contentId, removedList.actorId);
|
57
|
+
});
|
58
|
+
}
|
59
|
+
|
60
|
+
function restoreContent () {
|
61
|
+
if (!lists.length) attachDescription();
|
62
|
+
refreshListElement();
|
63
|
+
showListElement();
|
64
|
+
return restoreFormHandler();
|
65
|
+
}
|
66
|
+
|
67
|
+
if (!haveLoadedLists) {
|
68
|
+
lists = await getLists(contentId);
|
69
|
+
haveLoadedLists = true;
|
70
|
+
}
|
16
71
|
|
17
|
-
function openOverlay (html, { name = 'myft-ui', title = ' ', shaded = false, trigger = false }) {
|
18
|
-
// If an overlay already exists of the same name destroy it.
|
19
72
|
const overlays = Overlay.getOverlays();
|
20
73
|
const existingOverlay = overlays[name];
|
21
74
|
if (existingOverlay) {
|
22
75
|
existingOverlay.destroy();
|
23
76
|
}
|
24
|
-
|
25
|
-
const
|
26
|
-
|
27
|
-
|
77
|
+
|
78
|
+
const headingElement = HeadingElement();
|
79
|
+
let [contentElement, removeDescription, attachDescription, restoreFormHandler] = ContentElement(!lists.length, openFormHandler);
|
80
|
+
const [listElement, refreshListElement, hideListElement, showListElement] = ListsElement(lists, addToList, removeFromList);
|
81
|
+
|
82
|
+
createListOverlay = new Overlay(name, {
|
83
|
+
modal,
|
84
|
+
html: contentElement,
|
85
|
+
heading: { title: headingElement.outerHTML },
|
86
|
+
parentnode: isMobile() ? '.o-share--horizontal' : '.o-share--vertical',
|
87
|
+
class: 'myft-ui-create-list',
|
28
88
|
});
|
29
89
|
|
30
|
-
|
90
|
+
function outsideClickHandler (e) {
|
91
|
+
const overlayContent = document.querySelector('.o-overlay__content');
|
92
|
+
const overlayContainer = document.querySelector('.o-overlay');
|
93
|
+
// we don't want to close the overlay if the click happened inside the
|
94
|
+
// overlay, except if the click happened on the overlay close button
|
95
|
+
const isTargetInsideOverlay = overlayContainer.contains(e.target) && !e.target.classList.contains('o-overlay__close');
|
96
|
+
if(createListOverlay.visible && (!overlayContent || !isTargetInsideOverlay)) {
|
97
|
+
createListOverlay.close();
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
function onFormCancel () {
|
102
|
+
showListElement();
|
103
|
+
restoreFormHandler();
|
104
|
+
}
|
105
|
+
|
106
|
+
function onFormListCreated () {
|
107
|
+
refreshListElement();
|
108
|
+
showListElement();
|
109
|
+
restoreFormHandler();
|
110
|
+
}
|
31
111
|
|
32
|
-
|
33
|
-
|
112
|
+
function openFormHandler () {
|
113
|
+
hideListElement();
|
114
|
+
const formElement = FormElement(createList, attachDescription, onFormListCreated, onFormCancel, modal);
|
115
|
+
const overlayContent = document.querySelector('.o-overlay__content');
|
116
|
+
removeDescription();
|
117
|
+
overlayContent.insertAdjacentElement('beforeend', formElement);
|
34
118
|
}
|
35
119
|
|
36
|
-
|
37
|
-
|
120
|
+
createListOverlay.open();
|
121
|
+
|
122
|
+
const scrollHandler = getScrollHandler(createListOverlay.wrapper);
|
123
|
+
const resizeHandler = getResizeHandler(createListOverlay.wrapper);
|
124
|
+
|
125
|
+
createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
|
126
|
+
const overlayContent = document.querySelector('.o-overlay__content');
|
127
|
+
overlayContent.insertAdjacentElement('afterbegin', listElement);
|
128
|
+
if (!lists.length) {
|
129
|
+
hideListElement();
|
130
|
+
}
|
131
|
+
|
132
|
+
if (!modal) {
|
133
|
+
positionOverlay(data.currentTarget);
|
134
|
+
|
135
|
+
window.addEventListener('oViewport.resize', resizeHandler);
|
136
|
+
window.addEventListener('scroll', scrollHandler);
|
137
|
+
}
|
138
|
+
|
139
|
+
restoreFormHandler();
|
140
|
+
|
141
|
+
document.body.addEventListener('click', outsideClickHandler);
|
142
|
+
|
143
|
+
|
144
|
+
});
|
145
|
+
|
146
|
+
createListOverlay.wrapper.addEventListener('oOverlay.destroy', () => {
|
147
|
+
window.removeEventListener('scroll', scrollHandler);
|
148
|
+
|
149
|
+
window.removeEventListener('oViewport.resize', resizeHandler);
|
150
|
+
|
151
|
+
document.body.removeEventListener('click', outsideClickHandler);
|
38
152
|
});
|
39
153
|
}
|
40
154
|
|
41
|
-
function
|
155
|
+
function getScrollHandler (target) {
|
156
|
+
return realignOverlay(window.scrollY, target);
|
157
|
+
}
|
158
|
+
|
159
|
+
function getResizeHandler (target) {
|
160
|
+
return function resizeHandler () {
|
161
|
+
positionOverlay(target);
|
162
|
+
};
|
163
|
+
}
|
164
|
+
|
165
|
+
function FormElement (createList, attachDescription, onListCreated, onCancel, modal=true) {
|
166
|
+
const formString = `
|
167
|
+
<form class="myft-ui-create-list-form">
|
168
|
+
<label class="myft-ui-create-list-form-name o-forms-field">
|
169
|
+
<span class="o-forms-input o-forms-input--text">
|
170
|
+
<input class="myft-ui-create-list-text" type="text" name="list-name">
|
171
|
+
<div class="myft-ui-create-list-label">List name</div>
|
172
|
+
</span>
|
173
|
+
</label>
|
174
|
+
|
175
|
+
<div class="myft-ui-create-list-form-public o-forms-field" role="group">
|
176
|
+
<span class="o-forms-input o-forms-input--toggle">
|
177
|
+
<label>
|
178
|
+
<input class="myft-ui-create-list-form-toggle" type="checkbox" name="is-public" value="public" checked data-trackable="private-link" text="private">
|
179
|
+
<span class="myft-ui-create-list-form-toggle-label o-forms-input__label">
|
180
|
+
<span class="o-forms-input__label__main">
|
181
|
+
Public
|
182
|
+
</span>
|
183
|
+
<span id="myft-ui-create-list-form-public-description" class="o-forms-input__label__prompt">
|
184
|
+
Your profession & list will be visible to others
|
185
|
+
</span>
|
186
|
+
</span>
|
187
|
+
</label>
|
188
|
+
</span>
|
189
|
+
</div>
|
190
|
+
|
191
|
+
<div class="myft-ui-create-list-form-buttons">
|
192
|
+
<button class="o-buttons o-buttons--primary o-buttons--inverse o-buttons--big" type="button" data-trackable="cancel-link" text="cancel">
|
193
|
+
Cancel
|
194
|
+
</button>
|
195
|
+
<button class="o-buttons o-buttons--big o-buttons--secondary" type="submit">
|
196
|
+
Add
|
197
|
+
</button>
|
198
|
+
</div>
|
199
|
+
</form>
|
200
|
+
`;
|
42
201
|
|
43
|
-
|
202
|
+
const formElement = stringToHTMLElement(formString);
|
203
|
+
|
204
|
+
function handleSubmit (event) {
|
205
|
+
event.preventDefault();
|
206
|
+
event.stopPropagation();
|
207
|
+
const inputListName = formElement.querySelector('input[name="list-name"]');
|
208
|
+
const inputIsPublic = formElement.querySelector('input[name="is-public"]');
|
44
209
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
${wasAdded ? `Article added to your list.
|
50
|
-
<a href="${personalisedUrl}" data-trackable="alerts">View list</a>` : 'Article removed from your list'}
|
51
|
-
`;
|
210
|
+
const newList = {
|
211
|
+
name: inputListName.value,
|
212
|
+
isPublic: inputIsPublic ? inputIsPublic.checked : false
|
213
|
+
};
|
52
214
|
|
53
|
-
|
54
|
-
|
215
|
+
createList(newList, ((contentId, createdList) => {
|
216
|
+
triggerCreateListEvent(contentId, createdList.uuid);
|
217
|
+
triggerAddToListEvent(contentId, createdList.uuid);
|
218
|
+
if (!modal) {
|
219
|
+
positionOverlay(createListOverlay.wrapper);
|
55
220
|
}
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
221
|
+
onListCreated();
|
222
|
+
}));
|
223
|
+
formElement.remove();
|
224
|
+
}
|
60
225
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
});
|
226
|
+
function handleCancelClick (event) {
|
227
|
+
event.preventDefault();
|
228
|
+
event.stopPropagation();
|
229
|
+
formElement.remove();
|
230
|
+
if (!lists.length) attachDescription();
|
231
|
+
onCancel();
|
232
|
+
}
|
233
|
+
|
234
|
+
formElement.querySelector('button[type="submit"]').addEventListener('click', handleSubmit);
|
235
|
+
formElement.querySelector('button[type="button"]').addEventListener('click', handleCancelClick);
|
236
|
+
|
237
|
+
addPublicToggleListener(formElement);
|
238
|
+
|
239
|
+
return formElement;
|
76
240
|
}
|
77
241
|
|
242
|
+
function addPublicToggleListener (formElement) {
|
243
|
+
function onPublicToggleClick (event) {
|
244
|
+
event.target.setAttribute('data-trackable', event.target.checked ? 'private-link' : 'public-link');
|
245
|
+
event.target.setAttribute('text', event.target.checked ? 'private' : 'public');
|
246
|
+
}
|
78
247
|
|
79
|
-
|
248
|
+
formElement.querySelector('input[name="is-public"]').addEventListener('click', onPublicToggleClick);
|
249
|
+
}
|
80
250
|
|
81
|
-
|
82
|
-
const
|
251
|
+
function ContentElement (hasDescription, onClick) {
|
252
|
+
const description = '<p class="myft-ui-create-list-add-description">Lists are a simple way to curate your content</p>';
|
83
253
|
|
84
|
-
|
85
|
-
|
86
|
-
|
254
|
+
const content = `
|
255
|
+
<div class="myft-ui-create-list-footer">
|
256
|
+
<button class="myft-ui-create-list-add myft-ui-create-list-add-collapsed" aria-expanded=false data-trackable="add-to-new-list" text="add to new list">Add to a new list</button>
|
257
|
+
${hasDescription ? `
|
258
|
+
${description}
|
259
|
+
` : ''}
|
260
|
+
<span
|
261
|
+
class="myft-ui-create-list-announcement o-normalise-visually-hidden"
|
262
|
+
role="region"
|
263
|
+
aria-live="assertive"
|
264
|
+
/>
|
265
|
+
</div>
|
266
|
+
`;
|
87
267
|
|
88
|
-
|
89
|
-
return;
|
90
|
-
}
|
268
|
+
const contentElement = stringToHTMLElement(content);
|
91
269
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
270
|
+
function removeDescription () {
|
271
|
+
const descriptionElement = contentElement.querySelector('.myft-ui-create-list-add-description');
|
272
|
+
if (descriptionElement) {
|
273
|
+
descriptionElement.remove();
|
274
|
+
}
|
275
|
+
}
|
276
|
+
|
277
|
+
function attachDescription () {
|
278
|
+
const descriptionElement = stringToHTMLElement(description);
|
279
|
+
contentElement.insertAdjacentElement('beforeend', descriptionElement);
|
280
|
+
}
|
281
|
+
|
282
|
+
function restoreFormHandler () {
|
283
|
+
contentElement.querySelector('.myft-ui-create-list-add').classList.add('myft-ui-create-list-add-collapsed');
|
284
|
+
contentElement.querySelector('.myft-ui-create-list-add').setAttribute('aria-expanded', false);
|
285
|
+
return contentElement.addEventListener('click', clickHandler, { once: true });
|
286
|
+
}
|
287
|
+
|
288
|
+
function clickHandler (event) {
|
289
|
+
contentElement.querySelector('.myft-ui-create-list-add').classList.remove('myft-ui-create-list-add-collapsed');
|
290
|
+
contentElement.querySelector('.myft-ui-create-list-add').setAttribute('aria-expanded', true);
|
291
|
+
onClick(event);
|
99
292
|
}
|
293
|
+
|
294
|
+
return [contentElement, removeDescription, attachDescription, restoreFormHandler];
|
100
295
|
}
|
101
296
|
|
102
|
-
function
|
297
|
+
function HeadingElement () {
|
298
|
+
const heading = `
|
299
|
+
<span class="myft-ui-create-list-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-icon"><span class="myft-ui-create-list-icon-visually-hidden">my FT</span></span></span>
|
300
|
+
`;
|
103
301
|
|
104
|
-
|
105
|
-
|
302
|
+
return stringToHTMLElement(heading);
|
303
|
+
}
|
106
304
|
|
107
|
-
|
108
|
-
|
305
|
+
function ListsElement (lists, addToList, removeFromList) {
|
306
|
+
const currentList = document.querySelector('.myft-ui-create-list-lists');
|
307
|
+
if (currentList) {
|
308
|
+
currentList.remove();
|
109
309
|
}
|
110
310
|
|
111
|
-
|
112
|
-
event.preventDefault();
|
311
|
+
const listCheckboxElement = ListCheckboxElement(addToList, removeFromList);
|
113
312
|
|
114
|
-
|
115
|
-
|
116
|
-
|
313
|
+
const listsTemplate = `
|
314
|
+
<div class="myft-ui-create-list-lists o-forms-field o-forms-field--optional" role="group">
|
315
|
+
<span class="myft-ui-create-list-lists-text">Add to list</span>
|
316
|
+
<span class="myft-ui-create-list-lists-container o-forms-input o-forms-input--checkbox">
|
317
|
+
</span>
|
318
|
+
</div>
|
319
|
+
`;
|
320
|
+
const listsElement = stringToHTMLElement(listsTemplate);
|
117
321
|
|
118
|
-
|
119
|
-
token: csrfToken,
|
120
|
-
name: nameInput.value
|
121
|
-
};
|
322
|
+
const listsElementContainer = listsElement.querySelector('.myft-ui-create-list-lists-container');
|
122
323
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
return myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken });
|
127
|
-
} else {
|
128
|
-
return detail;
|
129
|
-
}
|
324
|
+
function refresh () {
|
325
|
+
listsElementContainer.replaceChildren(...lists.map(list => listCheckboxElement(list)));
|
326
|
+
}
|
130
327
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
content: contentId ? 'Error adding article to new list' : 'Error creating new list',
|
141
|
-
type: 'error'
|
142
|
-
});
|
143
|
-
throw err;
|
144
|
-
});
|
145
|
-
});
|
328
|
+
function hide () {
|
329
|
+
listsElement.style.display = 'none';
|
330
|
+
}
|
331
|
+
|
332
|
+
function show () {
|
333
|
+
listsElement.style.display = 'flex';
|
334
|
+
}
|
335
|
+
|
336
|
+
refresh();
|
146
337
|
|
338
|
+
return [listsElement, refresh, hide, show];
|
147
339
|
}
|
148
340
|
|
341
|
+
function ListCheckboxElement (addToList, removeFromList) {
|
342
|
+
return function (list) {
|
343
|
+
|
344
|
+
const listCheckbox = `<label>
|
345
|
+
<input type="checkbox" name="default" value="${list.uuid}" ${list.checked ? 'checked' : ''}>
|
346
|
+
<span class="o-forms-input__label">
|
347
|
+
<span class="o-normalise-visually-hidden">
|
348
|
+
${list.checked ? 'Remove article from ' : 'Add article to ' }
|
349
|
+
</span>
|
350
|
+
${escapeText(list.name)}
|
351
|
+
</span>
|
352
|
+
</label>
|
353
|
+
`;
|
354
|
+
|
355
|
+
const listCheckboxElement = stringToHTMLElement(listCheckbox);
|
356
|
+
|
357
|
+
const [ input ] = listCheckboxElement.children;
|
358
|
+
|
359
|
+
function handleCheck (event) {
|
360
|
+
event.preventDefault();
|
361
|
+
const isChecked = event.target.checked;
|
149
362
|
|
150
|
-
function
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
}))
|
155
|
-
.then(res => {
|
156
|
-
if (res.ok) {
|
157
|
-
return res.text();
|
158
|
-
} else {
|
159
|
-
throw new Error(`Unexpected response: ${res.statusText}`);
|
363
|
+
function onListUpdated () {
|
364
|
+
const indexToUpdate = lists.indexOf(list);
|
365
|
+
lists[indexToUpdate] = { ...lists[indexToUpdate], checked: isChecked };
|
366
|
+
listCheckboxElement.querySelector('input').checked = isChecked;
|
160
367
|
}
|
161
|
-
})
|
162
|
-
.then(html => openOverlay(html, { name: 'myft-lists', title: overlayTitle, trigger }))
|
163
|
-
.then(overlay => {
|
164
|
-
oForms.init(overlay.content);
|
165
|
-
setUpSaveToExistingListListeners(overlay, contentId);
|
166
|
-
setUpCreateListListeners(overlay, contentId);
|
167
|
-
})
|
168
|
-
.catch(err => {
|
169
|
-
nNotification.show({
|
170
|
-
content: 'Sorry, something went wrong',
|
171
|
-
type: 'error'
|
172
|
-
});
|
173
|
-
throw err;
|
174
|
-
});
|
175
368
|
|
176
|
-
|
369
|
+
return isChecked ? addToList(list, onListUpdated) : removeFromList(list, onListUpdated);
|
370
|
+
}
|
177
371
|
|
178
|
-
|
179
|
-
showListsOverlay('Copy to list', `/myft/list?fragment=true©=true&contentId=${contentId}&excludeList=${excludeList}`, contentId, trigger);
|
180
|
-
}
|
372
|
+
input.addEventListener('click', handleCheck);
|
181
373
|
|
182
|
-
|
183
|
-
|
374
|
+
return listCheckboxElement;
|
375
|
+
};
|
184
376
|
}
|
185
377
|
|
186
|
-
function
|
187
|
-
|
188
|
-
|
378
|
+
function realignOverlay (originalScrollPosition, target) {
|
379
|
+
return function () {
|
380
|
+
const currentScrollPosition = window.scrollY;
|
189
381
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
382
|
+
if(Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
|
383
|
+
return;
|
384
|
+
}
|
385
|
+
|
386
|
+
if (currentScrollPosition) {
|
387
|
+
originalScrollPosition = currentScrollPosition;;
|
388
|
+
}
|
389
|
+
|
390
|
+
positionOverlay(target);
|
194
391
|
};
|
392
|
+
}
|
195
393
|
|
196
|
-
|
394
|
+
function positionOverlay (target) {
|
395
|
+
target.style['min-width'] = '340px';
|
396
|
+
target.style['width'] = '100%';
|
397
|
+
target.style['margin-top'] = 0;
|
398
|
+
target.style['left'] = 0;
|
399
|
+
target.style['top'] = 0;
|
400
|
+
|
401
|
+
if (isMobile()) {
|
402
|
+
const shareNavComponent = document.querySelector('.share-nav__horizontal');
|
403
|
+
const topHalfOffset = target.clientHeight + 10;
|
404
|
+
target.style['position'] = 'absolute';
|
405
|
+
target.style['margin-left'] = 0;
|
406
|
+
target.style['top'] = calculateLargerScreenHalf(shareNavComponent) === 'ABOVE' ? `-${topHalfOffset}px` : '50px';
|
407
|
+
} else {
|
408
|
+
target.style['position'] = 'absolute';
|
409
|
+
target.style['margin-left'] = '45px';
|
410
|
+
}
|
197
411
|
}
|
198
412
|
|
199
|
-
function
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
413
|
+
function calculateLargerScreenHalf (target) {
|
414
|
+
if (!target) {
|
415
|
+
return 'BELOW';
|
416
|
+
}
|
417
|
+
|
418
|
+
const vh = Math.min(document.documentElement.clientHeight || 0, window.innerHeight || 0);
|
419
|
+
|
420
|
+
const targetBox = target.getBoundingClientRect();
|
421
|
+
const spaceAbove = targetBox.top;
|
422
|
+
const spaceBelow = vh - targetBox.bottom;
|
423
|
+
|
424
|
+
return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
|
207
425
|
}
|
208
426
|
|
209
|
-
function
|
210
|
-
return myFtClient.
|
211
|
-
.then(
|
212
|
-
|
213
|
-
return
|
214
|
-
});
|
427
|
+
async function getLists (contentId) {
|
428
|
+
return myFtClient.getListsContent()
|
429
|
+
.then(results => results.items.map(list => {
|
430
|
+
const isChecked = Array.isArray(list.content) && list.content.some(content => content.uuid === contentId);
|
431
|
+
return { name: list.name, uuid: list.uuid, checked: isChecked, content: list.content, isPublic: list.isPublic };
|
432
|
+
}));
|
215
433
|
}
|
216
434
|
|
217
|
-
function
|
218
|
-
|
435
|
+
function triggerAddToListEvent (contentId, listId) {
|
436
|
+
return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
|
437
|
+
detail: {
|
438
|
+
category: 'list',
|
439
|
+
action: 'add-success',
|
440
|
+
article_id: contentId,
|
441
|
+
list_id: listId,
|
442
|
+
teamName: 'customer-products-us-growth',
|
443
|
+
amplitudeExploratory: true
|
444
|
+
},
|
445
|
+
bubbles: true
|
446
|
+
}));
|
447
|
+
}
|
219
448
|
|
220
|
-
|
221
|
-
|
449
|
+
function triggerRemoveFromListEvent (contentId, listId) {
|
450
|
+
return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
|
451
|
+
detail: {
|
452
|
+
category: 'list',
|
453
|
+
action: 'remove-success',
|
454
|
+
article_id: contentId,
|
455
|
+
list_id: listId,
|
456
|
+
teamName: 'customer-products-us-growth',
|
457
|
+
amplitudeExploratory: true
|
458
|
+
},
|
459
|
+
bubbles: true
|
460
|
+
}));
|
461
|
+
}
|
222
462
|
|
223
|
-
|
224
|
-
|
225
|
-
|
463
|
+
function triggerCreateListEvent (contentId, listId) {
|
464
|
+
document.body.dispatchEvent(new CustomEvent('oTracking.event', {
|
465
|
+
detail: {
|
466
|
+
category: 'list',
|
467
|
+
action: 'create-success',
|
468
|
+
article_id: contentId,
|
469
|
+
list_id: listId,
|
470
|
+
teamName: 'customer-products-us-growth',
|
471
|
+
amplitudeExploratory: true
|
472
|
+
},
|
473
|
+
bubbles: true
|
474
|
+
}));
|
475
|
+
}
|
226
476
|
|
227
|
-
|
228
|
-
const
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
if (!csrfToken || !csrfToken.value) {
|
234
|
-
document.body.dispatchEvent(new CustomEvent('oErrors.log', {
|
235
|
-
bubbles: true,
|
236
|
-
detail: {
|
237
|
-
error: new Error('myFT form submitted without a CSRF token'),
|
238
|
-
info: {
|
239
|
-
action,
|
240
|
-
actorType: 'list',
|
241
|
-
actorId: listId,
|
242
|
-
relationshipName: 'contained',
|
243
|
-
subjectType: 'content',
|
244
|
-
subjectId: contentId,
|
245
|
-
},
|
246
|
-
},
|
247
|
-
}));
|
248
|
-
}
|
477
|
+
function showCreateListAndAddArticleOverlay (contentId, config) {
|
478
|
+
const options = {
|
479
|
+
name: 'myft-ui-create-list',
|
480
|
+
...config
|
481
|
+
};
|
249
482
|
|
250
|
-
|
483
|
+
return openSaveArticleToList(contentId, options);
|
484
|
+
}
|
251
485
|
|
252
|
-
|
486
|
+
function openCreateListAndAddArticleOverlay (contentId, config) {
|
487
|
+
return myFtClient.getAll('created', 'list')
|
253
488
|
.then(() => {
|
254
|
-
|
255
|
-
|
256
|
-
document.body.dispatchEvent(new CustomEvent('oTracking.event', {
|
257
|
-
detail: {
|
258
|
-
category: 'list',
|
259
|
-
action: action === 'add' ? 'add-success' : 'remove-success',
|
260
|
-
article_id: contentId,
|
261
|
-
list_id: listId,
|
262
|
-
content: {
|
263
|
-
uuid: contentId
|
264
|
-
},
|
265
|
-
teamName: 'customer-products-us-growth',
|
266
|
-
amplitudeExploratory: true
|
267
|
-
},
|
268
|
-
bubbles: true
|
269
|
-
}));
|
270
|
-
})
|
271
|
-
.catch(error => {
|
272
|
-
setTimeout(() => submitBtnEl.removeAttribute('disabled'));
|
273
|
-
throw error;
|
489
|
+
return showCreateListAndAddArticleOverlay(contentId, config);
|
274
490
|
});
|
275
491
|
}
|
276
492
|
|
277
493
|
function initialEventListeners () {
|
278
494
|
|
279
495
|
document.body.addEventListener('myft.user.saved.content.add', event => {
|
496
|
+
event.stopPropagation();
|
280
497
|
const contentId = event.detail.subject;
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
// otherwise it will show the classic overlay
|
285
|
-
const newListDesign = event.currentTarget.querySelector('[data-myft-ui-save-new="manageArticleLists"]');
|
286
|
-
if (newListDesign) {
|
287
|
-
const configKeys = newListDesign.dataset.myftUiSaveNewConfig.split(',');
|
288
|
-
const config = configKeys.reduce((configObj, key) => (key ? { ...configObj, [key]: true} : configObj), {});
|
289
|
-
|
290
|
-
// Temporary events on the public toggle feature.
|
291
|
-
// These will be used to build a sanity check dashboard, and will be removed after we get clean-up this test.
|
292
|
-
document.body.dispatchEvent(new CustomEvent('oTracking.event', {
|
293
|
-
detail: {
|
294
|
-
category: 'lists',
|
295
|
-
action: 'savedArticle',
|
296
|
-
article_id: contentId,
|
297
|
-
teamName: 'customer-products-us-growth',
|
298
|
-
amplitudeExploratory: true
|
299
|
-
},
|
300
|
-
bubbles: true
|
301
|
-
}));
|
302
|
-
|
303
|
-
return openCreateListAndAddArticleOverlay(contentId, config);
|
498
|
+
const configSet = event.currentTarget.querySelector('[data-myft-ui-save-config]');
|
499
|
+
if (!configSet) {
|
500
|
+
return openCreateListAndAddArticleOverlay(contentId);
|
304
501
|
}
|
305
|
-
|
306
|
-
|
502
|
+
const configKeys = configSet.dataset.myftUiSaveConfig.split(',');
|
503
|
+
const config = configKeys.reduce((configObj, key) => (key ? { ...configObj, [key]: true} : configObj), {});
|
504
|
+
return openCreateListAndAddArticleOverlay(contentId, config);
|
307
505
|
});
|
308
506
|
|
309
507
|
document.body.addEventListener('myft.user.saved.content.remove', event => {
|
508
|
+
event.stopPropagation();
|
310
509
|
const contentId = event.detail.subject;
|
311
|
-
|
312
|
-
const newListDesign = event.currentTarget.querySelector('[data-myft-ui-save-new="manageArticleLists"]');
|
313
|
-
if (newListDesign) {
|
314
|
-
return showUnsavedNotification(contentId);
|
315
|
-
}
|
316
|
-
});
|
317
|
-
|
318
|
-
delegate.on('click', '[data-myft-ui="copy-to-list"]', event => {
|
319
|
-
event.preventDefault();
|
320
|
-
showCopyToListOverlay(event.target.getAttribute('data-content-id'), event.target.getAttribute('data-actor-id'), event.target);
|
510
|
+
return showUnsavedNotification(contentId);
|
321
511
|
});
|
322
|
-
delegate.on('click', '[data-myft-ui="create-list"]', event => {
|
323
|
-
event.preventDefault();
|
324
|
-
showCreateListOverlay(event.target);
|
325
|
-
});
|
326
|
-
|
327
|
-
delegate.on('submit', '[data-myft-ui="contained"]', handleRemoveToggleSubmit);
|
328
512
|
}
|
329
513
|
|
330
514
|
function showUnsavedNotification () {
|
package/package.json
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
const expect = require('chai').expect;
|
2
|
+
const escapeText = require('../../myft/ui/lib/escape-text');
|
3
|
+
|
4
|
+
describe('escapeText', function () {
|
5
|
+
it('should escape an anchor tag', () => {
|
6
|
+
const input = '<a onclick="console.log(\'muahaha\')"></a>';
|
7
|
+
const expectedOutput = '<a onclick="console.log(\'muahaha\')"></a>';
|
8
|
+
expect(escapeText(input)).to.equal(expectedOutput);
|
9
|
+
});
|
10
|
+
|
11
|
+
it('should escape ampersands', () => {
|
12
|
+
const input = '<a onclick="console.log(\'muahaha\')"></a>';
|
13
|
+
const expectedOutput = '&lt;a onclick="console.log(\'muahaha\')"&gt;&lt;/a&gt;';
|
14
|
+
expect(escapeText(input)).to.equal(expectedOutput);
|
15
|
+
});
|
16
|
+
|
17
|
+
it('should return empty string for inputs that are not strings', () => {
|
18
|
+
const input = true;
|
19
|
+
const expectedOutput = '';
|
20
|
+
expect(escapeText(input)).to.equal(expectedOutput);
|
21
|
+
});
|
22
|
+
});
|
@@ -1,512 +0,0 @@
|
|
1
|
-
import Overlay from '@financial-times/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 isMobile from './lib/is-mobile';
|
6
|
-
import stringToHTMLElement from './lib/convert-string-to-html-element';
|
7
|
-
|
8
|
-
const csrfToken = getToken();
|
9
|
-
|
10
|
-
let lists = [];
|
11
|
-
let haveLoadedLists = false;
|
12
|
-
let createListOverlay;
|
13
|
-
let scrolledOnOpen;
|
14
|
-
let listOverlayBottom;
|
15
|
-
|
16
|
-
export default async function openSaveArticleToListVariant (contentId, options = {}) {
|
17
|
-
const { name, modal = false } = options;
|
18
|
-
|
19
|
-
function createList (newList, cb) {
|
20
|
-
if(!newList || !newList.name) {
|
21
|
-
return restoreContent();
|
22
|
-
}
|
23
|
-
|
24
|
-
myFtClient.add('user', null, 'created', 'list', uuid(), { name: newList.name, token: csrfToken, isPublic: newList.isPublic })
|
25
|
-
.then(detail => {
|
26
|
-
myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((data) => {
|
27
|
-
const createdList = { name: newList.name, uuid: data.actorId, checked: true, isPublic: !!newList.isPublic };
|
28
|
-
lists.unshift(createdList);
|
29
|
-
const announceListContainer = document.querySelector('.myft-ui-create-list-variant-announcement');
|
30
|
-
announceListContainer.textContent = `${newList.name} created`;
|
31
|
-
cb(contentId, createdList);
|
32
|
-
});
|
33
|
-
})
|
34
|
-
.catch(() => {
|
35
|
-
return restoreContent();
|
36
|
-
});
|
37
|
-
}
|
38
|
-
|
39
|
-
function addToList (list, cb) {
|
40
|
-
if(!list) {
|
41
|
-
return;
|
42
|
-
}
|
43
|
-
|
44
|
-
myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((addedList) => {
|
45
|
-
cb();
|
46
|
-
triggerAddToListEvent(contentId, addedList.actorId);
|
47
|
-
});
|
48
|
-
}
|
49
|
-
|
50
|
-
function removeFromList (list, cb) {
|
51
|
-
if(!list) {
|
52
|
-
return;
|
53
|
-
}
|
54
|
-
|
55
|
-
myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((removedList) => {
|
56
|
-
cb();
|
57
|
-
triggerRemoveFromListEvent(contentId, removedList.actorId);
|
58
|
-
});
|
59
|
-
}
|
60
|
-
|
61
|
-
function restoreContent () {
|
62
|
-
if (!lists.length) attachDescription();
|
63
|
-
refreshListElement();
|
64
|
-
showListElement();
|
65
|
-
return restoreFormHandler();
|
66
|
-
}
|
67
|
-
|
68
|
-
if (!haveLoadedLists) {
|
69
|
-
lists = await getLists(contentId);
|
70
|
-
haveLoadedLists = true;
|
71
|
-
}
|
72
|
-
|
73
|
-
const overlays = Overlay.getOverlays();
|
74
|
-
const existingOverlay = overlays[name];
|
75
|
-
if (existingOverlay) {
|
76
|
-
existingOverlay.destroy();
|
77
|
-
}
|
78
|
-
|
79
|
-
const headingElement = HeadingElement();
|
80
|
-
let [contentElement, removeDescription, attachDescription, restoreFormHandler] = ContentElement(!lists.length, openFormHandler);
|
81
|
-
const [listElement, refreshListElement, hideListElement, showListElement] = ListsElement(lists, addToList, removeFromList);
|
82
|
-
|
83
|
-
createListOverlay = new Overlay(name, {
|
84
|
-
modal,
|
85
|
-
html: contentElement,
|
86
|
-
heading: { title: headingElement.outerHTML },
|
87
|
-
parentnode: isMobile() ? '.o-share--horizontal' : '.o-share--vertical',
|
88
|
-
class: 'myft-ui-create-list-variant',
|
89
|
-
});
|
90
|
-
|
91
|
-
function outsideClickHandler (e) {
|
92
|
-
const overlayContent = document.querySelector('.o-overlay__content');
|
93
|
-
const overlayContainer = document.querySelector('.o-overlay');
|
94
|
-
// we don't want to close the overlay if the click happened inside the
|
95
|
-
// overlay, except if the click happened on the overlay close button
|
96
|
-
const isTargetInsideOverlay = overlayContainer.contains(e.target) && !e.target.classList.contains('o-overlay__close');
|
97
|
-
if(createListOverlay.visible && (!overlayContent || !isTargetInsideOverlay)) {
|
98
|
-
createListOverlay.close();
|
99
|
-
}
|
100
|
-
}
|
101
|
-
|
102
|
-
function onFormCancel () {
|
103
|
-
showListElement();
|
104
|
-
restoreFormHandler();
|
105
|
-
}
|
106
|
-
|
107
|
-
function onFormListCreated () {
|
108
|
-
refreshListElement();
|
109
|
-
showListElement();
|
110
|
-
restoreFormHandler();
|
111
|
-
}
|
112
|
-
|
113
|
-
function openFormHandler () {
|
114
|
-
hideListElement();
|
115
|
-
const formElement = FormElement(createList, attachDescription, onFormListCreated, onFormCancel, modal);
|
116
|
-
const overlayContent = document.querySelector('.o-overlay__content');
|
117
|
-
removeDescription();
|
118
|
-
overlayContent.insertAdjacentElement('beforeend', formElement);
|
119
|
-
}
|
120
|
-
|
121
|
-
createListOverlay.open();
|
122
|
-
scrolledOnOpen = window.scrollY;
|
123
|
-
|
124
|
-
const scrollHandler = getScrollHandler(createListOverlay.wrapper);
|
125
|
-
const resizeHandler = getResizeHandler(createListOverlay.wrapper);
|
126
|
-
|
127
|
-
createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
|
128
|
-
const overlayContent = document.querySelector('.o-overlay__content');
|
129
|
-
overlayContent.insertAdjacentElement('afterbegin', listElement);
|
130
|
-
if (!lists.length) {
|
131
|
-
hideListElement();
|
132
|
-
}
|
133
|
-
|
134
|
-
if (!modal) {
|
135
|
-
positionOverlay(data.currentTarget);
|
136
|
-
|
137
|
-
window.addEventListener('oViewport.resize', resizeHandler);
|
138
|
-
window.addEventListener('scroll', scrollHandler);
|
139
|
-
}
|
140
|
-
|
141
|
-
listOverlayBottom = document.querySelector('.myft-ui-create-list-variant').getBoundingClientRect().bottom;
|
142
|
-
|
143
|
-
restoreFormHandler();
|
144
|
-
|
145
|
-
document.body.addEventListener('click', outsideClickHandler);
|
146
|
-
|
147
|
-
|
148
|
-
});
|
149
|
-
|
150
|
-
createListOverlay.wrapper.addEventListener('oOverlay.destroy', () => {
|
151
|
-
window.removeEventListener('scroll', scrollHandler);
|
152
|
-
|
153
|
-
window.removeEventListener('oViewport.resize', resizeHandler);
|
154
|
-
|
155
|
-
document.body.removeEventListener('click', outsideClickHandler);
|
156
|
-
});
|
157
|
-
}
|
158
|
-
|
159
|
-
function getScrollHandler (target) {
|
160
|
-
return realignOverlay(window.scrollY, target);
|
161
|
-
}
|
162
|
-
|
163
|
-
function getResizeHandler (target) {
|
164
|
-
return function resizeHandler () {
|
165
|
-
positionOverlay(target);
|
166
|
-
};
|
167
|
-
}
|
168
|
-
|
169
|
-
function FormElement (createList, attachDescription, onListCreated, onCancel, modal=false) {
|
170
|
-
const formString = `
|
171
|
-
<form class="myft-ui-create-list-variant-form">
|
172
|
-
<label class="myft-ui-create-list-variant-form-name o-forms-field">
|
173
|
-
<span class="o-forms-input o-forms-input--text">
|
174
|
-
<input class="myft-ui-create-list-variant-text" type="text" name="list-name">
|
175
|
-
<div class="myft-ui-create-list-variant-label">List name</div>
|
176
|
-
</span>
|
177
|
-
</label>
|
178
|
-
|
179
|
-
<div class="myft-ui-create-list-variant-form-public o-forms-field" role="group">
|
180
|
-
<span class="o-forms-input o-forms-input--toggle">
|
181
|
-
<label>
|
182
|
-
<input class="myft-ui-create-list-variant-form-toggle" type="checkbox" name="is-public" value="public" checked data-trackable="private-link" text="private">
|
183
|
-
<span class="myft-ui-create-list-variant-form-toggle-label o-forms-input__label">
|
184
|
-
<span class="o-forms-input__label__main">
|
185
|
-
Public
|
186
|
-
</span>
|
187
|
-
<span id="myft-ui-create-list-variant-form-public-description" class="o-forms-input__label__prompt">
|
188
|
-
Your profession & list will be visible to others
|
189
|
-
</span>
|
190
|
-
</span>
|
191
|
-
</label>
|
192
|
-
</span>
|
193
|
-
</div>
|
194
|
-
|
195
|
-
<div class="myft-ui-create-list-variant-form-buttons">
|
196
|
-
<button class="o-buttons o-buttons--primary o-buttons--inverse o-buttons--big" type="button" data-trackable="cancel-link" text="cancel">
|
197
|
-
Cancel
|
198
|
-
</button>
|
199
|
-
<button class="o-buttons o-buttons--big o-buttons--secondary" type="submit">
|
200
|
-
Add
|
201
|
-
</button>
|
202
|
-
</div>
|
203
|
-
</form>
|
204
|
-
`;
|
205
|
-
|
206
|
-
const formElement = stringToHTMLElement(formString);
|
207
|
-
|
208
|
-
function handleSubmit (event) {
|
209
|
-
event.preventDefault();
|
210
|
-
event.stopPropagation();
|
211
|
-
const inputListName = formElement.querySelector('input[name="list-name"]');
|
212
|
-
const inputIsPublic = formElement.querySelector('input[name="is-public"]');
|
213
|
-
|
214
|
-
const newList = {
|
215
|
-
name: inputListName.value,
|
216
|
-
isPublic: inputIsPublic ? inputIsPublic.checked : false
|
217
|
-
};
|
218
|
-
|
219
|
-
createList(newList, ((contentId, createdList) => {
|
220
|
-
triggerCreateListEvent(contentId, createdList.uuid);
|
221
|
-
triggerAddToListEvent(contentId, createdList.uuid);
|
222
|
-
if (!modal) {
|
223
|
-
positionOverlay(createListOverlay.wrapper);
|
224
|
-
}
|
225
|
-
onListCreated();
|
226
|
-
}));
|
227
|
-
formElement.remove();
|
228
|
-
}
|
229
|
-
|
230
|
-
function handleCancelClick (event) {
|
231
|
-
event.preventDefault();
|
232
|
-
event.stopPropagation();
|
233
|
-
formElement.remove();
|
234
|
-
if (!lists.length) attachDescription();
|
235
|
-
onCancel();
|
236
|
-
}
|
237
|
-
|
238
|
-
formElement.querySelector('button[type="submit"]').addEventListener('click', handleSubmit);
|
239
|
-
formElement.querySelector('button[type="button"]').addEventListener('click', handleCancelClick);
|
240
|
-
|
241
|
-
addPublicToggleListener(formElement);
|
242
|
-
|
243
|
-
return formElement;
|
244
|
-
}
|
245
|
-
|
246
|
-
function addPublicToggleListener (formElement) {
|
247
|
-
function onPublicToggleClick (event) {
|
248
|
-
event.target.setAttribute('data-trackable', event.target.checked ? 'private-link' : 'public-link');
|
249
|
-
event.target.setAttribute('text', event.target.checked ? 'private' : 'public');
|
250
|
-
}
|
251
|
-
|
252
|
-
formElement.querySelector('input[name="is-public"]').addEventListener('click', onPublicToggleClick);
|
253
|
-
}
|
254
|
-
|
255
|
-
function ContentElement (hasDescription, onClick) {
|
256
|
-
const description = '<p class="myft-ui-create-list-variant-add-description">Lists are a simple way to curate your content</p>';
|
257
|
-
|
258
|
-
const content = `
|
259
|
-
<div class="myft-ui-create-list-variant-footer">
|
260
|
-
<button class="myft-ui-create-list-variant-add myft-ui-create-list-variant-add-collapsed" aria-expanded=false data-trackable="add-to-new-list" text="add to new list">Add to a new list</button>
|
261
|
-
${hasDescription ? `
|
262
|
-
${description}
|
263
|
-
` : ''}
|
264
|
-
<span
|
265
|
-
class="myft-ui-create-list-variant-announcement o-normalise-visually-hidden"
|
266
|
-
role="region"
|
267
|
-
aria-live="assertive"
|
268
|
-
/>
|
269
|
-
</div>
|
270
|
-
`;
|
271
|
-
|
272
|
-
const contentElement = stringToHTMLElement(content);
|
273
|
-
|
274
|
-
contentElement.querySelector('.myft-ui-create-list-variant-add').addEventListener('click', checkScrollToAdd);
|
275
|
-
contentElement.querySelector('.myft-ui-create-list-variant-add').addEventListener('click', triggerAddToNewListEvent);
|
276
|
-
|
277
|
-
function removeDescription () {
|
278
|
-
const descriptionElement = contentElement.querySelector('.myft-ui-create-list-variant-add-description');
|
279
|
-
if (descriptionElement) {
|
280
|
-
descriptionElement.remove();
|
281
|
-
}
|
282
|
-
}
|
283
|
-
|
284
|
-
function attachDescription () {
|
285
|
-
const descriptionElement = stringToHTMLElement(description);
|
286
|
-
contentElement.insertAdjacentElement('beforeend', descriptionElement);
|
287
|
-
}
|
288
|
-
|
289
|
-
function restoreFormHandler () {
|
290
|
-
contentElement.querySelector('.myft-ui-create-list-variant-add').classList.add('myft-ui-create-list-variant-add-collapsed');
|
291
|
-
contentElement.querySelector('.myft-ui-create-list-variant-add').setAttribute('aria-expanded', false);
|
292
|
-
return contentElement.addEventListener('click', clickHandler, { once: true });
|
293
|
-
}
|
294
|
-
|
295
|
-
function clickHandler (event) {
|
296
|
-
contentElement.querySelector('.myft-ui-create-list-variant-add').classList.remove('myft-ui-create-list-variant-add-collapsed');
|
297
|
-
contentElement.querySelector('.myft-ui-create-list-variant-add').setAttribute('aria-expanded', true);
|
298
|
-
onClick(event);
|
299
|
-
}
|
300
|
-
|
301
|
-
return [contentElement, removeDescription, attachDescription, restoreFormHandler];
|
302
|
-
}
|
303
|
-
|
304
|
-
function HeadingElement () {
|
305
|
-
const heading = `
|
306
|
-
<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>
|
307
|
-
`;
|
308
|
-
|
309
|
-
return stringToHTMLElement(heading);
|
310
|
-
}
|
311
|
-
|
312
|
-
function ListsElement (lists, addToList, removeFromList) {
|
313
|
-
const currentList = document.querySelector('.myft-ui-create-list-variant-lists');
|
314
|
-
if (currentList) {
|
315
|
-
currentList.remove();
|
316
|
-
}
|
317
|
-
|
318
|
-
const listCheckboxElement = ListCheckboxElement(addToList, removeFromList);
|
319
|
-
|
320
|
-
const listsTemplate = `
|
321
|
-
<div class="myft-ui-create-list-variant-lists o-forms-field o-forms-field--optional" role="group">
|
322
|
-
<span class="myft-ui-create-list-variant-lists-text">Add to list</span>
|
323
|
-
<span class="myft-ui-create-list-variant-lists-container o-forms-input o-forms-input--checkbox">
|
324
|
-
</span>
|
325
|
-
</div>
|
326
|
-
`;
|
327
|
-
const listsElement = stringToHTMLElement(listsTemplate);
|
328
|
-
|
329
|
-
const listsElementContainer = listsElement.querySelector('.myft-ui-create-list-variant-lists-container');
|
330
|
-
|
331
|
-
function refresh () {
|
332
|
-
listsElementContainer.replaceChildren(...lists.map(list => listCheckboxElement(list)));
|
333
|
-
}
|
334
|
-
|
335
|
-
function hide () {
|
336
|
-
listsElement.style.display = 'none';
|
337
|
-
}
|
338
|
-
|
339
|
-
function show () {
|
340
|
-
listsElement.style.display = 'flex';
|
341
|
-
}
|
342
|
-
|
343
|
-
refresh();
|
344
|
-
|
345
|
-
return [listsElement, refresh, hide, show];
|
346
|
-
}
|
347
|
-
|
348
|
-
function ListCheckboxElement (addToList, removeFromList) {
|
349
|
-
return function (list) {
|
350
|
-
|
351
|
-
const listCheckbox = `<label>
|
352
|
-
<input type="checkbox" name="default" value="${list.uuid}" ${list.checked ? 'checked' : ''}>
|
353
|
-
<span class="o-forms-input__label">
|
354
|
-
<span class="o-normalise-visually-hidden">
|
355
|
-
${list.checked ? 'Remove article from ' : 'Add article to ' }
|
356
|
-
</span>
|
357
|
-
${list.name}
|
358
|
-
</span>
|
359
|
-
</label>
|
360
|
-
`;
|
361
|
-
|
362
|
-
const listCheckboxElement = stringToHTMLElement(listCheckbox);
|
363
|
-
|
364
|
-
const [ input ] = listCheckboxElement.children;
|
365
|
-
|
366
|
-
function handleCheck (event) {
|
367
|
-
event.preventDefault();
|
368
|
-
const isChecked = event.target.checked;
|
369
|
-
|
370
|
-
function onListUpdated () {
|
371
|
-
const indexToUpdate = lists.indexOf(list);
|
372
|
-
lists[indexToUpdate] = { ...lists[indexToUpdate], checked: isChecked };
|
373
|
-
listCheckboxElement.querySelector('input').checked = isChecked;
|
374
|
-
}
|
375
|
-
|
376
|
-
return isChecked ? addToList(list, onListUpdated) : removeFromList(list, onListUpdated);
|
377
|
-
}
|
378
|
-
|
379
|
-
input.addEventListener('click', handleCheck);
|
380
|
-
|
381
|
-
return listCheckboxElement;
|
382
|
-
};
|
383
|
-
}
|
384
|
-
|
385
|
-
function realignOverlay (originalScrollPosition, target) {
|
386
|
-
return function () {
|
387
|
-
const currentScrollPosition = window.scrollY;
|
388
|
-
|
389
|
-
if(Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
|
390
|
-
return;
|
391
|
-
}
|
392
|
-
|
393
|
-
if (currentScrollPosition) {
|
394
|
-
originalScrollPosition = currentScrollPosition;;
|
395
|
-
}
|
396
|
-
|
397
|
-
positionOverlay(target);
|
398
|
-
};
|
399
|
-
}
|
400
|
-
|
401
|
-
function positionOverlay (target) {
|
402
|
-
target.style['min-width'] = '340px';
|
403
|
-
target.style['width'] = '100%';
|
404
|
-
target.style['margin-top'] = 0;
|
405
|
-
target.style['left'] = 0;
|
406
|
-
target.style['top'] = 0;
|
407
|
-
|
408
|
-
if (isMobile()) {
|
409
|
-
const shareNavComponent = document.querySelector('.share-nav__horizontal');
|
410
|
-
const topHalfOffset = target.clientHeight + 10;
|
411
|
-
target.style['position'] = 'absolute';
|
412
|
-
target.style['margin-left'] = 0;
|
413
|
-
target.style['top'] = calculateLargerScreenHalf(shareNavComponent) === 'ABOVE' ? `-${topHalfOffset}px` : '50px';
|
414
|
-
} else {
|
415
|
-
target.style['position'] = 'absolute';
|
416
|
-
target.style['margin-left'] = '45px';
|
417
|
-
}
|
418
|
-
}
|
419
|
-
|
420
|
-
function calculateLargerScreenHalf (target) {
|
421
|
-
if (!target) {
|
422
|
-
return 'BELOW';
|
423
|
-
}
|
424
|
-
|
425
|
-
const vh = Math.min(document.documentElement.clientHeight || 0, window.innerHeight || 0);
|
426
|
-
|
427
|
-
const targetBox = target.getBoundingClientRect();
|
428
|
-
const spaceAbove = targetBox.top;
|
429
|
-
const spaceBelow = vh - targetBox.bottom;
|
430
|
-
|
431
|
-
return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
|
432
|
-
}
|
433
|
-
|
434
|
-
async function getLists (contentId) {
|
435
|
-
return myFtClient.getListsContent()
|
436
|
-
.then(results => results.items.map(list => {
|
437
|
-
const isChecked = Array.isArray(list.content) && list.content.some(content => content.uuid === contentId);
|
438
|
-
return { name: list.name, uuid: list.uuid, checked: isChecked, content: list.content, isPublic: list.isPublic };
|
439
|
-
}));
|
440
|
-
}
|
441
|
-
|
442
|
-
function triggerAddToListEvent (contentId, listId) {
|
443
|
-
return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
|
444
|
-
detail: {
|
445
|
-
category: 'list',
|
446
|
-
action: 'add-success',
|
447
|
-
article_id: contentId,
|
448
|
-
list_id: listId,
|
449
|
-
teamName: 'customer-products-us-growth',
|
450
|
-
amplitudeExploratory: true
|
451
|
-
},
|
452
|
-
bubbles: true
|
453
|
-
}));
|
454
|
-
}
|
455
|
-
|
456
|
-
function triggerRemoveFromListEvent (contentId, listId) {
|
457
|
-
return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
|
458
|
-
detail: {
|
459
|
-
category: 'list',
|
460
|
-
action: 'remove-success',
|
461
|
-
article_id: contentId,
|
462
|
-
list_id: listId,
|
463
|
-
teamName: 'customer-products-us-growth',
|
464
|
-
amplitudeExploratory: true
|
465
|
-
},
|
466
|
-
bubbles: true
|
467
|
-
}));
|
468
|
-
}
|
469
|
-
|
470
|
-
function triggerCreateListEvent (contentId, listId) {
|
471
|
-
document.body.dispatchEvent(new CustomEvent('oTracking.event', {
|
472
|
-
detail: {
|
473
|
-
category: 'list',
|
474
|
-
action: 'create-success',
|
475
|
-
article_id: contentId,
|
476
|
-
list_id: listId,
|
477
|
-
teamName: 'customer-products-us-growth',
|
478
|
-
amplitudeExploratory: true
|
479
|
-
},
|
480
|
-
bubbles: true
|
481
|
-
}));
|
482
|
-
}
|
483
|
-
|
484
|
-
// Temporary event on the public toggle feature.
|
485
|
-
// These will be used to build a sanity check dashboard, and will be removed after we get clean-up this test.
|
486
|
-
function triggerAddToNewListEvent () {
|
487
|
-
document.body.dispatchEvent(new CustomEvent('oTracking.event', {
|
488
|
-
detail: {
|
489
|
-
category: 'publicToggle',
|
490
|
-
action: 'addToNewList',
|
491
|
-
teamName: 'customer-products-us-growth',
|
492
|
-
amplitudeExploratory: true
|
493
|
-
},
|
494
|
-
bubbles: true
|
495
|
-
}));
|
496
|
-
}
|
497
|
-
|
498
|
-
//Temporary event to determine whether users need to scroll to add to a list
|
499
|
-
function checkScrollToAdd () {
|
500
|
-
//if the bottom of the overlay was not showing and scrolling has occurred since it was opened
|
501
|
-
if(listOverlayBottom > window.innerHeight && window.scrollY > scrolledOnOpen) {
|
502
|
-
document.body.dispatchEvent(new CustomEvent('oTracking.event', {
|
503
|
-
detail: {
|
504
|
-
category: 'publicToggle',
|
505
|
-
action: 'scrollToAdd',
|
506
|
-
teamName: 'customer-products-us-growth',
|
507
|
-
amplitudeExploratory: true
|
508
|
-
},
|
509
|
-
bubbles: true
|
510
|
-
}));
|
511
|
-
}
|
512
|
-
}
|