-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcomment-widget.js
532 lines (460 loc) · 23.2 KB
/
comment-widget.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
/*
(PLEASE DO NOT DELETE THIS HEADER OR CREDIT!)
User customizable settings below!
Please refer to my guide over on https://virtualobserver.moe/ayano/comment-widget if you're confused on how to use this.
The IDs at the top are a requirement but everything else is optional!
Do not delete any settings even if you aren't using them! It could break the program.
After filling out your options, just paste this anywhere you want a comment section
(But change the script src URL to wherever you have this widget stored on your site!)
<div id="c_widget"></div>
<script src="comment-widget.js"></script>
Have fun! Bug reports are encouraged if you happen to run into any issues.
- Ayano (https://virtualobserver.moe/)
*/
// The values in this section are REQUIRED for the widget to work! Keep them in quotes!
const s_stylePath = 'comment-widget-dark.css';
const s_formId = '1FAIpQLSd2evvFWnVp1Sdvv7Eucah91qffzi_xziHatxpK3f6kaOKQNg';
const s_nameId = '664103297';
const s_websiteId = '1394927891';
const s_textId = '1217615441';
const s_pageId = '278754493';
const s_replyId = '338039413';
const s_sheetId = '1J7QZfk4szfoESAFVRA5JD9BAvX47Mj1PZhhVsHqe2EA';
// The values below are necessary for accurate timestamps, I've filled it in with EST as an example
const s_timezone = -5; // Your personal timezone (Example: UTC-5:00 is -5 here, UTC+10:30 would be 10.5)
const s_daylightSavings = true; // If your personal timezone uses DST, set this to true
// For the dates DST start and end where you live: [Month, Weekday, which number of that weekday, hour (24 hour time)]
const s_dstStart = ['March', 'Sunday', 2, 2]; // Example shown is the second Sunday of March at 2:00 am
const s_dstEnd = ['November', 'Sunday', 1, 2]; // Example shown is the first Sunday of November at 2:00 am
// Misc - Other random settings
const s_commentsPerPage = 5; // The max amount of comments that can be displayed on one page, any number >= 1 (Replies not counted)
const s_maxLength = 500; // The max character length of a comment
const s_maxLengthName = 16; // The max character length of a name
const s_commentsOpen = true; // Change to false if you'd like to close your comment section site-wide (Turn it off on Google Forms too!)
const s_collapsedReplies = true; // True for collapsed replies with a button, false for replies to display automatically
const s_longTimestamp = false; // True for a date + time, false for just the date
let s_includeUrlParameters = false; // Makes new comment sections on pages with URL parameters when set to true (If you don't know what this does, leave it disabled)
const s_fixRarebitIndexPage = false; // If using Rarebit, change to true to make the index page and page 1 of your webcomic have the same comment section
// Word filter - Censor profanity, etc
const s_wordFilterOn = false; // True for on, false for off
const s_filterReplacement = '****'; // Change what filtered words are censored with (**** is the default)
const s_filteredWords = [ // Add words to filter by putting them in quotes and separating with commas (ie. 'heck', 'dang')
'heck', 'dang'
]
// Text - Change what messages/text appear on the form and in the comments section (Mostly self explanatory)
const s_widgetTitle = "Cbass92's guestbook";
const s_nameFieldLabel = 'Name';
const s_websiteFieldLabel = 'Website (Optional)';
const s_textFieldLabel = '';
const s_submitButtonLabel = 'SEND';
const s_loadingText = 'Loading guestbook...';
const s_noCommentsText = 'No comments yet!';
const s_closedCommentsText = 'Comments are closed temporarily!';
const s_websiteText = 'Website'; // The links to websites left by users on their comments
const s_replyButtonText = 'Reply'; // The button for replying to someone
const s_replyingText = 'Replying to'; // The text that displays while the user is typing a reply
const s_expandRepliesText = 'Show Replies';
const s_leftButtonText = '<-';
const s_rightButtonText = '->';
/*
DO NOT edit below this point unless you are confident you know what you're doing!
Everything else is automatic, you don't have to change anything else. ^^
However, feel free to edit this code as much as you like! Just please don't remove my credit if possible <3
*/
// Fix the URL parameters setting for Rarebit just in case
if (s_fixRarebitIndexPage) {s_includeUrlParameters = true}
// Apply CSS
const c_cssLink = document.createElement('link');
c_cssLink.type = 'text/css';
c_cssLink.rel = 'stylesheet';
c_cssLink.href = s_stylePath;
document.getElementsByTagName('head')[0].appendChild(c_cssLink);
// HTML Form
const v_mainHtml = `
<div id="c_inputDiv">
<form id="c_form" onsubmit="c_submitButton.disabled = true; v_submitted = true;" method="post" target="c_hiddenIframe" action="https://docs.google.com/forms/d/e/${s_formId}/formResponse"></form>
</div>
<div id="c_container">${s_loadingText}</div>
`;
const v_formHtml = `
<h2 id="c_widgetTitle">${s_widgetTitle}</h2>
<div id="c_nameWrapper" class="c-inputWrapper">
<label class="c-label c-nameLabel" for="entry.${s_nameId}">${s_nameFieldLabel}</label>
<input class="c-input c-nameInput" name="entry.${s_nameId}" id="entry.${s_nameId}" type="text" maxlength="${s_maxLengthName}" required>
</div>
<div id="c_websiteWrapper" class="c-inputWrapper">
<label class="c-label c-websiteLabel" for="entry.${s_websiteId}">${s_websiteFieldLabel}</label>
<input class="c-input c-websiteInput" name="entry.${s_websiteId}" id="entry.${s_websiteId}" type="url" pattern="https://.*">
</div>
<div id="c_textWrapper" class="c-inputWrapper">
<label class="c-label c-textLabel" for="entry.${s_textId}">${s_textFieldLabel}</label>
<textarea class="c-input c-textInput" name="entry.${s_textId}" id="entry.${s_textId}" rows="4" cols="50" maxlength="${s_maxLength}" required></textarea>
</div>
<input id="c_submitButton" name="c_submitButton" type="submit" value="${s_submitButtonLabel}" disabled>
`;
// Insert main HTML to page
document.getElementById('c_widget').innerHTML = v_mainHtml;
const c_form = document.getElementById('c_form');
if (s_commentsOpen) {c_form.innerHTML = v_formHtml}
else {c_form.innerHTML = s_closedCommentsText}
// Initialize misc things
const c_container = document.getElementById('c_container');
let v_pageNum = 1;
let v_amountOfPages = 1;
let v_commentMax = 1;
let v_commentMin = 1;
// Set up the word filter if applicable
let v_filteredWords;
if (s_wordFilterOn) {
v_filteredWords = s_filteredWords.join('|');
v_filteredWords = new RegExp(String.raw `\b(${v_filteredWords})\b`, 'ig');
}
// The fake button is just a dummy placeholder for when comments are closed
let c_submitButton;
if (s_commentsOpen) {c_submitButton = document.getElementById('c_submitButton')}
else {c_submitButton = document.createElement('button')}
// Add invisible page input to document
let v_pagePath = window.location.pathname;
if (s_includeUrlParameters) {v_pagePath += window.location.search}
if (s_fixRarebitIndexPage && v_pagePath == '/') {v_pagePath = '/?pg=1'}
const c_pageInput = document.createElement('input');
c_pageInput.value = v_pagePath; c_pageInput.type = 'text'; c_pageInput.style.display = 'none';
c_pageInput.id = 'entry.' + s_pageId; c_pageInput.name = c_pageInput.id;
c_form.appendChild(c_pageInput);
// Add the "Replying to..." text to document
let c_replyingText = document.createElement('span');
c_replyingText.style.display = 'none'; c_replyingText.id = 'c_replyingText';
c_form.appendChild(c_replyingText);
c_replyingText = document.getElementById('c_replyingText');
// Add the invisible reply input to document
let c_replyInput = document.createElement('input');
c_replyInput.type = 'text'; c_replyInput.style.display = 'none';
c_replyInput.id = 'entry.' + s_replyId; c_replyInput.name = c_replyInput.id;
c_form.appendChild(c_replyInput);
c_replyInput = document.getElementById('entry.' + s_replyId);
// Add the invisible iFrame to the document for catching the default Google Forms submisson page
let v_submitted = false;
let c_hiddenIframe = document.createElement('iframe');
c_hiddenIframe.id = 'c_hiddenIframe'; c_hiddenIframe.name = 'c_hiddenIframe'; c_hiddenIframe.style.display = 'none'; c_hiddenIframe.setAttribute('onload', 'if(v_submitted){fixFrame()}');
c_form.appendChild(c_hiddenIframe);
c_hiddenIframe = document.getElementById('c_hiddenIframe');
// Fix the invisible iFrame so it doesn't keep trying to load stuff
function fixFrame() {
v_submitted = false;
c_hiddenIframe.srcdoc = '';
getComments(); // Reload comments after submission
}
// Processes comment data with the Google Sheet ID
function getComments() {
// Disable the submit button while comments are reloaded
c_submitButton.disabled;
// Reset reply stuff to default
c_replyingText.style.display = 'none';
c_replyInput.value = '';
// Clear input fields too
if (s_commentsOpen) {
document.getElementById(`entry.${s_nameId}`).value = '';
document.getElementById(`entry.${s_websiteId}`).value = '';
document.getElementById(`entry.${s_textId}`).value = '';
}
// Get the data
const url = `https://docs.google.com/spreadsheets/d/${s_sheetId}/gviz/tq?`;
const retrievedSheet = getSheet(url);
// Do stuff with the data here
retrievedSheet.then(result => {
// The data comes with extra stuff at the beginning, get rid of it
const json = JSON.parse(result.split('\n')[1].replace(/google.visualization.Query.setResponse\(|\);/g, ''));
// Need index of page column for checking if comments are for the right page
const isPage = (col) => col.label == 'Page';
let pageIdx = json.table.cols.findIndex(isPage);
// Turn that data into usable comment data
// All of the messy val checks are because Google Sheets can be weird sometimes with comment deletion
let comments = [];
if (json.table.parsedNumHeaders > 0) { // Check if any comments exist in the sheet at all before continuing
for (r = 0; r < json.table.rows.length; r++) {
// Check for null rows
let val1;
if (!json.table.rows[r].c[pageIdx]) {val1 = ''}
else {val1 = json.table.rows[r].c[pageIdx].v}
// Check if the page name matches before adding to comment array
if (val1 == v_pagePath) {
let comment = {}
for (c = 0; c < json.table.cols.length; c++) {
// Check for null values
let val2;
if (!json.table.rows[r].c[c]) {val2 = ''}
else {val2 = json.table.rows[r].c[c].v}
// Finally set the value properly
comment[json.table.cols[c].label] = val2;
}
comment.Timestamp2 = json.table.rows[r].c[0].f;
comments.push(comment);
}
}
}
// Check for empty comments before displaying to page
if (comments.length == 0 || Object.keys(comments[0]).length < 2) { // Once again, Google Sheets can be weird
c_container.innerHTML = s_noCommentsText;
} else {displayComments(comments)}
c_submitButton.disabled = false // Now that everything is done, re-enable the submit button
})
}
// Fetches the Google Sheet resource from the provided URL
function getSheet(url) {
return new Promise(function (resolve, reject) {
fetch(url).then(response => {
if (!response.ok) {reject('Could not find Google Sheet with that URL')} // Checking for a 404
else {
response.text().then(data => {
if (!data) {reject('Invalid data pulled from sheet')}
resolve(data);
})
}
})
})
}
// Displays comments on page
let a_commentDivs = []; // For use in other functions
function displayComments(comments) {
// Clear for re-display
a_commentDivs = [];
c_container.innerHTML = '';
// Get all reply comments by taking them out of the comment array
let replies = [];
for (i = 0; i < comments.length; i++) {
if (comments[i].Reply) {
replies.push(comments[i]);
comments.splice(i, 1);
i--;
}
}
// Values for pagination
v_amountOfPages = Math.ceil(comments.length / s_commentsPerPage);
v_commentMax = s_commentsPerPage * v_pageNum;
v_commentMin = v_commentMax - s_commentsPerPage;
// Main comments (not replies)
comments.reverse(); // Newest comments go to top
for (i = 0; i < comments.length; i++) {
let comment = createComment(comments[i]);
// Reply button
let button = document.createElement('button');
button.innerHTML = s_replyButtonText;
button.value = comment.id;
button.setAttribute('onclick', `openReply(this.value)`);
button.className = 'c-replyButton';
comment.appendChild(button);
// Choose whether to display or not based on page number
comment.style.display = 'none';
if (i >= v_commentMin && i < v_commentMax) {comment.style.display = 'block'}
comment.className = 'c-comment';
c_container.appendChild(comment);
a_commentDivs.push(document.getElementById(comment.id)); // Add to array for use later
}
// Replies
for (i = 0; i < replies.length; i++) {
let reply = createComment(replies[i]);
const parentId = replies[i].Reply;
const parentDiv = document.getElementById(parentId);
// Check if a container doesn't already exist for this comment, if not, make one
let container;
if (!document.getElementById(parentId + '-replies')) {
container = document.createElement('div');
container.id = parentId + '-replies';
if (s_collapsedReplies) {container.style.display = 'none'} // Default to hidden if collapsed
container.className = 'c-replyContainer';
parentDiv.appendChild(container);
} else {container = document.getElementById(parentId + '-replies')}
reply.className = 'c-reply';
container.appendChild(reply);
}
// Handle adding the buttons to show or hide replies if collapsed replies are enabled
if (s_collapsedReplies) {
const containers = document.getElementsByClassName('c-replyContainer');
for (i = 0; i < containers.length; i++) {
const num = containers[i].childNodes.length;
const parentDiv = containers[i].parentElement;
// The button to expand replies
const button = document.createElement('button');
button.innerHTML = s_expandRepliesText + ` (${num})`;
button.setAttribute('onclick', `expandReplies(this.parentElement.id)`);
button.className = 'c-expandButton';
parentDiv.insertBefore(button, parentDiv.lastChild);
}
}
// Handle pagination if there's more than one page
if (v_amountOfPages > 1) {
let pagination = document.createElement('div');
leftButton = document.createElement('button');
leftButton.innerHTML = s_leftButtonText; leftButton.id = 'c_leftButton'; leftButton.name = 'left';
leftButton.setAttribute('onclick', `changePage(this.name)`);
if (v_pageNum == 1) {leftButton.disabled = true} // Can't go before page 1
leftButton.className = 'c-paginationButton';
pagination.appendChild(leftButton);
rightButton = document.createElement('button');
rightButton.innerHTML = s_rightButtonText; rightButton.id = 'c_rightButton'; rightButton.name = 'right';
rightButton.setAttribute('onclick', `changePage(this.name)`);
if (v_pageNum == v_amountOfPages) {rightButton.disabled = true} // Can't go after the last page
rightButton.className = 'c-paginationButton';
pagination.appendChild(rightButton);
pagination.id = 'c_pagination';
c_container.appendChild(pagination);
}
}
// Create basic HTML comment, reply or not
function createComment(data) {
let comment = document.createElement('div');
// Get the right timestamps
let timestamps = convertTimestamp(data.Timestamp);
let timestamp;
if (s_longTimestamp) {timestamp = timestamps[0]}
else {timestamp = timestamps[1]}
// Set the ID (uses Name + Full Timestamp format)
const id = data.Name + '|--|' + data.Timestamp2;
comment.id = id;
// Name of user
let name = document.createElement('h3');
let filteredName = data.Name;
if (s_wordFilterOn) {filteredName = filteredName.replace(v_filteredWords, s_filterReplacement)}
name.innerText = filteredName;
name.className = 'c-name';
comment.appendChild(name);
// Timestamp
let time = document.createElement('span');
time.innerText = timestamp;
time.className = 'c-timestamp';
comment.appendChild(time);
// Website URL, if one was provided
if (data.Website) {
let site = document.createElement('a');
site.innerText = s_websiteText;
site.target = "_blank"
site.href = data.Website;
site.className = 'c-site';
comment.appendChild(site);
}
// Text content
let text = document.createElement('p');
let filteredText = data.Text;
if (s_wordFilterOn) {filteredText = filteredText.replace(v_filteredWords, s_filterReplacement)}
text.innerText = filteredText;
text.className = 'c-text';
comment.appendChild(text);
return comment;
}
// Makes the Google Sheet timestamp usable
function convertTimestamp(timestamp) {
const vals = timestamp.split('(')[1].split(')')[0].split(',');
const date = new Date(vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]);
const timezoneDiff = (s_timezone * 60 + date.getTimezoneOffset()) * -1;
let offsetDate = new Date(date.getTime() + timezoneDiff * 60 * 1000);
if (s_daylightSavings) {offsetDate = isDST(offsetDate)}
return [offsetDate.toLocaleString(), offsetDate.toLocaleDateString()];
}
// DST checker
function isDST(date) {
const dstStart = [getMonthNum(s_dstStart[0]), getDayNum(s_dstStart[1]), s_dstStart[2], s_dstStart[3]];
const dstEnd = [getMonthNum(s_dstEnd[0]), getDayNum(s_dstEnd[1]), s_dstEnd[2], s_dstEnd[3]];
const year = date.getFullYear();
let startDate = new Date(year, dstStart[0], 1);
startDate = nthDayOfMonth(dstStart[1], dstStart[2], startDate, dstStart[3]).getTime();
let endDate = new Date(year, dstEnd[0], 1);
endDate = nthDayOfMonth(dstEnd[1], dstEnd[2], endDate, dstEnd[3]).getTime();
time = date.getTime();
if (time >= startDate && time < endDate) {date.setHours(date.getHours() - 1)}
return date;
}
// Thank you to https://stackoverflow.com/questions/32192982/get-a-given-weekday-in-a-given-month-with-javascript for the below function
function nthDayOfMonth(day, n, date, hour) {
var count = 0;
var idate = new Date(date);
idate.setDate(1);
while ((count) < n) {
idate.setDate(idate.getDate() + 1);
if (idate.getDay() == day) {
count++;
}
}
idate.setHours(hour);
return idate;
}
// Convert weekday and month names into numbers
function getDayNum(day) {
let num;
switch (day.toLowerCase()) {
case 'sunday': num = 0; break;
case 'monday': num = 1; break;
case 'tuesday': num = 2; break;
case 'wednesday': num = 3; break;
case 'thursday': num = 4; break;
case 'friday': num = 5; break;
case 'saturday': num = 6; break;
default: num = 0; break;
}
return num;
}
function getMonthNum(month) {
let num;
switch (month.toLowerCase()) {
case 'january': num = 0; break;
case 'february': num = 1; break;
case 'march': num = 2; break;
case 'april': num = 3; break;
case 'may': num = 4; break;
case 'june': num = 5; break;
case 'july': num = 6; break;
case 'august': num = 7; break;
case 'september': num = 8; break;
case 'october': num = 9; break;
case 'november': num = 10; break;
case 'december': num = 11; break;
}
return num;
}
// Handle making replies
const link = document.createElement('a');
link.href = '#c_inputDiv';
function openReply(id) {
if (c_replyingText.style.display == 'none') {
c_replyingText.innerHTML = s_replyingText + ` ${id.split('|--|')[0]}...`;
c_replyInput.value = id;
c_replyingText.style.display = 'block';
} else {
c_replyingText.innerHTML = '';
c_replyInput.value = '';
c_replyingText.style.display = 'none';
}
link.click(); // Jump to the space to type
}
// Handle expanding replies (should only be accessible with collapsed replies enabled)
function expandReplies(id) {
const targetDiv = document.getElementById(`${id}-replies`);
if (targetDiv.style.display == 'none') {targetDiv.style.display = 'block'}
else {targetDiv.style.display = 'none'}
}
function changePage(dir) {
const leftButton = document.getElementById('c_leftButton');
const rightButton = document.getElementById('c_rightButton');
// Find directional number
let num;
switch (dir) {
case 'left': num = -1; break;
case 'right': num = 1; break;
default: num = 0; break;
}
let targetPage = v_pageNum + num;
// Cancel if impossible direction for safety, should never happen though
if (targetPage > v_amountOfPages || targetPage < 1) {return}
// Enable/disable buttons if needed
leftButton.disabled = false; rightButton.disabled = false;
if (targetPage == 1) {leftButton.disabled = true} // Can't go before page 1
if (targetPage == v_amountOfPages) {rightButton.disabled = true} // Can't go past the last page
// Hide all comments and then display the correct ones
v_pageNum = targetPage;
v_commentMax = s_commentsPerPage * v_pageNum;
v_commentMin = v_commentMax - s_commentsPerPage;
for (i = 0; i < a_commentDivs.length; i++) {
a_commentDivs[i].style.display = 'none';
if (i >= v_commentMin && i < v_commentMax) {a_commentDivs[i].style.display = 'block'}
}
}
getComments(); // Run once on page load