diff --git a/README.md b/README.md index 1687db7..26a926d 100644 --- a/README.md +++ b/README.md @@ -224,3 +224,31 @@ jekyllTabs.init({ ``` Default styles for the toast message are present in the [css file](https://github.com/Ovski4/jekyll-tabs/blob/master/docs/tabs.css#L50-L70). + +Development +----------- + +### Building the script + +Execute: + +```bash +npm run build +``` + +The add the following content to the `tabs.js` file. + +```js +window.addEventListener('load', function () { + jekyllTabs.init(); +}); +``` + +### Building and pushing the gem + +Update the version number in `jekyll-tabs/version.rb`, then execute: + +```bash +gem build jekyll-tabs.gemspec +gem push jekyll-tabs-{version_here}.gem +``` diff --git a/docs/tabs.js b/docs/tabs.js index f525ed4..86b51e5 100644 --- a/docs/tabs.js +++ b/docs/tabs.js @@ -1,4 +1,4 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.jekyllTabs=e():t.jekyllTabs=e()}(self,(()=>{return t={918:t=>{t.exports={getChildPosition:t=>{const e=t.parentNode;for(let o=0;o{const o=document.querySelectorAll(t),a=[];for(let t=0;t{const e=document.createElement("template");return e.innerHTML=t.trim(),e.content.firstChild},addClass:(t,e,o)=>{t.className=t.className?`${t.className} ${e}`:e,setTimeout((()=>{t.className=t.className.replace(e,"").trim()}),o)}}},613:(t,e,o)=>{const{activateTabFromUrl:a,updateUrlWithActiveTab:n,handleTabClicked:s,addCopyToClipboardButtons:r,syncTabsWithSameLabels:l,appendToastMessageHTML:i}=o(925);t.exports={init:(t={})=>{const e={syncTabsWithSameLabels:!1,activateTabFromUrl:!1,addCopyToClipboardButtons:!1,copyToClipboardSettings:{buttonHTML:"",showToastMessageOnCopy:!1,toastMessage:"Code copied to clipboard",toastDuration:3e3}},o={...e,...t,copyToClipboardSettings:{...e.copyToClipboardSettings,...t.copyToClipboardSettings}},c=document.querySelectorAll("ul.tab > li > a");if(Array.prototype.forEach.call(c,(t=>{t.addEventListener("click",(e=>{e.preventDefault(),s(t),o.activateTabFromUrl&&n(t),o.syncTabsWithSameLabels&&l(t)}),!1)})),o.addCopyToClipboardButtons){const t=o.copyToClipboardSettings;r(t),t.showToastMessageOnCopy&&i(t.toastMessage)}o.activateTabFromUrl&&a()}}},925:(t,e,o)=>{const{getChildPosition:a,createElementFromHTML:n,findElementsWithTextContent:s,addClass:r}=o(918),l=t=>{const e=t.querySelectorAll("ul > li");Array.prototype.forEach.call(e,(t=>{t.classList.remove("active")}))},i=t=>{const e=t.parentNode,o=e.parentNode,n=a(e);if(e.className.includes("active"))return;const s=o.getAttribute("data-tab");if(!s)return;const r=document.getElementById(s);l(o),l(r),r.querySelectorAll("ul.tab-content > li")[n].classList.add("active"),e.classList.add("active")},c=(t,e)=>{if(navigator.clipboard&&window.isSecureContext)navigator.clipboard.writeText(t);else{const e=document.createElement("textarea");e.value=t,e.style.position="absolute",e.style.left="-999999px",document.body.prepend(e),e.select();try{document.execCommand("copy")}catch(t){console.error(t)}finally{e.remove()}}"function"==typeof e&&e()},d=t=>{r(document.getElementById("jekyll-tabs-copy-to-clipboard-message"),"show",t)};t.exports={removeActiveClasses:l,handleTabClicked:i,copyToClipboard:c,addCopyToClipboardButtons:({buttonHTML:t,showToastMessageOnCopy:e,toastDuration:o})=>{const a=document.querySelectorAll("ul.tab-content > li pre");for(let s=0;s{d(o)}),i.addEventListener("click",(()=>{c(r.innerText,p)}))}},activateTabFromUrl:()=>{const t=window.location.hash?.substring(1);if(!t)return;const e=document.getElementById(t);if(!e)return;const o=new URLSearchParams(window.location.search).get("active_tab");if(!o)return;const a=e.querySelector("li#"+o+" > a");a&&i(a)},updateUrlWithActiveTab:t=>{const e=t.parentNode,o=e.parentNode,a=new URLSearchParams(window.location.search);a.set("active_tab",e.id);const n=window.location.pathname+"?"+a.toString()+"#"+o.id;history.replaceState(null,"",n)},syncTabsWithSameLabels:t=>{const e=s("a",t.textContent);for(let o=0;o{const e=document.createElement("div");e.id="jekyll-tabs-copy-to-clipboard-message",e.textContent=t,document.getElementsByTagName("body")[0].appendChild(e)}}}},e={},function o(a){var n=e[a];if(void 0!==n)return n.exports;var s=e[a]={exports:{}};return t[a](s,s.exports,o),s.exports}(613);var t,e})); +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.jekyllTabs=t():e.jekyllTabs=t()}(self,(()=>(()=>{"use strict";var e={973:(e,t,o)=>{o.r(t),o.d(t,{addClass:()=>r,createElementFromHTML:()=>s,findElementsWithTextContent:()=>n,getChildPosition:()=>a});const a=e=>{const t=e.parentNode;for(let o=0;o{const o=document.querySelectorAll(e),a=[];for(let e=0;e{const t=document.createElement("template");return t.innerHTML=e.trim(),t.content.firstChild},r=(e,t,o)=>{e.className=e.className?`${e.className} ${t}`:t,setTimeout((()=>{e.className=e.className.replace(t,"").trim()}),o)}},39:(e,t,o)=>{o.r(t),o.d(t,{activateTabFromUrl:()=>d,addCopyToClipboardButtons:()=>u,appendToastMessageHTML:()=>b,copyToClipboard:()=>c,handleTabClicked:()=>i,removeActiveClasses:()=>l,syncTabsWithSameLabels:()=>y,updateUrlWithActiveTab:()=>p});const{getChildPosition:a,createElementFromHTML:n,findElementsWithTextContent:s,addClass:r}=o(973),l=e=>{const t=e.querySelectorAll("ul > li");Array.prototype.forEach.call(t,(e=>{e.classList.remove("active")}))},i=e=>{const t=e.parentNode,o=t.parentNode,n=a(t);if(t.className.includes("active"))return;const s=o.getAttribute("data-tab");if(!s)return;const r=document.getElementById(s);l(o),l(r),r.querySelectorAll("ul.tab-content > li")[n].classList.add("active"),t.classList.add("active")},c=(e,t)=>{if(navigator.clipboard&&window.isSecureContext)navigator.clipboard.writeText(e);else{const t=document.createElement("textarea");t.value=e,t.style.position="absolute",t.style.left="-999999px",document.body.prepend(t),t.select();try{document.execCommand("copy")}catch(e){console.error(e)}finally{t.remove()}}"function"==typeof t&&t()},d=()=>{var e;const t=null===(e=window.location.hash)||void 0===e?void 0:e.substring(1);if(!t)return;const o=document.getElementById(t);if(!o)return;const a=new URLSearchParams(window.location.search).get("active_tab");if(!a)return;const n=o.querySelector("li#"+a+" > a");n&&i(n)},p=e=>{const t=e.parentNode,o=t.parentNode,a=new URLSearchParams(window.location.search);a.set("active_tab",t.id);const n=window.location.pathname+"?"+a.toString()+"#"+o.id;history.replaceState(null,"",n)},u=({buttonHTML:e,showToastMessageOnCopy:t,toastDuration:o})=>{const a=document.querySelectorAll("ul.tab-content > li pre");for(let s=0;s{m(o)}),i.addEventListener("click",(()=>{c(r.innerText,d)}))}},b=e=>{const t=document.createElement("div");t.id="jekyll-tabs-copy-to-clipboard-message",t.textContent=e,document.getElementsByTagName("body")[0].appendChild(t)},m=e=>{r(document.getElementById("jekyll-tabs-copy-to-clipboard-message"),"show",e)},y=e=>{const t=s("a",e.textContent);for(let o=0;o{for(var a in t)o.o(t,a)&&!o.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var a={};return(()=>{o.r(a),o.d(a,{init:()=>i});const{activateTabFromUrl:e,updateUrlWithActiveTab:t,handleTabClicked:n,addCopyToClipboardButtons:s,syncTabsWithSameLabels:r,appendToastMessageHTML:l}=o(39),i=(o={})=>{const a={syncTabsWithSameLabels:!1,activateTabFromUrl:!1,addCopyToClipboardButtons:!1,copyToClipboardSettings:{buttonHTML:"",showToastMessageOnCopy:!1,toastMessage:"Code copied to clipboard",toastDuration:3e3}},i=Object.assign(Object.assign(Object.assign({},a),o),{copyToClipboardSettings:Object.assign(Object.assign({},a.copyToClipboardSettings),o.copyToClipboardSettings)}),c=document.querySelectorAll("ul.tab > li > a");if(Array.prototype.forEach.call(c,(e=>{e.addEventListener("click",(o=>{o.preventDefault(),n(e),i.activateTabFromUrl&&t(e),i.syncTabsWithSameLabels&&r(e)}),!1)})),i.addCopyToClipboardButtons){const e=i.copyToClipboardSettings;s(e),e.showToastMessageOnCopy&&l(e.toastMessage)}i.activateTabFromUrl&&e()}})(),a})())); window.addEventListener('load', function () { jekyllTabs.init(); diff --git a/jest.config.js b/jest.config.js index 53f9031..ee29da3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('jest').Config} */ const config = { + preset: 'ts-jest', verbose: true, testEnvironment: 'jsdom', collectCoverage: true, diff --git a/js/domHelpers.js b/js/domHelpers.ts similarity index 85% rename from js/domHelpers.js rename to js/domHelpers.ts index f219b57..4771374 100644 --- a/js/domHelpers.js +++ b/js/domHelpers.ts @@ -11,7 +11,7 @@ * * Then getChildPosition(document.querySelector('.one')) would return 1. */ -const getChildPosition = (element) => { +const getChildPosition = (element: HTMLElement) => { const parent = element.parentNode; for (let i = 0; i < parent.children.length; i++) { @@ -24,7 +24,7 @@ const getChildPosition = (element) => { /** * Returns a list of elements that match the given selector and text content. */ -const findElementsWithTextContent = (selector, text) => { +const findElementsWithTextContent = (selector: string, text: string) => { const elementsMatchingSelector = document.querySelectorAll(selector); const elementsWithTextContent = []; @@ -42,7 +42,7 @@ const findElementsWithTextContent = (selector, text) => { /** * Create a javascript element from HTML markup. */ -const createElementFromHTML = (html) => { +const createElementFromHTML = (html: string) => { const template = document.createElement('template'); template.innerHTML = html.trim(); @@ -52,7 +52,7 @@ const createElementFromHTML = (html) => { /** * Add the class on the given element for the duration of the timeout. */ -const addClass = (element, addedClass, timeout) => { +const addClass = (element: HTMLElement, addedClass: string, timeout: number) => { element.className = element.className ? `${element.className} ${addedClass}` : addedClass; @@ -63,7 +63,7 @@ const addClass = (element, addedClass, timeout) => { }, timeout); } -module.exports = { +export { getChildPosition, findElementsWithTextContent, createElementFromHTML, diff --git a/js/jekyllTabs.js b/js/jekyllTabs.ts similarity index 67% rename from js/jekyllTabs.js rename to js/jekyllTabs.ts index 90caab3..39dffb2 100644 --- a/js/jekyllTabs.js +++ b/js/jekyllTabs.ts @@ -7,8 +7,23 @@ const { appendToastMessageHTML, } = require('./tabsHelpers'); -const init = (overriddenConfiguration = {}) => { - const defaultConfiguration = { +interface CopyToClipboardSettings { + buttonHTML: string, + showToastMessageOnCopy: boolean, + toastMessage: string, + toastDuration: number, +} + +interface Configuration { + syncTabsWithSameLabels: boolean; + activateTabFromUrl: boolean; + addCopyToClipboardButtons: boolean; + copyToClipboardSettings: CopyToClipboardSettings; +} + +const init = (overriddenConfiguration: any = {}) => { + + const defaultConfiguration: Configuration = { syncTabsWithSameLabels: false, activateTabFromUrl: false, addCopyToClipboardButtons: false, @@ -20,7 +35,7 @@ const init = (overriddenConfiguration = {}) => { } }; - const configuration = { + const configuration: Configuration = { ...defaultConfiguration, ...overriddenConfiguration, copyToClipboardSettings: { @@ -29,10 +44,10 @@ const init = (overriddenConfiguration = {}) => { } }; - const tabLinks = document.querySelectorAll('ul.tab > li > a'); + const tabLinks: NodeList = document.querySelectorAll('ul.tab > li > a'); - Array.prototype.forEach.call(tabLinks, (link) => { - link.addEventListener('click', (event) => { + Array.prototype.forEach.call(tabLinks, (link: HTMLAnchorElement) => { + link.addEventListener('click', (event: MouseEvent) => { event.preventDefault(); handleTabClicked(link); @@ -62,6 +77,6 @@ const init = (overriddenConfiguration = {}) => { } }; -module.exports = { +export { init, } diff --git a/js/tabsHelpers.js b/js/tabsHelpers.ts similarity index 81% rename from js/tabsHelpers.js rename to js/tabsHelpers.ts index 65ad3ab..1d21d58 100644 --- a/js/tabsHelpers.js +++ b/js/tabsHelpers.ts @@ -8,10 +8,10 @@ const { /** * Remove all "active" classes on li elements that belong to the given ul element. */ -const removeActiveClasses = (ulElement) => { +const removeActiveClasses = (ulElement: HTMLUListElement) => { const liElements = ulElement.querySelectorAll('ul > li'); - Array.prototype.forEach.call(liElements, (liElement) => { + Array.prototype.forEach.call(liElements, (liElement: HTMLLIElement) => { liElement.classList.remove('active'); }); } @@ -19,9 +19,9 @@ const removeActiveClasses = (ulElement) => { /** * Handle adding or removing active classes on tab list items. */ -const handleTabClicked = (link) => { - const liTab = link.parentNode; - const ulTab = liTab.parentNode; +const handleTabClicked = (link: HTMLAnchorElement) => { + const liTab = link.parentNode as HTMLLIElement; + const ulTab = liTab.parentNode as HTMLUListElement; const liPositionInUl = getChildPosition(liTab); if (liTab.className.includes('active')) { @@ -34,7 +34,7 @@ const handleTabClicked = (link) => { return; } - const tabContentElement = document.getElementById(tabContentId); + const tabContentElement = document.getElementById(tabContentId) as HTMLUListElement; // Remove all "active" classes first. removeActiveClasses(ulTab); @@ -50,7 +50,7 @@ const handleTabClicked = (link) => { * * See https://stackoverflow.com/questions/51805395/navigator-clipboard-is-undefined */ -const copyToClipboard = (text, callBack) => { +const copyToClipboard = (text: string, callBack: Function) => { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text); } else { @@ -105,7 +105,7 @@ const activateTabFromUrl = () => { return; } - const tabLink = targetedTabs.querySelector('li#' + tabIdToActivate + ' > a'); + const tabLink = targetedTabs.querySelector('li#' + tabIdToActivate + ' > a') as HTMLAnchorElement; if (!tabLink) { return; @@ -117,9 +117,9 @@ const activateTabFromUrl = () => { /** * Update the url when clicking on a tab. See method activateTabFromUrl above. */ -const updateUrlWithActiveTab = (link) => { - const liTab = link.parentNode; - const ulTab = liTab.parentNode; +const updateUrlWithActiveTab = (link: HTMLAnchorElement) => { + const liTab = link.parentNode as HTMLLIElement; + const ulTab = liTab.parentNode as HTMLUListElement; const searchParams = new URLSearchParams(window.location.search); searchParams.set('active_tab', liTab.id); @@ -131,12 +131,15 @@ const updateUrlWithActiveTab = (link) => { /** * Add the "Copy to clipboard" button on the top right hand side of tabs with embedded code (
 tags).
  */
-const addCopyToClipboardButtons = ({ buttonHTML, showToastMessageOnCopy, toastDuration }) => {
-    const preElements = document.querySelectorAll('ul.tab-content > li pre');
+const addCopyToClipboardButtons = (
+    { buttonHTML, showToastMessageOnCopy, toastDuration }:
+    { buttonHTML: string, showToastMessageOnCopy: boolean, toastDuration: number }
+) => {
+    const preElements = document.querySelectorAll('ul.tab-content > li pre') as NodeListOf;
 
     for(let i = 0; i < preElements.length; i++) {
         const preElement = preElements[i];
-        const preParentNode = preElement.parentNode;
+        const preParentNode = preElement.parentNode as HTMLElement;
         const button = createElementFromHTML(buttonHTML);
 
         preParentNode.style.position = 'relative';
@@ -146,7 +149,7 @@ const addCopyToClipboardButtons = ({ buttonHTML, showToastMessageOnCopy, toastDu
 
         preParentNode.appendChild(button);
 
-        let copyToClipboardCallBack;
+        let copyToClipboardCallBack: Function;
 
         if (showToastMessageOnCopy) {
             copyToClipboardCallBack = () => {
@@ -163,7 +166,7 @@ const addCopyToClipboardButtons = ({ buttonHTML, showToastMessageOnCopy, toastDu
 /**
  * Insert a div that contains the toast message at the end of the  tag.
  */
-const appendToastMessageHTML = (toastMessage) => {
+const appendToastMessageHTML = (toastMessage: string) => {
     const toastMessageDiv = document.createElement('div');
 
     toastMessageDiv.id = 'jekyll-tabs-copy-to-clipboard-message';
@@ -175,14 +178,14 @@ const appendToastMessageHTML = (toastMessage) => {
 /**
  * Set '.show' class on the div that contains the toast message for the given duration.
  */
-const showToastMessage = (toastDuration) => {
+const showToastMessage = (toastDuration: number) => {
     addClass(document.getElementById('jekyll-tabs-copy-to-clipboard-message'), 'show', toastDuration);
 }
 
 /**
  * Activate tabs that have the same label as the one related to the given link.
  */
-const syncTabsWithSameLabels = (link) => {
+const syncTabsWithSameLabels = (link: HTMLAnchorElement) => {
     const linksWithSameName = findElementsWithTextContent('a', link.textContent);
 
     for(let i = 0; i < linksWithSameName.length; i++) {
@@ -192,7 +195,7 @@ const syncTabsWithSameLabels = (link) => {
     }
 }
 
-module.exports = {
+export {
     removeActiveClasses,
     handleTabClicked,
     copyToClipboard,
diff --git a/package-lock.json b/package-lock.json
index 2613b61..bb7144f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1370,6 +1370,15 @@
         "update-browserslist-db": "^1.0.13"
       }
     },
+    "bs-logger": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
+      "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
+      "dev": true,
+      "requires": {
+        "fast-json-stable-stringify": "2.x"
+      }
+    },
     "bser": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@@ -2853,6 +2862,12 @@
         "p-locate": "^4.1.0"
       }
     },
+    "lodash.memoize": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+      "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
+      "dev": true
+    },
     "lru-cache": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -2897,6 +2912,12 @@
         }
       }
     },
+    "make-error": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+      "dev": true
+    },
     "makeerror": {
       "version": "1.0.12",
       "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -3519,6 +3540,93 @@
         "punycode": "^2.1.1"
       }
     },
+    "ts-jest": {
+      "version": "29.1.2",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz",
+      "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==",
+      "dev": true,
+      "requires": {
+        "bs-logger": "0.x",
+        "fast-json-stable-stringify": "2.x",
+        "jest-util": "^29.0.0",
+        "json5": "^2.2.3",
+        "lodash.memoize": "4.x",
+        "make-error": "1.x",
+        "semver": "^7.5.3",
+        "yargs-parser": "^21.0.1"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "semver": {
+          "version": "7.5.4",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+          "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+          "dev": true
+        }
+      }
+    },
+    "ts-loader": {
+      "version": "9.5.1",
+      "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
+      "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^4.1.0",
+        "enhanced-resolve": "^5.0.0",
+        "micromatch": "^4.0.0",
+        "semver": "^7.3.4",
+        "source-map": "^0.7.4"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "semver": {
+          "version": "7.5.4",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+          "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "source-map": {
+          "version": "0.7.4",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+          "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+          "dev": true
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+          "dev": true
+        }
+      }
+    },
     "type-detect": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
@@ -3531,6 +3639,12 @@
       "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
       "dev": true
     },
+    "typescript": {
+      "version": "5.3.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
+      "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+      "dev": true
+    },
     "undici-types": {
       "version": "5.26.5",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
diff --git a/package.json b/package.json
index 7b14e94..e116151 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,9 @@
     "devDependencies": {
         "jest": "^29.7.0",
         "jest-environment-jsdom": "^29.7.0",
+        "ts-jest": "^29.1.2",
+        "ts-loader": "^9.5.1",
+        "typescript": "^5.3.3",
         "webpack": "^5.89.0",
         "webpack-cli": "^5.1.4"
     }
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..7288f7f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,11 @@
+{
+    "compilerOptions": {
+        "esModuleInterop": true,
+        "outDir": "./docs/",
+        "noImplicitAny": true,
+        "module": "es6",
+        "target": "es6",
+        "allowJs": true,
+        "moduleResolution": "node"
+    }
+}
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
index 7736cbb..73baef6 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,7 +1,19 @@
 const path = require('path');
 
 module.exports = {
-    entry: './js/jekyllTabs.js',
+    entry: './js/jekyllTabs.ts',
+    module: {
+        rules: [
+            {
+                test: /\.ts$/,
+                use: 'ts-loader',
+                exclude: /node_modules/,
+            },
+        ],
+    },
+    resolve: {
+        extensions: ['.ts'],
+    },
     output: {
         filename: 'tabs.js',
         path: path.resolve(__dirname, 'docs'),