File size: 45,281 Bytes
4614287 2ee4481 8137781 0c4660b 2ee4481 304cdac a4a5767 304cdac 1b77e8d de48b4c cb64b5f de48b4c 4614287 125ee1c 2ee4481 a4a5767 9db4577 a4a5767 6c35a72 a4a5767 1b77e8d a4a5767 6c35a72 0c4660b a4a5767 6c35a72 1b77e8d a4a5767 9db4577 a4a5767 1b77e8d a4a5767 0c4660b f4cd2d5 a4a5767 1b77e8d a4a5767 63cfb2c 87def91 5dbc043 9db4577 5dbc043 9db4577 5dbc043 6c35a72 63cfb2c 6c35a72 9db4577 6c35a72 63cfb2c 9db4577 5dbc043 6c35a72 63cfb2c 6c35a72 9db4577 63cfb2c 5fa765b 5dbc043 cb64b5f 5dbc043 6c35a72 cb64b5f ceed7c1 5dbc043 6c35a72 ceed7c1 cb64b5f 5fa765b 5dbc043 ceed7c1 cb64b5f ceed7c1 5dbc043 da2266a 2ee4481 a05c9de 8679a50 da2266a 304cdac a05c9de 8679a50 9db4577 8679a50 9db4577 8679a50 9db4577 8244a47 9db4577 8244a47 8679a50 9db4577 8679a50 9db4577 6c35a72 9db4577 6c35a72 a05c9de 6c35a72 9db4577 cb64b5f 9db4577 cb64b5f de48b4c a05c9de 9db4577 0c4660b 9db4577 a05c9de 8679a50 1b77e8d 6c35a72 9db4577 6c35a72 1b77e8d 9113af3 a05c9de 8679a50 6c35a72 9db4577 6c35a72 5dbc043 a4a5767 5fa765b 5dbc043 8679a50 8137781 63cfb2c 8679a50 8137781 8679a50 da2266a 4614287 6c35a72 1b77e8d 2ee4481 4614287 8244a47 4614287 1b77e8d 4614287 a4a5767 9113af3 a4a5767 6c35a72 a4a5767 6c35a72 a4a5767 a05c9de 8679a50 da2266a 2ee4481 da2266a 2ee4481 da2266a 2ee4481 da2266a 2ee4481 da2266a f2474aa 9db4577 f2474aa da2266a 9074889 da2266a 9074889 f2474aa 9074889 2ee4481 da2266a 0c4660b 9db4577 da2266a 9db4577 2ee4481 8137781 2ee4481 fbdab60 da2266a 2ee4481 a90c443 9074889 2ee4481 1b77e8d 2ee4481 4614287 1b77e8d 2ee4481 9db4577 2ee4481 1b77e8d 2ee4481 8679a50 0c4660b 8679a50 9db4577 8679a50 0c4660b 8679a50 0c4660b 5dbc043 2ee4481 0c4660b 8137781 8244a47 8679a50 125ee1c 8679a50 9db4577 8679a50 9db4577 9c1561c a05c9de 4614287 125ee1c 8244a47 9db4577 a05c9de 9c1561c 4614287 1b77e8d 6c35a72 9db4577 1b77e8d 125ee1c 1b77e8d 9113af3 9db4577 1b77e8d 125ee1c 1b77e8d f419f31 1b77e8d 8244a47 8679a50 9db4577 8679a50 9db4577 0c4660b 8244a47 0c4660b 8244a47 0c4660b cb64b5f 8244a47 9db4577 1b77e8d 8244a47 304cdac 0c4660b 125ee1c 0c4660b 8244a47 0c4660b 9cb8062 0c4660b 9cb8062 0c4660b 9cb8062 0c4660b a4a5767 0c4660b a4a5767 0c4660b a4a5767 9db4577 a4a5767 9db4577 a4a5767 6c35a72 9db4577 6c35a72 cb64b5f de48b4c cb64b5f de48b4c cb64b5f 6c35a72 cb64b5f 6c35a72 9db4577 4614287 9db4577 4614287 9db4577 4614287 9db4577 4614287 9db4577 4614287 9db4577 4614287 9db4577 4614287 125ee1c 4614287 fcb80e0 4614287 125ee1c 4614287 |
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 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 |
const wordsFrequencyTableTitleText = "Words Freq. Stats"
const wordsFrequencyTableTitleMobileText = "Words Freq. Stats"
let wfo = {
"words_frequency": {},
"nTotalRows": null,
"rowArray": []
}
const editorFieldLabel = "editor"
const remoteWebServer = "http://localhost:7860"
const underlinedPrimary = "underlinedBlue"
const underlinedClicked = "underlinedDarkViolet"
const underlinedPrimaryTable = "underlinedBlueTable"
const underlinedClickedTable = "underlinedDarkVioletTable"
const objectChildNodeNamesToParse = {
"#text": "textContent",
"SPAN": "textContent"
}
const mobileInnerSize = 767
const minNCharsMore = 10
/**
* Object containing functions for word frequency analysis.
*
* @property {function} 'id-input-webserver-wordfreq-checkbox' - Analyzes input text using webserver API.
* @property {function} 'stemmer-embedded' - Analyzes input text using embedded functionality.
*/
const wordsFrequencyAnalyzers = {
/**
* Analyzes input text using 'My Ghost Writer' webserver API.
*
* @param {Array<Object>} arrayOfValidTextChildWithNrow - Array of objects representing text rows to analyze.
* @returns {Promise<void>} Populates the frequency tables with the response from the webserver.
*/
"id-input-webserver-wordfreq-checkbox": async function(arrayOfValidTextChildWithNrow) {
let bodyRequest = {"text": arrayOfValidTextChildWithNrow}
console.log("use the webserver for word freq analysis...")
const wordsFrequencyURL = parseWebserverDomain()
try {
let response = await fetch(wordsFrequencyURL, {
method: "POST",
body: JSON.stringify(bodyRequest)
})
console.assert(response.status === 200, `response.status: ${response.status}!`)
let bodyResponseJson = await response.json()
setElementCssClassById("waiting-for-be", "display-none")
let freq = bodyResponseJson["words_frequency"]
let nTotalRows = bodyResponseJson["n_total_rows"]
console.log(`wordsFrequencyAnalyzers::nTotalRows: '${nTotalRows}'`)
populateWordsFrequencyTables(freq, nTotalRows, arrayOfValidTextChildWithNrow)
} catch (err) {
console.error("wordsFrequencyAnalyzers::err on webserver request/response:", err, "#")
console.log(`wordsFrequencyAnalyzers::wordsFrequencyURL: ${typeof wordsFrequencyURL}:`, wordsFrequencyURL, "#")
setElementCssClassById("waiting-for-be", "display-none")
setElementCssClassById("waiting-for-be-error", "display-block")
}
},
/**
* Analyzes input text using embedded functionality.
*
* @param {Array<Object>} inputText - Array of objects representing text rows to analyze.
* @returns {void} Populates the frequency tables with the embedded analysis.
*/
'stemmer-embedded': function(inputText) {
console.log("use the embedded functionality for word freq analysis...")
try {
const bodyResponseJson = textStemming(inputText)
setElementCssClassById("waiting-for-be", "display-none")
let freq = bodyResponseJson["wordsStemsDict"]
let nTotalRows = bodyResponseJson["nTotalRows"]
console.log(`getWordsFreq::nTotalRows: '${nTotalRows}', populateWordsFrequencyTables...`)
populateWordsFrequencyTables(freq, nTotalRows, inputText)
// temp until we have the new UI
let hiddenOutputSpan = document.getElementById("id-hidden-editor")
hiddenOutputSpan.textContent = JSON.stringify(freq, null, 2)
} catch (err) {
console.error("getWordsFrequency::err on useWordfreqWebserver:", err, "#")
setElementCssClassById("waiting-for-be", "display-none")
setElementCssClassById("waiting-for-be-error", "display-block")
}
}
}
// lunr.stemmer
// Copyright (C) 2020 Oliver Nightingale, Code included under the MIT license
// Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt
const porterStemmer = (function(){
let step2list = {
"ational" : "ate",
"tional" : "tion",
"enci" : "ence",
"anci" : "ance",
"izer" : "ize",
"bli" : "ble",
"alli" : "al",
"entli" : "ent",
"eli" : "e",
"ousli" : "ous",
"ization" : "ize",
"ation" : "ate",
"ator" : "ate",
"alism" : "al",
"iveness" : "ive",
"fulness" : "ful",
"ousness" : "ous",
"aliti" : "al",
"iviti" : "ive",
"biliti" : "ble",
"logi" : "log"
},
step3list = {
"icate" : "ic",
"ative" : "",
"alize" : "al",
"iciti" : "ic",
"ical" : "ic",
"ful" : "",
"ness" : ""
},
c = "[^aeiou]", // consonant
v = "[aeiouy]", // vowel
C = c + "[^aeiouy]*", // consonant sequence
V = v + "[aeiou]*", // vowel sequence
mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0
meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1
mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1
s_v = "^(" + C + ")?" + v; // vowel in stem
return function (w) {
let stem,
suffix,
firstch,
re,
re2,
re3,
re4,
origword = w;
if (w.length < 3) { return w; }
firstch = w.substr(0,1);
if (firstch === "y") {
w = firstch.toUpperCase() + w.substr(1);
}
// Step 1a
re = /^(.+?)(ss|i)es$/;
re2 = /^(.+?)([^s])s$/;
if (re.test(w)) { w = w.replace(re,"$1$2"); }
else if (re2.test(w)) { w = w.replace(re2,"$1$2"); }
// Step 1b
re = /^(.+?)eed$/;
re2 = /^(.+?)(ed|ing)$/;
if (re.test(w)) {
let fp = re.exec(w);
re = new RegExp(mgr0);
if (re.test(fp[1])) {
re = /.$/;
w = w.replace(re,"");
}
} else if (re2.test(w)) {
let fp = re2.exec(w);
stem = fp[1];
re2 = new RegExp(s_v);
if (re2.test(stem)) {
w = stem;
re2 = /(at|bl|iz)$/;
re3 = new RegExp("([^aeiouylsz])\\1$");
re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
if (re2.test(w)) { w = w + "e"; }
else if (re3.test(w)) { re = /.$/; w = w.replace(re,""); }
else if (re4.test(w)) { w = w + "e"; }
}
}
// Step 1c
re = /^(.+?)y$/;
if (re.test(w)) {
let fp = re.exec(w);
stem = fp[1];
re = new RegExp(s_v);
if (re.test(stem)) { w = stem + "i"; }
}
// Step 2
re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
if (re.test(w)) {
let fp = re.exec(w);
stem = fp[1];
suffix = fp[2];
re = new RegExp(mgr0);
if (re.test(stem)) {
w = stem + step2list[suffix];
}
}
// Step 3
re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
if (re.test(w)) {
let fp = re.exec(w);
stem = fp[1];
suffix = fp[2];
re = new RegExp(mgr0);
if (re.test(stem)) {
w = stem + step3list[suffix];
}
}
// Step 4
re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
re2 = /^(.+?)(s|t)(ion)$/;
if (re.test(w)) {
let fp = re.exec(w);
stem = fp[1];
re = new RegExp(mgr1);
if (re.test(stem)) {
w = stem;
}
} else if (re2.test(w)) {
let fp = re2.exec(w);
stem = fp[1] + fp[2];
re2 = new RegExp(mgr1);
if (re2.test(stem)) {
w = stem;
}
}
// Step 5
re = /^(.+?)e$/;
if (re.test(w)) {
let fp = re.exec(w);
stem = fp[1];
re = new RegExp(mgr1);
re2 = new RegExp(meq1);
re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {
w = stem;
}
}
re = /ll$/;
re2 = new RegExp(mgr1);
if (re.test(w) && re2.test(w)) {
re = /.$/;
w = w.replace(re,"");
}
// and turn initial Y back to y
if (firstch === "y") {
w = firstch.toLowerCase() + w.substr(1);
}
return w;
}
})();
/**
* Filters elements from an array based on specified conditions.
* @param {Array} inputArray - The array of elements to filter.
* @param {boolean} [filterWhitespaces=false] - Whether to remove elements that are only whitespace.
* @param {Array<string>} [filterArgs=["", " "]] - List of elements to exclude from the array.
* @returns {Array} - The filtered array.
*/
function filterElementsFromList(inputArray, filterWhitespaces = false, filterArgs=["", " "]) {
if (filterWhitespaces) {
inputArray = inputArray.filter(e => String(e).trim());
}
return inputArray.filter((x) => !filterArgs.includes(x));
}
/**
* Tokenizes a string using a custom pattern and filters out specified elements.
* @param {string} s - The input string to tokenize.
* @param {RegExp} [pattern=/([A-Za-zÀ-ÿ-]+|[0-9._]+|.|!|\?|'|"|:|;|,|-)/i] - The regex pattern for tokenization.
* @param {boolean} [filterWhitespaces=true] - Whether to remove whitespace elements after tokenization.
* @returns {Array<string>} - The list of tokens filtered based on specified conditions.
*/
function customWordPunctTokenize(s, pattern = /([A-Za-zÀ-ÿ-]+|[0-9._]+|.|!|\?|'|"|:|;|,|-)/i, filterWhitespaces=true) {
const results = s.split(pattern)
return filterElementsFromList(results, filterWhitespaces)
}
/**
* Applies Porter Stemmer algorithm to reduce words in a given text to their base form,
* then produces a dictionary of word frequencies with, for every recognized base form,
* a list of these repeated words with their position.
*
* Handles nested SPAN childNodes and tracks idxRow, idxRowChild, idxRowParent.
*
* @param {Array<Object>} textSplitNewline - Array of objects: {idxRow, text, idxRowChild, idxRowParent}
* @returns {Object} - { nTotalRows, wordsStemsDict }
*/
function textStemming(textSplitNewline) {
// textSplitNewline: [{idxRow: number, text: string, idxRowChild: number|null, idxRowParent: number|null}]
const wordsStemsDict = {};
let nTotalRows = textSplitNewline.length;
textSplitNewline.forEach((data) => {
const row = data.text;
const idxRow = data.idxRow;
const idxRowChild = data.idxRowChild ?? null;
const idxRowParent = data.idxRowParent ?? null;
const tokens = customWordPunctTokenize(row);
const offsets = getOffsets(row, tokens);
tokens.forEach((word, i) => {
const wordLower = word.toLowerCase();
const stem = porterStemmer(wordLower);
if (!wordsStemsDict[stem]) {
wordsStemsDict[stem] = { count: 0, word_prefix: stem, offsets_array: [] };
}
wordsStemsDict[stem].count += 1;
wordsStemsDict[stem].offsets_array.push({
word: word, // keep original casing for display
offsets: [offsets[i].start, offsets[i].end],
n_row: idxRow,
n_row_child: idxRowChild,
n_row_parent: idxRowParent
});
});
});
return { nTotalRows, wordsStemsDict };
}
/**
* Get the offsets of each token in the original text.
*
* @param {string} text - The original text.
* @param {Array<string>} tokens - The tokens extracted from the text.
* @returns {Array<Object>} - Array of objects containing the start and end offsets of each token.
*/
function getOffsets(text, tokens) {
const offsets = [];
let currentIndex = 0;
tokens.forEach(token => {
const start = text.indexOf(token, currentIndex);
const end = start + token.length;
offsets.push({ start, end });
currentIndex = end;
});
return offsets;
}
/**
* Retrieves the value of a form field by its key from a FormData object.
*
* @param {string} formId - The ID of the HTML form element that contains the field.
* @param {string} key - The name attribute of the form field to retrieve.
* @returns {*} The value of the form field, or null if it does not exist in the FormData object.
*/
function getFormDataByKey(formId, key) {
let formElement = document.getElementById(formId)
const data = new FormData(formElement);
let dataValue = data.get(key)
return dataValue
}
/**
* Read and preview a selected text file.
*
* @function previewFile
* @description Displays the contents of a selected text file within an element with id 'editor'.
*/
function previewFile() {
const editor = document.getElementById(editorFieldLabel);
const [file] = document.querySelector("input[type=file]").files;
const reader = new FileReader();
reader.addEventListener("load", () => {
// this will then display a text file
editor.innerText = reader.result;
}, false
);
if (file) {
reader.readAsText(file);
}
}
/**
* Scrolls to the specified point within an element.
*
* @param {HTMLElement} editorElement - The target element to scroll.
* @param {number} yClientOffset - The offset from the top of the element to scroll to.
* @param {number} [negativeMultiplierFontSize=3] - Multiplier for font size adjustment.
*/
function scrollToGivenPoint(editorElement, yClientOffset, negativeMultiplierFontSize = 3) {
const editorComputedStyle = window.getComputedStyle(editorElement)
const fontSize = parseInt(editorComputedStyle.getPropertyValue("font-size"), 10)
const negativeOffset = fontSize * negativeMultiplierFontSize
const scrollHeight = editorElement.scrollHeight
if (yClientOffset < (scrollHeight - negativeOffset)) {
yClientOffset -= negativeOffset
}
editorElement.scrollTo(0, yClientOffset)
}
/**
* Scrolls the editor to a specified position and sets the caret at that point.
*
* @param {number} line - The line/row number (0-indexed) where the caret should be placed.
* @param {Array<number>} offsetColumn - A number array containing two numbers representing the column offsets for the start and end of the selection range.
* @param {number} nRowChild - The index of the child node (if applicable).
* @param {number} nRowParent - The index of the parent node (if applicable).
*/
function setCaret(line, offsetColumn, nRowChild, nRowParent) {
const editorElement = document.getElementById(editorFieldLabel)
editorElement.scrollTo(0, 0) // workaround: first reset the scroll position moving it at editorElement beginning
const childNodes = editorElement.childNodes
let rng = document.createRange();
let sel = window.getSelection();
let col0 = offsetColumn[0]
let col1 = offsetColumn[1]
let childNode = childNodes[line]
let subChildNode;
/// handle case of childNodes not of type #text, e.g., SPAN
if (nRowParent !== null) {
console.assert(line === nRowParent, `line ${line} nth === parent line ${nRowParent} nth???`)
}
switch (childNode.nodeName) {
case "#text":
rng.setStart(childNode, col0)
rng.setEnd(childNode, col1)
break
case "SPAN":
subChildNode = childNode.childNodes[nRowChild]
rng.setStart(subChildNode, col0)
rng.setEnd(subChildNode, col1)
break
default:
throw Error(`childNode.nodeName ${childNode.nodeName} not yet handled!`)
}
sel.removeAllRanges();
sel.addRange(rng);
editorElement.focus();
const offsetsEditor = getOffsetsWithElement(editorElement)
const yBase = offsetsEditor.top
const {y} = getBoundingClientRect(rng)
const yClientOffset = y - yBase
scrollToGivenPoint(editorElement, yClientOffset)
}
/**
* Gets the offsetTop and offsetHeight of an element.
* @param {HTMLElement} el - The element to get offsets from.
* @returns {Object} An object with 'top' and 'height' properties.
*/
function getOffsetsWithElement(el) {
return {top: el.offsetTop, height: el.offsetHeight}
}
/**
* Gets the bounding client rectangle of an element or range.
* @param {HTMLElement|Range} el - The element or range to get bounding rect from.
* @returns {Object} An object with x, y, bottom, and top properties.
*/
function getBoundingClientRect(el) {
let bounds = el.getBoundingClientRect();
return {x: bounds.left, y: bounds.y, bottom: bounds.bottom, top: bounds.top};
}
/**
* Updates the CSS class of an HTML element with the specified ID.
*
* @param {string} elementId - The ID of the HTML element to update.
* @param {string} currentClass - The new CSS class to apply to the element.
*/
function setElementCssClassById(elementId, currentClass) {
let elementWithClassToChange = document.getElementById(elementId)
elementWithClassToChange.setAttribute("class", currentClass)
}
/**
* Sets a CSS class by replacing an old class.
* @param {string} oldClassName - The old class name to replace.
* @param {string} currentClass - The new class name to set.
*/
function setElementCssClassByOldClass(oldClassName, currentClass) {
try {
let oldClassElement = document.getElementsByClassName(oldClassName)
oldClassElement[0].className = currentClass
} catch {}
}
/**
* Parses the web server domain from an input element and returns the full API endpoint.
* @returns {string} The parsed web server domain with /words-frequency endpoint.
*/
function parseWebserverDomain () {
const remoteWebServerEl = document.getElementById("id-input-webserver-wordfreq")
console.log("remoteWebServer.value:", remoteWebServerEl.value, "#")
const remoteWebServerValue = remoteWebServerEl.value ?? remoteWebServer
const remoteWebServerDomain = remoteWebServerValue.trim().replace(/\/+$/, '')
return `${remoteWebServerDomain}/words-frequency`
}
/**
* Fetches words frequency data from the server and populates the words frequency tables.
* The user can choose to use an embedded stemmer or a remote web server for processing.
*
* @async
* @function getWordsFrequency
*/
async function getWordsFrequency() {
if (isMobile()) {
toggleElementWithClassById('id-container-desktop-menu')
}
let {validChildContent} = getValidChildNodesFromEditorById(editorFieldLabel)
setElementCssClassById("waiting-for-be-error", "display-none")
setElementCssClassById("waiting-for-be", "display-block")
let wordsFrequencyTableTitleEl = document.getElementById("id-words-frequency-table-title")
let wordsFrequencyTableTitleElMobile = document.getElementById("id-words-frequency-table-title-mobile")
wordsFrequencyTableTitleEl.innerText = wordsFrequencyTableTitleText
wordsFrequencyTableTitleElMobile.innerText = wordsFrequencyTableTitleMobileText
let listOfWords = document.getElementById("id-list-of-words")
listOfWords.innerHTML = ""
let currentTableOfWords = document.getElementById("id-current-table-of-words")
currentTableOfWords.innerHTML = ""
let currentTableTitle = document.getElementById("id-current-table-of-words-title")
currentTableTitle.innerText = ""
const choiceWordFreqAnalyzerEl = document.getElementById('id-input-webserver-wordfreq-checkbox')
console.log("choiceWordFreqAnalyzerEl checked:", typeof choiceWordFreqAnalyzerEl.checked, choiceWordFreqAnalyzerEl.checked, "#")
switch (choiceWordFreqAnalyzerEl.checked) {
case true: // webserver
await wordsFrequencyAnalyzers['id-input-webserver-wordfreq-checkbox'](validChildContent)
break;
case false: // embedded
wordsFrequencyAnalyzers['stemmer-embedded'](validChildContent)
break;
default:
console.warn("No valid analyzer selected.");
break;
}
}
/**
* Returns a sorting function for the given property and order.
*
* @param {string} property - The property to sort by.
* @param {string} order - The order of sorting ('asc' or 'desc').
* @returns {function} A comparison function that sorts data in the specified order.
*/
function dynamicSort(property, order) {
let sort_order = order === "desc" ? -1 : 1
return function (a, b){
// a should come before b in the sorted order
if(a[property] < b[property]){
return -1 * sort_order;
// a should come after b in the sorted order
}else if(a[property] > b[property]){
return 1 * sort_order;
// a and b are the same
}
return 0 * sort_order;
}
}
/**
* Recursively extracts all string values from any level of a nested object or array.
*
* Traverses the input (object, array, or primitive) and collects every string value found.
* The result is a flat array of all string values found within the input.
*
* @param {*} obj - The input object, array, or value to search for strings.
* @returns {Array<string>} An array containing all string values found in the input.
*/
function extractStringValues(obj) {
let result = [];
if (typeof obj === "string") {
result.push(obj);
} else if (Array.isArray(obj)) {
for (const item of obj) {
result = result.concat(extractStringValues(item));
}
} else if (typeof obj === "object" && obj !== null) {
for (const key in obj) {
result = result.concat(extractStringValues(obj[key]));
}
}
return result;
}
/**
* Filters an array of objects by checking if a specified value exists within any nested property.
* The search is case-insensitive and recursively collects all string values from each object,
* then checks if the concatenated string contains the search value.
*
* @param {Array<Object>} array - The array of objects to filter.
* @param {string} nestedValue - The value to search for within the objects.
* @returns {Array<Object>} - A new array containing objects where the nested value is found.
*/
function arrayFilterNestedValue(array, nestedValue) {
return array.filter(item => {
let valuesFromObject = extractStringValues(item).join(" ")
return valuesFromObject.toLowerCase().includes(nestedValue.toLowerCase());
});
}
/**
* Updates the words frequency tables with new data.
*
* Called whenever a change in form input fields or the uploaded text file affects the word frequency table's content.
* Sorts and filters the word groups based on user preferences, and updates the HTML elements containing these tables.
*
* @function updateWordsFrequencyTables
*/
function updateWordsFrequencyTables() {
let nTotalRows = wfo["nTotalRows"]
if (nTotalRows === null || nTotalRows < 1) {
alert("let's get some data before updating the result table...")
}
let _wfo = wfo["words_frequency"]
let reduced = Object.values(_wfo)
let order = getFormDataByKey("id-form-order-by", "order")
let sort = getFormDataByKey("id-form-sort-by", "sort")
reduced.sort(dynamicSort(sort, order))
let inputFilter = document.getElementById("filter-words-frequency")
let inputFilterValue = inputFilter.value
if (inputFilterValue !== undefined && inputFilter.value !== "") {
reduced = arrayFilterNestedValue(reduced, inputFilterValue)
}
let listOfWords = document.getElementById("id-list-of-words")
listOfWords.innerHTML = ""
let currentTableOfWords = document.getElementById("id-current-table-of-words")
currentTableOfWords.innerHTML = ""
let wordsFrequencyTableTitleEl = document.getElementById("id-words-frequency-table-title")
wordsFrequencyTableTitleEl.innerText = `${wordsFrequencyTableTitleText} (${reduced.length} word groups, ${nTotalRows} rows)`
let wordsFrequencyTableTitleMobileEl = document.getElementById("id-words-frequency-table-title-mobile")
wordsFrequencyTableTitleMobileEl.innerText = `${wordsFrequencyTableTitleMobileText} (${reduced.length} word groups, ${nTotalRows} rows)`
const wordListElement = document.createElement("list")
for (let i=0; i<reduced.length; i++ ) {
insertListOfWords(i, reduced[i], wordListElement, currentTableOfWords);
}
listOfWords.append(wordListElement)
}
/**
* Populate the word frequency tables in the UI with data from the provided object or JSON string.
*
* @param {Object|string} wordsFrequencyObj - The object or JSON string containing word frequencies.
* @param {number} nTotalRows - The total number of lines/rows to display for each word group.
* @param rowArray - An array of objects representing the rows of text, each containing the text and its corresponding index.
*/
function populateWordsFrequencyTables(wordsFrequencyObj, nTotalRows, rowArray) {
wfo["words_frequency"] = wordsFrequencyObj
if (typeof wordsFrequencyObj === "string") {
wfo["words_frequency"] = JSON.parse(wordsFrequencyObj)
}
wfo["nTotalRows"] = nTotalRows
wfo["rowArray"] = Object.values(rowArray)
updateWordsFrequencyTables()
}
function getRepetitionsText(iReduced) {
return isMobile() || isMobilePortrait() ? `${iReduced["word_prefix"]}: ${iReduced["count"]} reps.` : `${iReduced["word_prefix"]}: ${iReduced["count"]} repetitions`
}
/**
* Inserts a table into the DOM displaying the frequency of word prefixes and their corresponding row nths and offsets.
*
* @param {number} i - The current index being processed (needed for adding unique HTML id/aria-labels).
* @param {Object} iReduced - An object containing the reduced data for the current index, including word prefix, count, and offsets array.
* @param {HTMLElement} currentTableOfWords - A container element to hold the current table representing chosen word positions.
*/
function insertCurrentTable(i, iReduced, currentTableOfWords) {
let currentTableWordsFreq = document.createElement("table")
currentTableWordsFreq.setAttribute("class", "border-black")
currentTableWordsFreq.setAttribute("id", `id-table-${i}-nth`)
currentTableWordsFreq.setAttribute("aria-label", `id-table-${i}-nth`)
// let currentCaption = currentTableWordsFreq.createCaption()
// currentCaption.setAttribute("aria-label", `id-table-${i}-caption`
const titleCurrentTable = document.getElementById("id-current-table-of-words-title")
titleCurrentTable.innerText = getRepetitionsText(iReduced)
let currentTBody = document.createElement("tbody")
let offsetsArray = iReduced.offsets_array
for (let ii = 0; ii < offsetsArray.length; ii++) {
insertCellIntoTRow(currentTBody, i, ii, offsetsArray[ii])
}
currentTableWordsFreq.appendChild(currentTBody)
// Wrap the table in a scrollable container
let scrollableDiv = document.createElement("div")
scrollableDiv.className = "scrollable-table-container"
scrollableDiv.appendChild(currentTableWordsFreq)
currentTableOfWords.appendChild(scrollableDiv)
}
/**
* Inserts a list of words into a word list element based on the current table of words.
* @param {number} i - The index of the current row.
* @param {Object} iReduced - An object containing information about the current word prefix and count.
* @param {HTMLElement} wordListElement - The element to insert the list of words into.
* @param {HTMLElement} currentTableOfWords - The element to insert the current table of words into.
*/
function insertListOfWords(i, iReduced, wordListElement, currentTableOfWords) {
const li = document.createElement("li");
const a = document.createElement("a")
a.innerText = getRepetitionsText(iReduced)
a.addEventListener("click", function() {
currentTableOfWords.innerHTML = ""
console.log(`insertListOfWords::'a', ${iReduced["word_prefix"]}: ${iReduced["count"]} repetitions`)
insertCurrentTable(i, iReduced, currentTableOfWords)
setElementCssClassByOldClass(underlinedClicked, underlinedPrimary)
a.className = underlinedClicked
console.log("insertListOfWords::click event:", isMobilePortrait(), "#")
if(isMobilePortrait()) {
gotoCurrentTableOfWords()
}
});
a.className = underlinedPrimary
a.setAttribute("id", `id-list-of-words-${i}-nth`)
a.setAttribute("aria-label", `id-list-of-words-${i}-nth`)
console.log(`insertListOfWords::a:`, a.id, a.ariaLabel, "=>", a, "#")
li.appendChild(a);
wordListElement.appendChild(li);
}
/**
* Inserts a new table row into the specified tbody with an associated click event listener.
*
* @param {HTMLTableSectionElement} currentTBody - The tbody element where the new row will be inserted.
* @param {number} i - A reference number for the parent's position in the DOM (needed for adding unique HTML id/aria-labels).
* @param {number} ii - A counter of how many lines/rows have been added to the table (needed for adding unique HTML id/aria-labels).
* @param {Object} nthOffset - An object containing information about a single offset word, including its row number and word text.
*/
function insertCellIntoTRow(currentTBody, i, ii, nthOffset) {
let rowArray = wfo["rowArray"]
let nthRowBody = currentTBody.insertRow()
nthRowBody.setAttribute("id", `id-table-${i}-row-${ii}-nth`)
nthRowBody.setAttribute("aria-label", `id-table-${i}-row-${ii}-nth`)
const nthRowIdx = nthOffset["n_row"]
let currentCell = nthRowBody.insertCell()
let currentUrl = document.createElement("a")
currentUrl.addEventListener("click", function() {
let nRow = nthRowIdx
let nRowChild = nthOffset["n_row_child"]
let nRowParent = nthOffset["n_row_parent"]
let offsetWord = nthOffset["offsets"]
setCaret(nRow, offsetWord, nRowChild, nRowParent)
setElementCssClassByOldClass(underlinedClickedTable, underlinedPrimaryTable)
currentUrl.className = underlinedClickedTable
})
currentUrl.className = underlinedPrimary
const wfoContainerWidth = getStylePropertyById("id-col2-words-frequency", "width", "int")
const listOfWordsWidth = getStylePropertyById("id-list-of-words", "width", "int")
const sentencesContainerWidth = wfoContainerWidth - listOfWordsWidth
let nCharsMore = Math.floor(sentencesContainerWidth / 20)
if (nCharsMore < minNCharsMore) {
nCharsMore = minNCharsMore
}
console.log(`insertCellIntoTRow::sentencesContainerWidth: ${sentencesContainerWidth}px, nCharsMore: ${nCharsMore}.`)
const {substring0, substringWord, substring2} = getSubstringForTextWithGivenOffset(rowArray, nthRowIdx, nthOffset, nCharsMore)
const span0 = document.createElement("span").innerText = substring0
const spanWord = document.createElement("span")
spanWord.setAttribute("class", "font-weight-bold")
spanWord.innerText = substringWord
const span2 = document.createElement("span").innerText = substring2
currentUrl.append(span0)
currentUrl.append(spanWord)
currentUrl.append(span2)
currentCell.appendChild(currentUrl)
}
/** Given a rowArray (array of objects) and an nthOffset (object), it returns an object containing three substrings:
* - substring0: the substring before the sliced word
* - substringWord: the word sliced from the text with the given offset from the nthOffset arg
* - substring2: the substring after the sliced word
*
* @param {Array} rowArray - The array of objects containing text and their corresponding indices.
* @param {number} nthRowIdx - The index of the row to process.
* @param {Object} nthOffset - The object containing the offsets and other properties of the word.
* @param {number} [nCharsMore=30] - The number of characters to include before and after the selected word.
*
* @returns {Object} - An object containing the substring before, the selected word, and the substring after.
* */
function getSubstringForTextWithGivenOffset(rowArray, nthRowIdx, nthOffset, nCharsMore = 30) {
try {
const currentRowArr = rowArray.filter(item => {
if (item.idxRowChild !== null) {
return item.idxRow === nthRowIdx && item.idxRowChild === nthOffset["n_row_child"] && item.idxRowParent === nthOffset["n_row_parent"]
}
return item.idxRow === nthRowIdx
})
const currentRow = currentRowArr[0]
const text = currentRow.text
let offset = nthOffset["offsets"]
let start = offset[0]
let end = offset[1]
let currentWord = nthOffset["word"]
let startOffset = Math.max(0, start - nCharsMore)
let endOffset = Math.min(text.length, end + nCharsMore)
let substringWord = text.substring(start, end)
// Prune incomplete word at the start
let substring0 = text.substring(startOffset, start)
substring0 = substring0.replace(/^\S*\s?/, '') // remove partial word at the start
// Prune incomplete word at the end
let substring2 = text.substring(end, endOffset)
substring2 = substring2.replace(/\s?\S*$/, '') // remove partial word at the end
// Rebuild substring for validation
let substring = substring0 + substringWord + substring2
if (substringWord !== currentWord || substring !== substring0 + substringWord + substring2) {
console.assert(substringWord === currentWord,
`text.substring(${start}, ${end}) !== currentWord: '${substringWord}', '${currentWord}'.`
)
console.assert(substring === substring0 + substringWord + substring2,
`## text.substring(${startOffset}, ${endOffset}) !== text.substring(${startOffset}, ${start}) + currentWord + text.substring(${end}, ${endOffset}).`
)
throw Error(`text.substring(${start}, ${end}): (${substringWord}) !== currentWord (${currentWord}).`)
}
return {substring0, substringWord, substring2};
} catch (e) {
console.error(`getSubstringForTextWithGivenOffset::error:`, e, ` #`)
throw e
}
}
/** Get the value of a CSS property for an element by its ID.
*
* @param {string} id - The ID of the element.
* @param {string} property - The CSS property to retrieve.
* @param {string} [parsing=""] - Optional parsing type ("int", "float").
*
* returns {string|number} - The value of the CSS property, parsed if specified.
* */
function getStylePropertyById(id, property, parsing="") {
const element = document.getElementById(id)
return getStylePropertyWithElement(element, property, parsing)
}
/** Get the value of a CSS property for a given element.
*
* @param {HTMLElement} element - The element to retrieve the property from.
* @param {string} property - The CSS property to retrieve.
* @param {string} [parsing=""] - Optional parsing type ("int", "float").
*
* returns {string|number} - The value of the CSS property, parsed if specified.
* */
function getStylePropertyWithElement(element, property, parsing="") {
const howToParse = {
"int": parseInt,
"float": parseFloat
}
const elementStyle = window.getComputedStyle(element)
let value = elementStyle.getPropertyValue(property)
if (howToParse[parsing] !== undefined) {
value = howToParse[parsing](value, 10)
}
return value
}
/**
* Updates the word frequency tables with new data if enter key is pressed.
* If the event target has a value (i.e., it's an input field) and the event key is "Enter",
* call the updateWordsFrequencyTables function to update the word frequency tables.
*
* @returns {void}
*/
function updateWordsFreqIfPressEnterSimple() {
if(event.key==='Enter'){
updateWordsFrequencyTables()
}
}
/**
* Retrieves valid child nodes from an editor element by ID.
* Traverses the DOM structure of the editor and collects valid text content from text nodes and SPANs.
* Handles nested SPANs and tracks their positions for later processing.
*
* @param {string} idElement - The ID of the editor element to retrieve child nodes from.
* @returns {Object} An object containing arrays of valid child nodes and their corresponding content, as well as the editor element itself.
*/
function getValidChildNodesFromEditorById(idElement) {
const editorElement = document.getElementById(idElement);
let validChildContent = [];
const validNodeNames = Object.keys(objectChildNodeNamesToParse);
// Helper: check if a node has at least one valid child node with non-empty text.
function hasValidChild(node) {
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (validNodeNames.includes(child.nodeName)) {
const prop = objectChildNodeNamesToParse[child.nodeName];
if (child[prop] && child[prop].trim() !== "") {
return true;
}
}
}
return false;
}
// Recursive helper function.
// topIdx: index of the top-level child from the editor element.
// childIdx: index within the parent node, or null if top-level.
// parentId: for nested nodes, the top-level parent's index (from a SPAN); otherwise null.
function processNode(node, topIdx, childIdx = null, parentId = null) {
// For nodes that are not SPAN, push their text if valid.
if (node.nodeName !== "SPAN" && validNodeNames.includes(node.nodeName)) {
const textField = objectChildNodeNamesToParse[node.nodeName];
const textContent = node[textField];
if (textContent && textContent.trim() !== "") {
validChildContent.push({
idxRow: topIdx,
text: textContent,
idxRowChild: childIdx,
idxRowParent: parentId
});
}
}
// For SPAN nodes, decide: if it has no valid child, then push its own text;
// otherwise, rely on its children.
if (node.nodeName === "SPAN") {
if (!hasValidChild(node)) {
const textField = objectChildNodeNamesToParse[node.nodeName];
const textContent = node[textField];
if (textContent && textContent.trim() !== "") {
validChildContent.push({
idxRow: topIdx,
text: textContent,
idxRowChild: childIdx,
idxRowParent: parentId
});
}
}
}
// Recurse into childNodes.
if (node.childNodes && node.childNodes.length > 0) {
let newParentId = parentId;
if (node.nodeName === "SPAN") {
newParentId = topIdx; // for nested nodes, use parent's top-level index.
}
for (let i = 0; i < node.childNodes.length; i++) {
processNode(node.childNodes[i], topIdx, i, newParentId);
}
}
}
// Process each top-level child node of the editor.
for (let i = 0; i < editorElement.childNodes.length; i++) {
processNode(editorElement.childNodes[i], i);
}
return { validChildContent, editorElement };
}
/** Needed by lite.koboldai.net */
function toggleElementWithClassById(idElement, className="collapse") {
let elementWithClassToChange = document.getElementById(idElement)
if (elementWithClassToChange.classList.contains(className)) {
elementWithClassToChange.classList.remove(className);
} else {
elementWithClassToChange.classList.add(className);
}
}
function addClassById(idElement, className) {
let elementWithClassToChange = document.getElementById(idElement)
elementWithClassToChange.classList.add(className);
}
function closeWordsFreqTopNav(idElement) {
addClassById(idElement, "collapse")
}
function removeClassById(idElement, className) {
let elementWithClassToChange = document.getElementById(idElement)
elementWithClassToChange.classList.remove(className);
}
function toggleOrCloseByBoolAndId(idElement, boolFlag, className="collapse") {
switch (boolFlag) {
case boolFlag === true:
toggleElementWithClassById(idElement, className)
break;
case boolFlag === false:
closeWordsFreqTopNav(idElement)
break;
default:
console.error("toggleOrCloseByBoolAndId::something is wrong: idElement => ", idElement, "#")
console.error("toggleOrCloseByBoolAndId::something is wrong: boolFlag => ", boolFlag, "#")
}
}
async function updateWordsFreqIfPressEnter() {
if (event.key === 'Enter') {
closeWordsFreqTopNav('wordsFreqNavbarNavDropdown')
const webserverIsCheckedEl = document.getElementById("id-input-webserver-wordfreq-checkbox")
const webserverIsChecked = webserverIsCheckedEl.checked
if (!webserverIsChecked) {
await getWordsFrequency()
} else {
// in case id-input-webserver-wordfreq-checkbox is checked, this will only fire updateWordsFrequencyTables()
// use instead btn4-get-words-frequency-get to fire getWordsFrequency()
updateWordsFrequencyTables()
}
}
}
function gotoCurrentTableOfWords() {
if (isMobilePortrait()) {
console.log("gotoCurrentTableOfWords::isMobilePortrait()...")
addClassById("id-current-table-of-words-btn-back", "display-block")
removeClassById("id-current-table-of-words-btn-back", "collapse")
addClassById("id-current-table-of-words-container", "display-block")
removeClassById("id-current-table-of-words-container", "collapse")
addClassById("id-list-of-words", "collapse")
removeClassById("id-list-of-words", "display-block")
}
}
function toggleWebserverCheckbox() {
const checked = document.getElementById("id-input-webserver-wordfreq-checkbox").checked
document.getElementById('id-input-webserver-wordfreq').disabled=!checked;
document.getElementById('id-wordfreq-show-analyzer').innerText=checked?'webserver':'embedded';
}
function backToListFromCurrentTable() {
if (isMobilePortrait()) {
removeClassById("id-current-table-of-words-container", "display-block")
addClassById("id-current-table-of-words-container", "collapse")
removeClassById("id-list-of-words", "collapse")
addClassById("id-list-of-words", "display-block")
}
}
function isMobilePortrait() {
console.log("isMobilePortrait::window.innerWidth:", window.innerWidth, window.screen.orientation, "#")
const orientation = window.screen.orientation
return window.innerWidth <= mobileInnerSize && (orientation.type === "portrait-primary" || orientation.type === "portrait-secondary")
}
function handleMobileWindow() {
if (isMobile()) {
closeWordsFreqTopNav("id-container-desktop-menu")
closeWordsFreqTopNav("id-container-filter-sort-order")
addClassById('id-words-frequency-table-title', "collapse");
removeClassById('id-words-frequency-table-title-mobile', "collapse");
removeClassById('id-container-mobile-menu', "collapse");
removeClassById('id-container-filter-sort-order', "display-flex");
removeClassById('id-container-filter-word-list', "width-50perc");
removeClassById('id-container-sort-order-word-list', "width-50perc");
removeClassById('id-current-table-of-words-container', "margin10px");
addClassById('id-current-table-of-words-container', "margin2px");
} else {
closeWordsFreqTopNav("id-container-mobile-menu")
// Always show desktop container on desktop
removeClassById('id-container-desktop-menu', "collapse");
removeClassById('id-container-filter-sort-order', "collapse");
addClassById('id-container-filter-sort-order', "display-flex");
addClassById('id-words-frequency-table-title-mobile', "collapse");
addClassById('id-container-filter-word-list', "width-50perc");
removeClassById('id-words-frequency-table-title', "collapse");
addClassById('id-current-table-of-words-container', "margin10px");
removeClassById('id-current-table-of-words-container', "margin2px");
}
}
function isMobile() {
return window.innerWidth <= mobileInnerSize || window.innerHeight <= mobileInnerSize;
}
window.addEventListener('resize', handleMobileWindow);
window.addEventListener('DOMContentLoaded', handleMobileWindow);
console.log('DOMContentLoaded'); |