From f9f81ae8f90a5b8f9b41da10b7e3dc41500b0eb9 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Fri, 20 Dec 2024 17:14:19 -0600 Subject: [PATCH 01/14] Initial Kapa.ai chat integration. Continue refactoring JavaScript into a component pattern and ESM. Replaces some jQuery with native DOM API. chore(ai): reference documentation and instructions for training AI chore(ai): implement Kapa AI chat widget - Move script tag to HTML template to make it obvious. - Cleanup javascript to make it more component-like - Set Kapa attributes, support setting userid chore(js): add JS dependencies, previously referenced in script tags, to package.json for JS builds. fix(api): indents chore(js): package Mermaid diagram library chore(js): refactor JS for AIChat and Theme as examples of using the component pattern for HTML/CSS/JS chore(js): Use the new local-storage API in refactored module code and in code not yet ported. Cleanup syntax in local-storage and make functions available from window.LocalStorageAPI. fix(js): theme.js name-change chore(js): fix ai-chat.js file name fix(js): refactor: - componentNames are snakecase in HTML - replace DOM selection method and jQuery eventhandler assignment - remove old theme.js references chore(ai): configure chat window overlay, size, and position: - removes overlay and scroll lock - positions chat to the right and bottom - expands sample question width to 12 cols chore(ai): edit disclaimer fix(ai): size and position chore(js): make ai-chat specific to configuration and and setting userid (for testing and future use). fix(js): copy referrerHost variable to v3-wayfinding instead of relying on influxdb-url to assign it. chore(ai): add a footer div at page bottom to contain modal triggers for custom-time and ask-ai. Still needs some CSS help. Moves tooltip text from CSS to HTML data attribute. chore(ai): dynamically load AI script tag after DOMContentLoaded to avoid race conditions. Call initialization from the modal trigger module and pass the show trigger function to the onload handler. fix(ai): fix modal triggers to viewport fix(modal-triggers): stack the triggers into a single column. restyle footer widgets updated time selector modal to use correct storage term minor style update WIP(ai-chat): get product data chore(js): Factor out pageContext module from influxdb-url.js chore(js): Refactor helpers.js out of inflluxdb-url.js WIP: refactor influxdburl - minimal changes for module conversions feat(ai): Custom AI chat example questions product and version. Ask AI example questions: - Adds support for customizing example Ask AI questions per product or version. - Configure questions in site `data/products.yml`; otherwise, it uses default questions from `ask-ai.js` Context, page, and product data: - Adds sample URLs for remaining versions in influxdb_urls - `page-context.js` consolidates and exports constants for page context (protocol, host, path, referrer) and path-to-data mappings for product and influxdb_url site data Module refactor: - Refactors some JavaScript into ES6 modules, and refactors some of those further into a Component pattern--just vanilla JS and no shadow DOM stuff. The Component pattern that uses data attributes to "bind" JavaScript modules with CSS and HTML is a popular approach in modern web development. This pattern enhances modularity, reusability, and maintainability by associating behavior (JavaScript), structure (HTML), and style (CSS) through the use of data attributes. - `assets/main.js` is the entrypoint - Passes pageParams from the Hugo page to modules that import `@params`. - Moves most external dependencies out of `script` tags and into package.json to be managed with `yarn`. - Adds `eslint`. - For modules that aren't yet components, wraps execution statements inside an `initialize()` function and calls the function from `main.js` on `DOMContentLoaded`. - For components, if the page contains the `data-component=""`, the matching element is passed to the component function on `DOMContentLoaded`. - I tried to avoid changing logic where it wasn't necessary. Update DOC_GPT_PROFILE.md customize ai chat modal styles fix(influxdb-url): Rename to cloud_dedicated in influxdb_urls.yml, remove newly added placeholder URL and use the extant default, refactor - Rename to in influxdb_urls.yml - Fix influxdb-url.js and data provision in local-storage.js to use the new name, mapping it to to retain the existing local storage key chore(api-lib): Use local-storage import instead of window global chore(js): cleanup fix(js): Ensure feature-callout initializes on page load fix(theme): Load preferred theme before making the page visible. Execute a predefined function by specifying the function name in data-theme-callback fix(search-toggle): Restores toggling the search field when sidebar is collapsed. Moves the event handler to a new search-button component fix(ai): Fix custom attribute assignment. Rename property to ai_example_questions Include the word `Bearer` or `Token`, a space, and your **token** value (all case-sensitive). Fix TOC links. Fixes #5781 fix(api-docs): Update API reference directories and generation script for influxdb3 URL paths, update links and names in reference content fix(api-ref): Update getswagger.sh destination paths to use the new directory structure when fetching spec files. Update the redocly plugin module path. hotfix: fix hlevel bug in children shortcode Remove underline from custom time widget add color to custom time widget styling --- DOC_GPT_PROFILE.md | 52 + PLATFORM_REFERENCE.md | 30 + .../influxdb3/cloud-serverless/v2/ref.yml | 16 +- assets/js/api-libs.js | 34 +- assets/js/ask-ai-trigger.js | 19 + assets/js/ask-ai.js | 100 + assets/js/code-controls.js | 161 +- assets/js/content-interactions.js | 260 +- assets/js/custom-timestamps.js | 237 +- assets/js/docs-themes.js | 42 - assets/js/feature-callouts.js | 20 +- assets/js/helpers.js | 13 + assets/js/influxdb-url.js | 1213 +++++----- assets/js/local-storage.js | 135 +- assets/js/main.js | 141 ++ assets/js/modals.js | 53 +- assets/js/notifications.js | 64 +- assets/js/page-context.js | 88 + assets/js/page-feedback.js | 103 +- assets/js/search-button.js | 10 + assets/js/sidebar-toggle.js | 26 +- assets/js/tabbed-content.js | 67 +- assets/js/theme-switch.js | 20 + assets/js/theme.js | 42 + assets/js/v3-wayfinding.js | 179 +- assets/jsconfig.json | 10 + .../styles/layouts/_custom-time-trigger.scss | 65 - assets/styles/layouts/_footer-widgets.scss | 139 ++ assets/styles/layouts/_top-nav.scss | 6 +- assets/styles/styles-default.scss | 2 +- content/shared/v3-core-get-started/_index.md | 39 +- .../v3-enterprise-get-started/_index.md | 70 +- data/influxdb_urls.yml | 17 + data/products.yml | 35 + eslint.config.mjs | 9 + layouts/404.html | 5 +- layouts/partials/footer.html | 4 +- .../partials/footer/custom-time-trigger.html | 5 - layouts/partials/footer/javascript.html | 26 +- layouts/partials/footer/modals.html | 2 +- .../modals/influxdb-gs-date-select.html | 7 +- layouts/partials/footer/widgets.html | 6 + .../footer/widgets/ask-ai-trigger.html | 6 + .../footer/widgets/custom-time-trigger.html | 7 + layouts/partials/header.html | 4 +- layouts/partials/header/javascript.html | 45 +- layouts/partials/header/stylesheets.html | 7 + layouts/partials/sidebar/sidebar-toggle.html | 2 +- layouts/partials/topnav.html | 16 +- package.json | 12 +- static/img/influx-logo-cubo-white.png | Bin 0 -> 3514 bytes yarn.lock | 2101 ++++++++++++++--- 52 files changed, 4097 insertions(+), 1675 deletions(-) create mode 100644 DOC_GPT_PROFILE.md create mode 100644 PLATFORM_REFERENCE.md create mode 100644 assets/js/ask-ai-trigger.js create mode 100644 assets/js/ask-ai.js delete mode 100644 assets/js/docs-themes.js create mode 100644 assets/js/helpers.js create mode 100644 assets/js/main.js create mode 100644 assets/js/page-context.js create mode 100644 assets/js/search-button.js create mode 100644 assets/js/theme-switch.js create mode 100644 assets/js/theme.js create mode 100644 assets/jsconfig.json delete mode 100644 assets/styles/layouts/_custom-time-trigger.scss create mode 100644 assets/styles/layouts/_footer-widgets.scss create mode 100644 eslint.config.mjs delete mode 100644 layouts/partials/footer/custom-time-trigger.html create mode 100644 layouts/partials/footer/widgets.html create mode 100644 layouts/partials/footer/widgets/ask-ai-trigger.html create mode 100644 layouts/partials/footer/widgets/custom-time-trigger.html create mode 100644 static/img/influx-logo-cubo-white.png diff --git a/DOC_GPT_PROFILE.md b/DOC_GPT_PROFILE.md new file mode 100644 index 000000000..fd5c16c44 --- /dev/null +++ b/DOC_GPT_PROFILE.md @@ -0,0 +1,52 @@ +Doc is a public custom GPT for OpenAI ChatGPT used to help write and style InfluxData and InfluxDB documentation. + +## Introduction + +Doc writes technical software documentation for InfluxData. The public web site is https://docs.influxdata.com and the source repository is https://github.com/influxdata/docs-v2. +Documentation provides step-by-step guides and reference documentation for InfluxDB and associated clients (CLIs, client libraries (SDKs), and Telegraf (https://docs.influxdata.com/telegraf/v1/)), and the legacy v1 components Kapacitor and Chronograf. + +## Instruction + +When a user asks a question and doesn't include a product from the list below, ask them which product in the list they are using, along with the version and query language: + +InfluxDB OSS 1.x (v1) + - Documentation: https://docs.influxdata.com/influxdb/v1/ + - Query languages: v1.8+ supports InfluxQL and Flux + - Clients: Telegraf, influx CLI, v1 client libraries +InfluxDB Enterprise (v1) + - Documentation: https://docs.influxdata.com/enterprise_influxdb/v1/ + - Query languages: v1.8+ supports InfluxQL and Flux + - Clients: Telegraf, influx CLI, v1 client libraries +InfluxDB OSS 2.x (v2) + - Documentation: https://docs.influxdata.com/influxdb/v2/ + - Query languages: InfluxQL and Flux + - Clients: Telegraf, influx CLI, v2 client libraries +InfluxDB Cloud (v2, multi-tenant) + - Documentation: https://docs.influxdata.com/influxdb/cloud/ + - Query languages: InfluxQL and Flux + - Clients: Telegraf, influx CLI, v2 client libraries +InfluxDB Clustered (v3, 3.0, self-managed distributed) + - Documentation: https://docs.influxdata.com/influxdb/clustered/ + - Query languages: SQL and InfluxQL + - Clients: Telegraf, influxctl CLI, v3 client libraries +InfluxDB Cloud Dedicated (3.0, v3, InfluxData-managed single tenant) + - Documentation: https://docs.influxdata.com/influxdb/cloud-dedicated/ + - Query languages: SQL and InfluxQL + - Clients: Telegraf, influxctl CLI, v3 client libraries +InfluxDB Cloud Serverless (v3, 3.0, InfluxData-managed multi-tenant) + - Documentation: https://docs.influxdata.com/influxdb/clustered/ + - Query languages: SQL and InfluxQL + - Clients: Telegraf, influx CLI, v3 client libraries + +If I ask about a REST API or SDK (client library) and don't specify a product, ask which product. +For API client libraries, refer to the documentation and to the source repositories in https://github.com/InfluxCommunity for the version-specific client library. + +When writing documentation, always use Google Developer Documentation style guidelines and Markdown format. +If writing REST API reference documentation follow YouTube Data API style and Google Developer Documentation style guidelines. + +The project uses the Hugo static site generator to build the documentation. +The site uses JavaScript and jQuery. +For information about linting, tests (using pytests for codeblocks), shortcode , refer to https://github.com/influxdata/docs-v2/blob/master/README.md and https://github.com/influxdata/docs-v2/blob/master/CONTRIBUTING.md. +If something in CONTRIBUTING.md needs clarification, then give me the suggested revision for CONTRIBUTING.md in Markdown. + +The community forum is https://community.influxdata.com/ and should not be used as a primary source of information, but might contain useful suggestions or solutions to specific problems from users. diff --git a/PLATFORM_REFERENCE.md b/PLATFORM_REFERENCE.md new file mode 100644 index 000000000..bc4f1f9ca --- /dev/null +++ b/PLATFORM_REFERENCE.md @@ -0,0 +1,30 @@ +When a user asks a question and doesn't include a product from the list below, ask them which product in the list they are using, along with the version and query language: + +InfluxDB OSS 1.x (v1) + - Documentation: https://docs.influxdata.com/influxdb/v1/ + - Query languages: v1.8+ supports InfluxQL and Flux + - Clients: Telegraf, influx CLI, v1 client libraries +InfluxDB Enterprise (v1) + - Documentation: https://docs.influxdata.com/enterprise_influxdb/v1/ + - Query languages: v1.8+ supports InfluxQL and Flux + - Clients: Telegraf, influx CLI, v1 client libraries +InfluxDB OSS 2.x (v2) + - Documentation: https://docs.influxdata.com/influxdb/v2/ + - Query languages: InfluxQL and Flux + - Clients: Telegraf, influx CLI, v2 client libraries +InfluxDB Cloud (v2, multi-tenant) + - Documentation: https://docs.influxdata.com/influxdb/cloud/ + - Query languages: InfluxQL and Flux + - Clients: Telegraf, influx CLI, v2 client libraries +InfluxDB Clustered (v3, 3.0, self-managed distributed) + - Documentation: https://docs.influxdata.com/influxdb/clustered/ + - Query languages: SQL and InfluxQL + - Clients: Telegraf, influxctl CLI, v3 client libraries +InfluxDB Cloud Dedicated (3.0, v3, InfluxData-managed single tenant) + - Documentation: https://docs.influxdata.com/influxdb/cloud-dedicated/ + - Query languages: SQL and InfluxQL + - Clients: Telegraf, influxctl CLI, v3 client libraries +InfluxDB Cloud Serverless (v3, 3.0, InfluxData-managed multi-tenant) + - Documentation: https://docs.influxdata.com/influxdb/clustered/ + - Query languages: SQL and InfluxQL + - Clients: Telegraf, influx CLI, v3 client libraries diff --git a/api-docs/influxdb3/cloud-serverless/v2/ref.yml b/api-docs/influxdb3/cloud-serverless/v2/ref.yml index 530c06856..78e7ff814 100644 --- a/api-docs/influxdb3/cloud-serverless/v2/ref.yml +++ b/api-docs/influxdb3/cloud-serverless/v2/ref.yml @@ -10280,9 +10280,9 @@ components: properties: results: description: | - A resultset object that contains the `statement_id` and the `series` array. + A resultset object that contains the `statement_id` and the `series` array. - Except for `statement_id`, all properties are optional and omitted if empty. If a property is not present, it is assumed to be `null`. + Except for `statement_id`, all properties are optional and omitted if empty. If a property is not present, it is assumed to be `null`. items: properties: error: @@ -10331,12 +10331,12 @@ components: type: integer type: object oneOf: - - required: - - statement_id - - error - - required: - - statement_id - - series + - required: + - statement_id + - error + - required: + - statement_id + - series type: array type: object IntegerLiteral: diff --git a/assets/js/api-libs.js b/assets/js/api-libs.js index 7cd5a7573..17cb76c85 100644 --- a/assets/js/api-libs.js +++ b/assets/js/api-libs.js @@ -1,37 +1,43 @@ //////////////////////////////////////////////////////////////////////////////// ///////////////// Preferred Client Library programming language /////////////// //////////////////////////////////////////////////////////////////////////////// +import { activateTabs, updateBtnURLs } from './tabbed-content.js'; +import { getPreference, setPreference } from './local-storage.js'; -function getVisitedApiLib () { +function getVisitedApiLib() { const path = window.location.pathname.match( /client-libraries\/((?:v[0-9]|flight)\/)?([a-zA-Z0-9]*)/ ); return path && path.length && path[2]; } -function isApiLib () { +function isApiLib() { return /\/client-libraries\//.test(window.location.pathname); } // Set the user's programming language (client library) preference. -function setApiLibPreference (preference) { +function setApiLibPreference(preference) { setPreference('api_lib', preference); } // Retrieve the user's programming language (client library) preference. -function getApiLibPreference () { +function getApiLibPreference() { return getPreference('api_lib') || ''; } -// When visit a client library page, set the api_lib preference -if (isApiLib()) { - var selectedApiLib = getVisitedApiLib(); - setPreference('api_lib', selectedApiLib); +function initialize() { + // When visiting a client library page, set the api_lib preference + if (isApiLib()) { + var selectedApiLib = getVisitedApiLib(); + setPreference('api_lib', selectedApiLib); + } + + // Activate code-tabs based on the cookie then override with query param. + const tab = getApiLibPreference(); + ['.tabs, .code-tabs'].forEach( + (selector) => activateTabs(selector, tab), + updateBtnURLs(tab) + ); } -// Activate code-tabs based on the cookie then override with query param. -var tab = getApiLibPreference(); -['.tabs, .code-tabs'].forEach( - selector => activateTabs(selector, tab), - updateBtnURLs(tab) -); +export { initialize }; diff --git a/assets/js/ask-ai-trigger.js b/assets/js/ask-ai-trigger.js new file mode 100644 index 000000000..406a6d727 --- /dev/null +++ b/assets/js/ask-ai-trigger.js @@ -0,0 +1,19 @@ +import AskAI from './ask-ai.js'; + +function showTrigger(element) { + // Remove the inline display: none style + element.removeAttribute('style'); +} + +export default function AskAITrigger({ component }) { + const kapaContainer = document.querySelector('#kapa-widget-container'); + if (!component && !kapaContainer) { + return; + } + if (!kapaContainer) { + // Initialize the chat widget + AskAI({ onChatLoad: () => showTrigger(component) }); + } else { + showTrigger(component); + } +} \ No newline at end of file diff --git a/assets/js/ask-ai.js b/assets/js/ask-ai.js new file mode 100644 index 000000000..844df1596 --- /dev/null +++ b/assets/js/ask-ai.js @@ -0,0 +1,100 @@ +import { productData } from './page-context.js'; + +function setUser(userid, email) { + const NAMESPACE = 'kapaSettings'; + + // Set the user ID and email in the global settings namespace. + // The chat widget will use this on subsequent chats to personalize the user's experience. + window[NAMESPACE] = { + user: { + uniqueClientId: userid, + email: email, + } + } +} + +// Initialize the chat widget +function initializeChat({onChatLoad, chatAttributes}) { + /* See https://docs.kapa.ai/integrations/website-widget/configuration for + * available configuration options. + * All values are strings. + */ + const requiredAttributes = { + websiteId: 'a02bca75-1dd3-411e-95c0-79ee1139be4d', + projectName: 'InfluxDB', + projectColor: '#020a47', + projectLogo: '/img/influx-logo-cubo-white.png', + } + + const optionalAttributes = { + modalDisclaimer: 'This AI can access [documentation for InfluxDB, clients, and related tools](https://docs.influxdata.com). Information you submit is used in accordance with our [Privacy Policy](https://www.influxdata.com/legal/privacy-policy/).', + modalExampleQuestions: 'Use Python to write data to InfluxDB 3,How do I query using SQL?,How do I use MQTT with Telegraf?', + buttonHide: 'true', + modalOpenOnCommandK: 'true', + modalExampleQuestionsColSpan: '8', + modalFullScreenOnMobile: 'true', + modalHeaderPadding: '.5rem', + modalInnerPositionRight: '0', + modalInnerPositionLeft: '', + modalLockScroll: 'false', + modalOverrideOpenClassAskAi: 'ask-ai-open', + modalSize: '500px', + modalWithOverlay: 'false', + modalInnerMaxWidth: '500px', + modalXOffset: '1rem', + modalYOffset: '10vh', + userAnalyticsFingerprintEnabled: 'true', + fontFamily: 'Proxima Nova, sans-serif', + modalHeaderBgColor: 'linear-gradient(90deg, #d30971 0%, #9b2aff 100%)', + modalHeaderBorderBottom: 'none', + modalTitleColor: '#fff', + modalTitleFontSize: '1.25rem', + } + + const scriptUrl = 'https://widget.kapa.ai/kapa-widget.bundle.js'; + const script = document.createElement('script'); + script.async = true; + script.src = scriptUrl; + script.onload = function() { + onChatLoad(); + window.influxdatadocs.AskAI = AskAI; + }; + script.onerror = function() { + console.error('Error loading AI chat widget script'); + }; + + const dataset = {...requiredAttributes, ...optionalAttributes, ...chatAttributes}; + Object.keys(dataset).forEach(key => { + // Assign dataset attributes from the object + script.dataset[key] = dataset[key]; + }); + + // Check for an existing script element to remove + const oldScript= document.querySelector(`script[src="${scriptUrl}"]`); + if (oldScript) { + oldScript.remove(); + } + document.head.appendChild(script); +} + +function getProductExampleQuestions() { + const questions = productData?.product?.ai_sample_questions || null; + return questions?.join(',') || ''; +} + +/** + * chatParams: specify custom (for example, page-specific) attribute values for the chat, pass the dataset key-values (collected in ...chatParams). See https://docs.kapa.ai/integrations/website-widget/configuration for available configuration options. + * onChatLoad: function to call when the chat widget has loaded + * userid: optional, a unique user ID for the user (not currently used for public docs) +*/ +export default function AskAI({ userid, email, onChatLoad, ...chatParams }) { + const chatAttributes = { + modalExampleQuestions: getProductExampleQuestions(), + ...chatParams, + } + initializeChat({onChatLoad, chatAttributes}); + + if (userid) { + setUser(userid, email); + } +} diff --git a/assets/js/code-controls.js b/assets/js/code-controls.js index 76589de43..2b6344d3d 100644 --- a/assets/js/code-controls.js +++ b/assets/js/code-controls.js @@ -1,7 +1,10 @@ -var codeBlockSelector = ".article--content pre"; -var codeBlocks = $(codeBlockSelector); +import $ from 'jquery'; -var appendHTML = ` +function initialize() { + var codeBlockSelector = '.article--content pre'; + var codeBlocks = $(codeBlockSelector); + + var appendHTML = `
    @@ -9,98 +12,106 @@ var appendHTML = `
  • Fill window
-` +`; -// Wrap all codeblocks with a new 'codeblock' div -$(codeBlocks).each(function() { - $(this).wrap("
"); -}); + // Wrap all codeblocks with a new 'codeblock' div + $(codeBlocks).each(function () { + $(this).wrap("
"); + }); -// Append code controls to all codeblock divs -$('.codeblock').append(appendHTML); + // Append code controls to all codeblock divs + $('.codeblock').append(appendHTML); -//////////////////////////// CODE CONTROLS TOGGLING //////////////////////////// + //////////////////////////// CODE CONTROLS TOGGLING //////////////////////////// -// Click outside of the code-controls to close them -$(document).click(function() { - $('.code-controls').removeClass('open') -}); + // Click outside of the code-controls to close them + $(document).click(function () { + $('.code-controls').removeClass('open'); + }); -// Click the code controls toggle to open code controls -$('.code-controls-toggle').click(function() { - $(this).parent('.code-controls').toggleClass('open'); -}) + // Click the code controls toggle to open code controls + $('.code-controls-toggle').click(function () { + $(this).parent('.code-controls').toggleClass('open'); + }); -// Stop event propagation for clicks inside of the code-controls div -$('.code-controls').click(function(e) { - e.stopPropagation(); -}); + // Stop event propagation for clicks inside of the code-controls div + $('.code-controls').click(function (e) { + e.stopPropagation(); + }); -/////////////////////////////// COPY TO CLIPBOARD ////////////////////////////// + /////////////////////////////// COPY TO CLIPBOARD ////////////////////////////// -// Update button text during lifecycles -function updateText(element, currentText, newText) { - let inner = (element)[0].innerHTML; - inner = inner.replace(currentText, newText) - - element[0].innerHTML = inner -} + // Update button text during lifecycles + function updateText(element, currentText, newText) { + let inner = element[0].innerHTML; + inner = inner.replace(currentText, newText); -// Trigger copy success state lifecycle -function copyLifeCycle(element, state) { - let stateData = ((state === 'success') ? {state: 'success', message: 'Copied!'} : {state: 'failed', message: 'Copy failed!'}) - - updateText(element, 'Copy', stateData.message) - element.addClass(stateData.state) - - setTimeout(function() { - updateText(element, stateData.message, 'Copy'); - element.removeClass(stateData.state) - }, 2500) -} - -// Trigger copy failure state lifecycle - -$('.copy-code').click(function() { - let text = $(this).closest('.code-controls').prev('pre')[0].innerText - - const copyContent = async () => { - try { - await navigator.clipboard.writeText(text); - copyLifeCycle($(this), 'success') - } catch (err) { - copyLifeCycle($(this), 'failed') - } + element[0].innerHTML = inner; } - copyContent() -}) + // Trigger copy success state lifecycle + function copyLifeCycle(element, state) { + let stateData = + state === 'success' + ? { state: 'success', message: 'Copied!' } + : { state: 'failed', message: 'Copy failed!' }; -/////////////////////////////// FULL WINDOW CODE /////////////////////////////// + updateText(element, 'Copy', stateData.message); + element.addClass(stateData.state); -/* + setTimeout(function () { + updateText(element, stateData.message, 'Copy'); + element.removeClass(stateData.state); + }, 2500); + } + + // Trigger copy failure state lifecycle + + $('.copy-code').click(function () { + let text = $(this).closest('.code-controls').prev('pre')[0].innerText; + + const copyContent = async () => { + try { + await navigator.clipboard.writeText(text); + copyLifeCycle($(this), 'success'); + } catch { + copyLifeCycle($(this), 'failed'); + } + }; + + copyContent(); + }); + + /////////////////////////////// FULL WINDOW CODE /////////////////////////////// + + /* On click, open the fullscreen code modal and append a clone of the selected codeblock. Disable scrolling on the body. Disable user selection on everything but the fullscreen codeblock. */ -$('.fullscreen-toggle').click(function() { - var code = $(this).closest('.code-controls').prev('pre').clone(); - - $('#fullscreen-code-placeholder').replaceWith(code[0]); - $('body').css('overflow', 'hidden'); - $('body > div:not(.fullscreen-code)').css('user-select', 'none'); - $('.fullscreen-code').fadeIn(); -}) + $('.fullscreen-toggle').click(function () { + var code = $(this).closest('.code-controls').prev('pre').clone(); -/* + $('#fullscreen-code-placeholder').replaceWith(code[0]); + $('body').css('overflow', 'hidden'); + $('body > div:not(.fullscreen-code)').css('user-select', 'none'); + $('.fullscreen-code').fadeIn(); + }); + + /* On click, close the fullscreen code block. Reenable scrolling on the body. Reenable user selection on everything. Close the modal and replace the code block with the placeholder element. */ -$('.fullscreen-close').click(function() { - $('body').css('overflow', 'auto'); - $('body > div:not(.fullscreen-code)').css('user-select', ''); - $('.fullscreen-code').fadeOut(); - $('.fullscreen-code pre').replaceWith('
') -}); + $('.fullscreen-close').click(function () { + $('body').css('overflow', 'auto'); + $('body > div:not(.fullscreen-code)').css('user-select', ''); + $('.fullscreen-code').fadeOut(); + $('.fullscreen-code pre').replaceWith( + '
' + ); + }); +} + +export { initialize }; diff --git a/assets/js/content-interactions.js b/assets/js/content-interactions.js index 3a1737f76..eb9b4e1bc 100644 --- a/assets/js/content-interactions.js +++ b/assets/js/content-interactions.js @@ -1,154 +1,200 @@ +import $ from 'jquery'; + ///////////////////////////// Make headers linkable ///////////////////////////// -var headingWhiteList = $("\ +function makeHeadersLinkable() { + var headingWhiteList = $( + '\ .article--content h2, \ .article--content h3, \ .article--content h4, \ .article--content h5, \ .article--content h6 \ -"); +' + ); -var headingBlackList = ("\ + var headingBlackList = + '\ .influxdbu-banner h4 \ -"); +'; -headingElements = headingWhiteList.not(headingBlackList); + const headingElements = headingWhiteList.not(headingBlackList); -headingElements.each(function() { + headingElements.each(function () { function getLink(element) { - return ((element.attr('href') === undefined ) ? $(element).attr("id") : element.attr('href')) + return element.attr('href') === undefined + ? $(element).attr('id') + : element.attr('href'); } - var link = "" - $(this).wrapInner( link ); - }) + var link = ''; + $(this).wrapInner(link); + }); +} ///////////////////////////////// Smooth Scroll ///////////////////////////////// -var elementWhiteList = [ - ".tabs p a", - ".code-tabs p a", - ".children-links a", - ".list-links a", - "a.url-trigger", - "a.fullscreen-close" -] +function smoothScroll() { + var elementWhiteList = [ + '.tabs p a', + '.code-tabs p a', + '.children-links a', + '.list-links a', + 'a.url-trigger', + 'a.fullscreen-close', + ]; + + $('.article a[href^="#"]:not(' + elementWhiteList + ')').click(function (e) { + e.preventDefault(); + scrollToAnchor(this.hash); + }); +} function scrollToAnchor(target) { var $target = $(target); - if($target && $target.length > 0) { - $('html, body').stop().animate({ - 'scrollTop': ($target.offset().top) - }, 400, 'swing', function () { - window.location.hash = target; - }); + if ($target && $target.length > 0) { + $('html, body') + .stop() + .animate( + { + scrollTop: $target.offset().top, + }, + 400, + 'swing', + function () { + window.location.hash = target; + } + ); // Unique accordion functionality // If the target is an accordion element, open the accordion after scrolling if ($target.hasClass('expand')) { - if ($(target + ' .expand-label .expand-toggle').hasClass('open')) {} - else { + if ($(target + ' .expand-label .expand-toggle').hasClass('open')) { + // Do nothing? + } else { $(target + '> .expand-label').trigger('click'); - }; - }; + } + } } } -$('.article a[href^="#"]:not(' + elementWhiteList + ')').click(function (e) { - e.preventDefault(); - scrollToAnchor(this.hash); -}); - ///////////////////////////// Left Nav Interactions ///////////////////////////// -$(".children-toggle").click(function(e) { - e.preventDefault() - $(this).toggleClass('open'); - $(this).siblings('.children').toggleClass('open'); -}) - +function leftNavInteractions() { + $('.children-toggle').click(function (e) { + e.preventDefault(); + $(this).toggleClass('open'); + $(this).siblings('.children').toggleClass('open'); + }); +} //////////////////////////// Mobile Contents Toggle //////////////////////////// -$('#contents-toggle-btn').click(function(e) { - e.preventDefault(); - $(this).toggleClass('open'); - $('#nav-tree').toggleClass('open'); -}) - +function mobileContentsToggle() { + $('#contents-toggle-btn').click(function (e) { + e.preventDefault(); + $(this).toggleClass('open'); + $('#nav-tree').toggleClass('open'); + }); +} /////////////////////////////// Truncate Content /////////////////////////////// -$(".truncate-toggle").click(function(e) { - e.preventDefault() - var truncateParent = $(this).closest('.truncate') - var truncateParentID = $(this).closest('.truncate')[0].id +function truncateContent() { + $('.truncate-toggle').click(function (e) { + e.preventDefault(); + var truncateParent = $(this).closest('.truncate'); + var truncateParentID = $(this).closest('.truncate')[0].id; - if (truncateParent.hasClass('closed')) { - $(this)[0].href = `#${truncateParentID}` - } else { - $(this)[0].href = "#" - } - - truncateParent.toggleClass('closed') - truncateParent.find('.truncate-content').toggleClass('closed') -}) - -////////////////////////////// Expand Accordions /////////////////////////////// - -$('.expand-label').click(function() { - $(this).children('.expand-toggle').toggleClass('open') - $(this).next('.expand-content').slideToggle(200) -}) - -// Expand accordions on load based on URL anchor -function openAccordionByHash() { - var anchor = window.location.hash; - - function expandElement() { - if ($(anchor).parents('.expand').length > 0) { - return $(anchor).closest('.expand').children('.expand-label'); - } else if ($(anchor).hasClass('expand')){ - return $(anchor).children('.expand-label'); + if (truncateParent.hasClass('closed')) { + $(this)[0].href = `#${truncateParentID}`; + } else { + $(this)[0].href = '#'; } - }; - if (expandElement() != null) { - if (expandElement().children('.expand-toggle').hasClass('open')) {} - else { - expandElement().children('.expand-toggle').trigger('click'); - }; - }; -}; + truncateParent.toggleClass('closed'); + truncateParent.find('.truncate-content').toggleClass('closed'); + }); +} +////////////////////////////// Expand Accordions /////////////////////////////// +function expandAccordions() { + $('.expand-label').click(function () { + $(this).children('.expand-toggle').toggleClass('open'); + $(this).next('.expand-content').slideToggle(200); + }); -// Open accordions by hash on page load. -openAccordionByHash() + // Expand accordions on load based on URL anchor + function openAccordionByHash() { + var anchor = window.location.hash; + function expandElement() { + if ($(anchor).parents('.expand').length > 0) { + return $(anchor).closest('.expand').children('.expand-label'); + } else if ($(anchor).hasClass('expand')) { + return $(anchor).children('.expand-label'); + } + } + + if (expandElement() != null) { + if (expandElement().children('.expand-toggle').hasClass('open')) { + // Do nothing? + } else { + expandElement().children('.expand-toggle').trigger('click'); + } + } + } + + // Open accordions by hash on page load. + openAccordionByHash(); +} ////////////////////////// Inject tooltips on load ////////////////////////////// -$('.tooltip').each( function(){ - $toolTipText = $('
').addClass('tooltip-text').text($(this).attr('data-tooltip-text')); - $toolTipElement = $('
').addClass('tooltip-container').append($toolTipText); - $(this).prepend($toolTipElement); -}); - +function injectTooltips() { + $('.tooltip').each(function () { + const $toolTipText = $('
') + .addClass('tooltip-text') + .text($(this).attr('data-tooltip-text')); + const $toolTipElement = $('
') + .addClass('tooltip-container') + .append($toolTipText); + $(this).prepend($toolTipElement); + }); +} //////////////////// Style time cells in tables to not wrap //////////////////// -$('.article--content table').each(function() { - var table = $(this); +function styleTimeCells() { + $('.article--content table').each(function () { + var table = $(this); - table.find('td').each(function() { - let cellContent = $(this)[0].innerText - - if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z/.test(cellContent)) { - $(this).addClass('nowrap') - } - }) -}) + table.find('td').each(function () { + let cellContent = $(this)[0].innerText; + if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z/.test(cellContent)) { + $(this).addClass('nowrap'); + } + }); + }); +} /////////////////////// Open external links in a new tab /////////////////////// -$('.article--content a').each(function() { - var currentHost = location.host; +function openExternalLinks() { + $('.article--content a').each(function () { + var currentHost = location.host; - if (!($(this)[0].href).includes(currentHost)) { - $(this).attr('target', '_blank'); - }; -}) + if (!$(this)[0].href.includes(currentHost)) { + $(this).attr('target', '_blank'); + } + }); +} + +/////////////////////// Initialize all functions ////////////////////////////// +function initialize() { + makeHeadersLinkable(); + smoothScroll(); + leftNavInteractions(); + mobileContentsToggle(); + truncateContent(); + expandAccordions(); + injectTooltips(); + styleTimeCells(); + openExternalLinks(); +} + +export { initialize, scrollToAnchor }; diff --git a/assets/js/custom-timestamps.js b/assets/js/custom-timestamps.js index 9f8cb6d4c..c9e32838b 100644 --- a/assets/js/custom-timestamps.js +++ b/assets/js/custom-timestamps.js @@ -1,13 +1,18 @@ +import $ from 'jquery'; +import { Datepicker } from 'vanillajs-datepicker'; +import { toggleModal } from './modals.js'; +import * as localStorage from './local-storage.js'; + // Placeholder start date used in InfluxDB custom timestamps const defaultStartDate = '2022-01-01'; // Return yyyy-mm-dd formatted string from a Date object -function formatDate (dateObj) { +function formatDate(dateObj) { return dateObj.toISOString().replace(/T.*$/, ''); } // Return yesterday's date -function yesterday () { +function yesterday() { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); @@ -15,25 +20,25 @@ function yesterday () { } // Split a date string into year, month, and day -function datePart (date) { - datePartRegex = /(\d{4})-(\d{2})-(\d{2})/; - year = date.replace(datePartRegex, '$1'); - month = date.replace(datePartRegex, '$2'); - day = date.replace(datePartRegex, '$3'); +function datePart(date) { + const datePartRegex = /(\d{4})-(\d{2})-(\d{2})/; + const year = date.replace(datePartRegex, '$1'); + const month = date.replace(datePartRegex, '$2'); + const day = date.replace(datePartRegex, '$3'); return { year: year, month: month, day: day }; } ///////////////////////// PREFERENCE COOKIE MANAGEMENT ///////////////////////// -prefID = 'sample_get_started_date'; +const prefID = 'sample_get_started_date'; -function setStartDate (setDate) { - setPreference(prefID, setDate); +function setStartDate(setDate) { + localStorage.setPreference(prefID, setDate); } -function getStartDate () { - return getPreference(prefID); +function getStartDate() { + return localStorage.getPreference(prefID); } //////////////////////////////////////////////////////////////////////////////// @@ -42,71 +47,71 @@ function getStartDate () { var startDate = getStartDate() || yesterday(); // Convert a time value to a Unix timestamp (seconds) -function timeToUnixSeconds (time) { - unixSeconds = new Date(time).getTime() / 1000; +function timeToUnixSeconds(time) { + const unixSeconds = new Date(time).getTime() / 1000; return unixSeconds; } -// Default time values in getting started sample data -let times = [ - { - rfc3339: `${defaultStartDate}T08:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T08:00:00Z`), - }, // 1641024000 - { - rfc3339: `${defaultStartDate}T09:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T09:00:00Z`), - }, // 1641027600 - { - rfc3339: `${defaultStartDate}T10:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T10:00:00Z`), - }, // 1641031200 - { - rfc3339: `${defaultStartDate}T11:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T11:00:00Z`), - }, // 1641034800 - { - rfc3339: `${defaultStartDate}T12:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T12:00:00Z`), - }, // 1641038400 - { - rfc3339: `${defaultStartDate}T13:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T13:00:00Z`), - }, // 1641042000 - { - rfc3339: `${defaultStartDate}T14:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T14:00:00Z`), - }, // 1641045600 - { - rfc3339: `${defaultStartDate}T15:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T15:00:00Z`), - }, // 1641049200 - { - rfc3339: `${defaultStartDate}T16:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T16:00:00Z`), - }, // 1641052800 - { - rfc3339: `${defaultStartDate}T17:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T17:00:00Z`), - }, // 1641056400 - { - rfc3339: `${defaultStartDate}T18:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T18:00:00Z`), - }, // 1641060000 - { - rfc3339: `${defaultStartDate}T19:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T19:00:00Z`), - }, // 1641063600 - { - rfc3339: `${defaultStartDate}T20:00:00Z`, - unix: timeToUnixSeconds(`${defaultStartDate}T20:00:00Z`), - }, // 1641067200 -]; + // Default time values in getting started sample data + const defaultTimes = [ + { + rfc3339: `${defaultStartDate}T08:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T08:00:00Z`), + }, // 1641024000 + { + rfc3339: `${defaultStartDate}T09:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T09:00:00Z`), + }, // 1641027600 + { + rfc3339: `${defaultStartDate}T10:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T10:00:00Z`), + }, // 1641031200 + { + rfc3339: `${defaultStartDate}T11:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T11:00:00Z`), + }, // 1641034800 + { + rfc3339: `${defaultStartDate}T12:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T12:00:00Z`), + }, // 1641038400 + { + rfc3339: `${defaultStartDate}T13:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T13:00:00Z`), + }, // 1641042000 + { + rfc3339: `${defaultStartDate}T14:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T14:00:00Z`), + }, // 1641045600 + { + rfc3339: `${defaultStartDate}T15:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T15:00:00Z`), + }, // 1641049200 + { + rfc3339: `${defaultStartDate}T16:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T16:00:00Z`), + }, // 1641052800 + { + rfc3339: `${defaultStartDate}T17:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T17:00:00Z`), + }, // 1641056400 + { + rfc3339: `${defaultStartDate}T18:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T18:00:00Z`), + }, // 1641060000 + { + rfc3339: `${defaultStartDate}T19:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T19:00:00Z`), + }, // 1641063600 + { + rfc3339: `${defaultStartDate}T20:00:00Z`, + unix: timeToUnixSeconds(`${defaultStartDate}T20:00:00Z`), + }, // 1641067200 + ]; -function updateTimestamps (newStartDate) { +function updateTimestamps (newStartDate, seedTimes=defaultTimes) { // Update the times array with replacement times - times = times.map(x => { + const times = seedTimes.map(x => { var newStartTimestamp = x.rfc3339.replace(/^.*T/, newStartDate + 'T'); return { @@ -128,13 +133,13 @@ function updateTimestamps (newStartDate) { var wrapper = $(this)[0]; times.forEach(function (x) { - oldDatePart = datePart(x.rfc3339.replace(/T.*$/, '')); - newDatePart = datePart(x.rfc3339_new.replace(/T.*$/, '')); - rfc3339Regex = new RegExp( + const oldDatePart = datePart(x.rfc3339.replace(/T.*$/, '')); + const newDatePart = datePart(x.rfc3339_new.replace(/T.*$/, '')); + const rfc3339Regex = new RegExp( `${oldDatePart.year}(.*?)${oldDatePart.month}(.*?)${oldDatePart.day}`, 'g' ); - rfc3339Repl = `${newDatePart.year}$1${newDatePart.month}$2${newDatePart.day}`; + const rfc3339Repl = `${newDatePart.year}$1${newDatePart.month}$2${newDatePart.day}`; wrapper.innerHTML = wrapper.innerHTML .replaceAll(x.unix, x.unix_new) @@ -146,13 +151,13 @@ function updateTimestamps (newStartDate) { var wrapper = $(this)[0]; times.forEach(function (x) { - oldDatePart = datePart(x.rfc3339.replace(/T.*$/, '')); - newDatePart = datePart(x.rfc3339_new.replace(/T.*$/, '')); - rfc3339Regex = new RegExp( + const oldDatePart = datePart(x.rfc3339.replace(/T.*$/, '')); + const newDatePart = datePart(x.rfc3339_new.replace(/T.*$/, '')); + const rfc3339Regex = new RegExp( `${oldDatePart.year}-${oldDatePart.month}-${oldDatePart.day}`, 'g' ); - rfc3339Repl = `${newDatePart.year}-${newDatePart.month}-${newDatePart.day}`; + const rfc3339Repl = `${newDatePart.year}-${newDatePart.month}-${newDatePart.day}`; wrapper.innerHTML = wrapper.innerHTML .replaceAll(x.unix, x.unix_new) @@ -161,7 +166,7 @@ function updateTimestamps (newStartDate) { }); // Create a new seed times array with new start time for next change - times = times.map(x => { + return times.map((x) => { var newStartTimestamp = x.rfc3339.replace(/^.*T/, newStartDate + 'T'); return { @@ -173,38 +178,50 @@ function updateTimestamps (newStartDate) { /////////////////////// MODAL INTERACTIONS / DATE PICKER /////////////////////// -// Date picker form element -var datePickerEl = $('#custom-date-selector'); +function CustomTimeTrigger({component}) { + const $component = $(component); + $component + .find('a[data-action="open"]:first') + .on('click', () => toggleModal('#influxdb-gs-date-select')); -// Initialize the date picker with the current startDate -const elem = datePickerEl[0]; -const datepicker = new Datepicker(elem, { - defaultViewDate: startDate, - format: 'yyyy-mm-dd', - nextArrow: '>', - prevArrow: '<', -}); + // Date picker form element + var datePickerEl = $('#custom-date-selector'); -//////////////////////////////////// ACTIONS /////////////////////////////////// + // Initialize the date picker with the current startDate + const elem = datePickerEl[0]; + const datepicker = new Datepicker(elem, { + defaultViewDate: startDate, + format: 'yyyy-mm-dd', + nextArrow: '>', + prevArrow: '<', + }); -// Initial update to yesterdays date ON PAGE LOAD -// Conditionally set the start date cookie it startDate is equal to the default value -updateTimestamps(startDate); -if (startDate === yesterday()) { - setStartDate(startDate); + //////////////////////////////////// ACTIONS /////////////////////////////////// + + // Initial update to yesterdays date ON PAGE LOAD + // Conditionally set the start date cookie it startDate is equal to the default value + let updatedTimes = updateTimestamps(startDate, defaultTimes); + + if (startDate === yesterday()) { + setStartDate(startDate); + } + + // Submit new date + $('#submit-custom-date').click(function () { + let newDate = datepicker.getDate(); + + if (newDate != undefined) { + newDate = formatDate(newDate); + + // Update the last updated timestamps with the new date + // and reassign the updated times. + updatedTimes = updateTimestamps(newDate, updatedTimes); + setStartDate(newDate); + toggleModal('#influxdb-gs-date-select'); + } else { + toggleModal('#influxdb-gs-date-select'); + } + }); } -// Sumbit new date -$('#submit-custom-date').click(function () { - let newDate = datepicker.getDate(); - - if (newDate != undefined) { - newDate = formatDate(newDate); - - updateTimestamps(newDate); - setStartDate(newDate); - toggleModal('#influxdb-gs-date-select'); - } else { - toggleModal('#influxdb-gs-date-select'); - } -}); +export { CustomTimeTrigger }; diff --git a/assets/js/docs-themes.js b/assets/js/docs-themes.js deleted file mode 100644 index ce9fa90f4..000000000 --- a/assets/js/docs-themes.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - Copied and pasted this script for CSS swaps w/ cookies from - http://www.thesitewizard.com/javascripts/change-style-sheets.shtml -*/ - -// *** TO BE CUSTOMISED *** -var style_preference_name = 'theme'; -var style_cookie_duration = 30; -var style_domain = 'docs.influxdata.com'; - -// *** END OF CUSTOMISABLE SECTION *** -// You do not need to customise anything below this line - -function switchStyle (css_title) { - // You may use this script on your site free of charge provided - // you do not remove this notice or the URL below. Script from - // http://www.thesitewizard.com/javascripts/change-style-sheets.shtml - var i, link_tag; - for ( - i = 0, link_tag = document.getElementsByTagName('link'); - i < link_tag.length; - i++ - ) { - if ( - link_tag[i].rel.indexOf('stylesheet') != -1 && - link_tag[i].title.includes('theme') - ) { - link_tag[i].disabled = true; - if (link_tag[i].title == css_title) { - link_tag[i].disabled = false; - } - } - setPreference(style_preference_name, css_title.replace(/-theme/, '')); - } -} - -function setStyleFromCookie () { - var css_title = `${getPreference(style_preference_name)}-theme`; - if (css_title !== undefined) { - switchStyle(css_title); - } -} diff --git a/assets/js/feature-callouts.js b/assets/js/feature-callouts.js index 918d783c2..253b09b13 100644 --- a/assets/js/feature-callouts.js +++ b/assets/js/feature-callouts.js @@ -13,21 +13,23 @@ function getCalloutID (el) { // Hide a callout and update the cookie with the viewed callout function hideCallout (calloutID) { - if (!notificationIsRead(calloutID)) { - setNotificationAsRead(calloutID, 'callout'); + if (!window.LocalStorageAPI.notificationIsRead(calloutID)) { + window.LocalStorageAPI.setNotificationAsRead(calloutID, 'callout'); $(`#${calloutID}`).fadeOut(200); } } // Show the url feature callouts on page load -$('.feature-callout').each(function () { - calloutID = calloutID($(this)); +$(document).ready(function () { + $('.feature-callout').each(function () { + const calloutID = getCalloutID($(this)); - if (!notificationIsRead(calloutID, 'callout')) { - $(`#${calloutID}.feature-callout`) - .fadeIn(300) - .removeClass('start-position'); - } + if (!window.LocalStorageAPI.notificationIsRead(calloutID, 'callout')) { + $(`#${calloutID}.feature-callout`) + .fadeIn(300) + .removeClass('start-position'); + } + }); }); // Hide the InfluxDB URL selector callout diff --git a/assets/js/helpers.js b/assets/js/helpers.js new file mode 100644 index 000000000..b3859be49 --- /dev/null +++ b/assets/js/helpers.js @@ -0,0 +1,13 @@ + +/** Delay execution of a function `fn` for a number of milliseconds `ms` + * e.g., delay a validation handler to avoid annoying the user. + */ +function delay(fn, ms) { + let timer = 0; + return function (...args) { + clearTimeout(timer); + timer = setTimeout(fn.bind(this, ...args), ms || 0); + }; +} + +export { delay }; \ No newline at end of file diff --git a/assets/js/influxdb-url.js b/assets/js/influxdb-url.js index 5d47995b1..2febe2b09 100644 --- a/assets/js/influxdb-url.js +++ b/assets/js/influxdb-url.js @@ -1,58 +1,44 @@ -var placeholderUrls = { - oss: 'http://localhost:8086', - cloud: 'https://cloud2.influxdata.com', - core: 'http://localhost:8181', - enterprise: 'http://localhost:8181', - serverless: 'https://cloud2.influxdata.com', - dedicated: 'cluster-id.a.influxdb.io', - clustered: 'cluster-host.com', -}; - /* - NOTE: The defaultUrls variable is defined in assets/js/local-storage.js +//////////////////////////////////////////////////////////////////////////////// +///////////////////////// INFLUXDB URL PREFERENCE ///////////////////////////// +//////////////////////////////////////////////////////////////////////////////// */ +import * as pageParams from '@params'; +import { + DEFAULT_STORAGE_URLS, + getPreference, + setPreference, + setInfluxDBUrls, + removeInfluxDBUrl, + getInfluxDBUrl, + getInfluxDBUrls, +} from './local-storage.js'; +import $ from 'jquery'; +import { context as PRODUCT_CONTEXT, referrerHost } from './page-context.js'; +import { delay } from './helpers.js'; +import { toggleModal } from './modals.js'; -var elementSelector = '.article--content pre:not(.preserve)'; +export const CLOUD_URLS = Object.values(pageParams.influxdb_urls.cloud.providers).flatMap((provider) => provider.regions?.map((region) => region.url)); -// Return the page context (cloud, serverless, oss/enterprise, dedicated, clustered, other) -function context() { - if (/\/influxdb\/cloud\//.test(window.location.pathname)) { - return 'cloud'; - } else if (/\/influxdb3\/core/.test(window.location.pathname)) { - return 'core'; - } else if (/\/influxdb3\/enterprise/.test(window.location.pathname)) { - return 'enterprise'; - } else if (/\/influxdb3\/cloud-serverless/.test(window.location.pathname)) { - return 'serverless'; - } else if (/\/influxdb3\/cloud-dedicated/.test(window.location.pathname)) { - return 'dedicated'; - } else if (/\/influxdb3\/clustered/.test(window.location.pathname)) { - return 'clustered'; - } else if ( - /\/(enterprise_|influxdb).*\/v[1-2]\//.test(window.location.pathname) - ) { - return 'oss/enterprise'; - } else { - return 'other'; +export function InfluxDBUrl() { + const UNIQUE_URL_PRODUCTS = ['dedicated', 'clustered']; + const IS_UNIQUE_URL_PRODUCT = UNIQUE_URL_PRODUCTS.includes(PRODUCT_CONTEXT); + + // Add actual cloud URLs as needed + const elementSelector = '.article--content pre:not(.preserve)'; + + ///////////////////// Stored preference management /////////////////////// + // Retrieve the user's InfluxDB preference (cloud or oss) from the influxdb_pref local storage key. Default is cloud. + function getURLPreference() { + return getPreference('influxdb_url'); } -} -//////////////////////////////////////////////////////////////////////////////// -///////////////////////// Session-management functions ///////////////////////// -//////////////////////////////////////////////////////////////////////////////// + // Set the user's selected InfluxDB preference (cloud or oss) + function setURLPreference(preference) { + setPreference('influxdb_url', preference); + } -// Retrieve the user's InfluxDB preference (cloud or oss) from the influxdb_pref -// local storage key. Default is cloud. -function getURLPreference() { - return getPreference('influxdb_url'); -} - -// Set the user's selected InfluxDB preference (cloud or oss) -function setURLPreference(preference) { - setPreference('influxdb_url', preference); -} - -/* + /* influxdata_docs_urls local storage object keys: - oss @@ -70,647 +56,634 @@ function setURLPreference(preference) { - custom */ -// Store URLs in the urls local storage object -function storeUrl(context, newUrl, prevUrl) { - urlsObj = {}; - urlsObj['prev_' + context] = prevUrl; - urlsObj[context] = newUrl; + // Store URLs in the urls local storage object + function storeUrl(context, newUrl, prevUrl) { + let urlsObj = {}; + urlsObj['prev_' + context] = prevUrl; + urlsObj[context] = newUrl; - setInfluxDBUrls(urlsObj); -} - -// Store custom URL in the url local storage object -// Used to populate the custom URL field -function storeCustomUrl(customUrl) { - setInfluxDBUrls({ custom: customUrl }); - $('input#custom[type=radio]').val(customUrl); -} - -// Set a URL in the urls local storage object to an empty string -// Used to clear the form when custom url input is left empty -function removeCustomUrl() { - removeInfluxDBUrl('custom'); -} - -// Store a product URL in the urls local storage object -// Used to populate the custom URL field -function storeProductUrl(product, productUrl) { - urlsObj = {}; - urlsObj[product] = productUrl; - - setInfluxDBUrls(urlsObj); - $(`input#${product}-url-field`).val(productUrl); -} - -// Set a product URL in the urls local storage object to an empty string -// Used to clear the form when dedicated url input is left empty -function removeProductUrl(product) { - removeInfluxDBUrl(product); -} - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////// InfluxDB URL utility functions //////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -// Preserve URLs in codeblocks that come just after or are inside a div -// with the class, .keep-url -function addPreserve() { - $('.keep-url').each(function () { - // For code blocks with no syntax highlighting - $(this).next('pre').addClass('preserve'); - // For code blocks with no syntax highlighting inside of a link (API endpoint blocks) - $(this).next('a').find('pre').addClass('preserve'); - // For code blocks with syntax highlighting - $(this).next('.highlight').find('pre').addClass('preserve'); - // For code blocks inside .keep-url div - // Special use case for codeblocks generated from yaml data / frontmatter - $(this).find('pre').addClass('preserve'); - }); -} - -// Retrieve the currently selected URLs from the urls local storage object. -function getUrls() { - var storedUrls = getInfluxDBUrls(); - var currentCloudUrl = storedUrls.cloud; - var currentOSSUrl = storedUrls.oss; - var currentCoreUrl = storedUrls.core; - var currentEnterpriseUrl = storedUrls.enterprise; - var currentServerlessUrl = storedUrls.serverless; - var currentDedicatedUrl = storedUrls.dedicated; - var currentClusteredUrl = storedUrls.clustered; - var urls = { - oss: currentOSSUrl, - cloud: currentCloudUrl, - core: currentCoreUrl, - enterprise: currentEnterpriseUrl, - serverless: currentServerlessUrl, - dedicated: currentDedicatedUrl, - clustered: currentClusteredUrl, - }; - return urls; -} - -// Retrieve the previously selected URLs from the from the urls local storage object. -// This is used to update URLs whenever you switch between browser tabs. -function getPrevUrls() { - var storedUrls = getInfluxDBUrls(); - var prevCloudUrl = storedUrls.prev_cloud; - var prevOSSUrl = storedUrls.prev_oss; - var prevCoreUrl = storedUrls.prev_core; - var prevEnterpriseUrl = storedUrls.prev_enterprise; - var prevServerlessUrl = storedUrls.prev_serverless; - var prevDedicatedUrl = storedUrls.prev_dedicated; - var prevClusteredUrl = storedUrls.prev_clustered; - var prevUrls = { - oss: prevOSSUrl, - cloud: prevCloudUrl, - core: prevCoreUrl, - enterprise: prevEnterpriseUrl, - serverless: prevServerlessUrl, - dedicated: prevDedicatedUrl, - clustered: prevClusteredUrl, - }; - return prevUrls; -} - -// Iterate through code blocks and update InfluxDB urls -function updateUrls(prevUrls, newUrls) { - var preference = getURLPreference(); - var prevUrlsParsed = { - oss: {}, - cloud: {}, - core: {}, - enterprise: {}, - serverless: {}, - dedicated: {}, - clustered: {}, - }; - - var newUrlsParsed = { - oss: {}, - cloud: {}, - core: {}, - enterprise: {}, - serverless: {}, - dedicated: {}, - clustered: {}, - }; - - Object.keys(prevUrls).forEach(function (k) { - try { - prevUrlsParsed[k] = new URL(prevUrls[k]); - } catch { - prevUrlsParsed[k] = { origin: prevUrls[k], host: prevUrls[k] }; - } - }); - - Object.keys(newUrls).forEach(function (k) { - try { - newUrlsParsed[k] = new URL(newUrls[k]); - } catch { - newUrlsParsed[k] = { origin: newUrls[k], host: newUrls[k] }; - } - }); - - /** - * Match and replace host with host - * then replace URL with URL. - **/ - var ossReplacements = [ - { replace: prevUrlsParsed.cloud, with: newUrlsParsed.cloud }, - { replace: prevUrlsParsed.oss, with: newUrlsParsed.oss }, - ]; - var cloudReplacements = [ - { replace: prevUrlsParsed.cloud, with: newUrlsParsed.cloud }, - { replace: prevUrlsParsed.oss, with: newUrlsParsed.cloud }, - ]; - var serverlessReplacements = [ - { replace: prevUrlsParsed.serverless, with: newUrlsParsed.serverless }, - { replace: prevUrlsParsed.oss, with: newUrlsParsed.serverless }, - ]; - var coreReplacements = [ - { replace: prevUrlsParsed.core, with: newUrlsParsed.core }, - ]; - var enterpriseReplacements = [ - { replace: prevUrlsParsed.enterprise, with: newUrlsParsed.enterprise }, - ]; - var dedicatedReplacements = [ - { replace: prevUrlsParsed.dedicated, with: newUrlsParsed.dedicated }, - ]; - var clusteredReplacements = [ - { replace: prevUrlsParsed.clustered, with: newUrlsParsed.clustered }, - ]; - - if (context() === 'cloud') { - var replacements = cloudReplacements; - } else if (context() === 'core') { - var replacements = coreReplacements; - } else if (context() === 'enterprise') { - var replacements = enterpriseReplacements; - } else if (context() === 'serverless') { - var replacements = serverlessReplacements; - } else if (context() === 'dedicated') { - var replacements = dedicatedReplacements; - } else if (context() === 'clustered') { - var replacements = clusteredReplacements; - } else if (context() === 'oss/enterprise') { - var replacements = ossReplacements; - } else if (preference === 'cloud') { - var replacements = cloudReplacements; - } else { - var replacements = ossReplacements; + setInfluxDBUrls(urlsObj); } - replacements.forEach(function (o) { - if (o.replace.origin != o.with.origin) { - var fuzzyOrigin = new RegExp(o.replace.origin + '(:(^443)|[0-9]+)?', 'g'); - $(elementSelector).each(function () { - $(this).html( - $(this) - .html() - .replace(fuzzyOrigin, function (m) { - return o.with.origin || m; - }) + // Store custom URL in the url local storage object + // Used to populate the custom URL field + function storeCustomUrl(customUrl) { + setInfluxDBUrls({ custom: customUrl }); + $('input#custom[type=radio]').val(customUrl); + } + + // Set a URL in the urls local storage object to an empty string + // Used to clear the form when custom url input is left empty + function removeCustomUrl() { + removeInfluxDBUrl('custom'); + } + + // Store a product URL in the urls local storage object + // Used to populate the custom URL field + function storeProductUrl(product, productUrl) { + let urlsObj = {}; + urlsObj[product] = productUrl; + + setInfluxDBUrls(urlsObj); + $(`input#${product}-url-field`).val(productUrl); + } + + // Set a product URL in the urls local storage object to an empty string + // Used to clear the form when dedicated url input is left empty + function removeProductUrl(product) { + removeInfluxDBUrl(product); + } + + //////////////////////////////////////////////////////////////////////////////// + //////////////////////// InfluxDB URL utility functions //////////////////////// + //////////////////////////////////////////////////////////////////////////////// + + // Preserve URLs in codeblocks that come just after or are inside a div + // with the class, .keep-url + function addPreserve() { + $('.keep-url').each(function () { + // For code blocks with no syntax highlighting + $(this).next('pre').addClass('preserve'); + // For code blocks with no syntax highlighting inside of a link (API endpoint blocks) + $(this).next('a').find('pre').addClass('preserve'); + // For code blocks with syntax highlighting + $(this).next('.highlight').find('pre').addClass('preserve'); + // For code blocks inside .keep-url div + // Special use case for codeblocks generated from yaml data / frontmatter + $(this).find('pre').addClass('preserve'); + }); + } + + // Retrieve the currently selected URLs from the urls local storage object. + function getUrls() { + const { cloud, oss, core, enterprise, serverless, dedicated, clustered } = getInfluxDBUrls(); + return { oss, cloud, core, enterprise, serverless, dedicated, clustered }; +} + + // Retrieve the previously selected URLs from the from the urls local storage object. + // This is used to update URLs whenever you switch between browser tabs. + function getPrevUrls() { + const { + prev_cloud: cloud, + prev_oss: oss, + prev_core: core, + prev_enterprise: enterprise, + prev_serverless: serverless, + prev_dedicated: dedicated, + prev_clustered: clustered, + } = getInfluxDBUrls(); + return { oss, cloud, core, enterprise, serverless, dedicated, clustered }; + } + + // Iterate through code blocks and update InfluxDB urls + function updateUrls(prevUrls, newUrls) { + var prevUrlsParsed = { + oss: {}, + cloud: {}, + core: {}, + enterprise: {}, + serverless: {}, + dedicated: {}, + clustered: {}, + }; + + var newUrlsParsed = { + oss: {}, + cloud: {}, + core: {}, + enterprise: {}, + serverless: {}, + dedicated: {}, + clustered: {}, + }; + + Object.keys(prevUrls).forEach(function (k) { + try { + prevUrlsParsed[k] = new URL(prevUrls[k]); + } catch { + prevUrlsParsed[k] = { origin: prevUrls[k], host: prevUrls[k] }; + } + }); + + Object.keys(newUrls).forEach(function (k) { + try { + newUrlsParsed[k] = new URL(newUrls[k]); + } catch { + newUrlsParsed[k] = { origin: newUrls[k], host: newUrls[k] }; + } + }); + + /** + * Match and replace host with host + * then replace URL with URL. + **/ + var ossReplacements = [ + { replace: prevUrlsParsed.cloud, with: newUrlsParsed.cloud }, + { replace: prevUrlsParsed.oss, with: newUrlsParsed.oss }, + ]; + var cloudReplacements = [ + { replace: prevUrlsParsed.cloud, with: newUrlsParsed.cloud }, + { replace: prevUrlsParsed.oss, with: newUrlsParsed.cloud }, + ]; + var serverlessReplacements = [ + { replace: prevUrlsParsed.serverless, with: newUrlsParsed.serverless }, + { replace: prevUrlsParsed.oss, with: newUrlsParsed.serverless }, + ]; + var coreReplacements = [ + { replace: prevUrlsParsed.core, with: newUrlsParsed.core }, + ]; + var enterpriseReplacements = [ + { replace: prevUrlsParsed.enterprise, with: newUrlsParsed.enterprise }, + ]; + var dedicatedReplacements = [ + { replace: prevUrlsParsed.dedicated, with: newUrlsParsed.dedicated }, + ]; + var clusteredReplacements = [ + { replace: prevUrlsParsed.clustered, with: newUrlsParsed.clustered }, + ]; + + var replacements; + switch (PRODUCT_CONTEXT) { + case 'cloud': + replacements = cloudReplacements; + break; + case 'core': + replacements = coreReplacements; + break; + case 'enterprise': + replacements = enterpriseReplacements; + break; + case 'serverless': + replacements = serverlessReplacements; + break; + case 'dedicated': + replacements = dedicatedReplacements; + break; + case 'clustered': + replacements = clusteredReplacements; + break; + case 'oss/enterprise': + replacements = ossReplacements; + break; + default: + if (getURLPreference() === 'cloud') { + replacements = cloudReplacements; + } else { + replacements = ossReplacements; + } + break; + } + replacements.forEach(function (o) { + if (o.replace.origin != o.with.origin) { + var fuzzyOrigin = new RegExp( + o.replace.origin + '(:(^443)|[0-9]+)?', + 'g' ); - }); - } - }); - - function replaceWholename(startStr, endStr, replacement) { - var startsWithSeparator = new RegExp('[/.]'); - var endsWithSeparator = new RegExp('[-.:]'); - if ( - !startsWithSeparator.test(startStr) && - !endsWithSeparator.test(endStr) - ) { - var newHost = startStr + replacement + endStr; - return newHost; - } - } - - replacements - .map(function (o) { - return { replace: o.replace.host, with: o.with.host }; - }) - .forEach(function (o) { - if (o.replace != o.with) { - var fuzzyHost = new RegExp('(.?)' + o.replace + '(.?)', 'g'); $(elementSelector).each(function () { $(this).html( $(this) .html() - .replace(fuzzyHost, function (m, p1, p2) { - var r = replaceWholename(p1, p2, o.with) || m; - return r; + .replace(fuzzyOrigin, function (m) { + return o.with.origin || m; }) ); }); } }); -} -// Append the URL selector button to each codeblock containing a placeholder URL -function appendUrlSelector() { - var appendToUrls = [ - placeholderUrls.oss, - placeholderUrls.cloud, - placeholderUrls.core, - placeholderUrls.enterprise, - placeholderUrls.serverless, - placeholderUrls.dedicated, - placeholderUrls.clustered, - ]; + function replaceWholename(startStr, endStr, replacement) { + var startsWithSeparator = new RegExp('[/.]'); + var endsWithSeparator = new RegExp('[-.:]'); + if ( + !startsWithSeparator.test(startStr) && + !endsWithSeparator.test(endStr) + ) { + var newHost = startStr + replacement + endStr; + return newHost; + } + } - getBtnText = (context) => { - contextText = { - 'oss/enterprise': 'Change InfluxDB URL', - cloud: 'InfluxDB Cloud Region', - core: 'Change InfluxDB URL', - enterprise: 'Change InfluxDB URL', - serverless: 'InfluxDB Cloud Region', - dedicated: 'Set Dedicated cluster URL', - clustered: 'Set InfluxDB cluster URL', - other: 'InfluxDB Cloud or OSS?', + replacements + .map(function (o) { + return { replace: o.replace.host, with: o.with.host }; + }) + .forEach(function (o) { + if (o.replace != o.with) { + var fuzzyHost = new RegExp('(.?)' + o.replace + '(.?)', 'g'); + $(elementSelector).each(function () { + $(this).html( + $(this) + .html() + .replace(fuzzyHost, function (m, p1, p2) { + var r = replaceWholename(p1, p2, o.with) || m; + return r; + }) + ); + }); + } + }); + } + + // Append the URL selector button to each codeblock containing a placeholder URL + function appendUrlSelector(urls={ + cloud: '', + oss: '', + core: '', + enterprise: '', + serverless: '', + dedicated: '', + clustered: '', + }) { + const appendToUrls = Object.values(urls); + + const getBtnText = (context) => { + const contextText = { + 'oss/enterprise': 'Change InfluxDB URL', + cloud: 'InfluxDB Cloud Region', + core: 'Change InfluxDB URL', + enterprise: 'Change InfluxDB URL', + serverless: 'InfluxDB Cloud Region', + dedicated: 'Set Cloud Dedicated cluster URL', + clustered: 'Set InfluxDB cluster URL', + other: 'InfluxDB Cloud or OSS?', + }; + + return contextText[context]; }; - return contextText[context]; - }; - appendToUrls.forEach(function (url) { - $(elementSelector).each(function () { - var code = $(this).html(); - if (code.includes(url)) { - $(this).after( - "' - ); - $('.select-url').fadeIn(400); - } + $(elementSelector).each(function () { + var code = $(this).html(); + if (code.includes(url)) { + $(this).after( + "' + ); + $('.select-url').fadeIn(400); + } + }); }); + } + +//////////////////////////////////////////////////////////////////////////// +////////////////// Initialize InfluxDB URL interactions //////////////////// +//////////////////////////////////////////////////////////////////////////// + + // Add the preserve tag to code blocks that shouldn't be updated + addPreserve(); + const { cloud, oss, core, enterprise, serverless, dedicated, clustered } = DEFAULT_STORAGE_URLS; + + // Append URL selector buttons to code blocks + appendUrlSelector({ cloud, oss, core, enterprise, serverless, dedicated, clustered }); + + // Update URLs on load + + updateUrls({ cloud, oss, core, enterprise, serverless, dedicated, clustered }, getUrls()); + + // Set active radio button on page load + setRadioButtons(getUrls()); + + //////////////////////////////////////////////////////////////////////////////// + ////////////////////////// Modal window interactions /////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + + // General modal window interactions are controlled in modals.js + + // Open the InfluxDB URL selector modal + $('.url-trigger').click(function (e) { + e.preventDefault(); + toggleModal('#influxdb-url-list'); }); -} -//////////////////////////////////////////////////////////////////////////////// -///////////////////////////// Function executions ////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -// Add the preserve tag to code blocks that shouldn't be updated -addPreserve(); - -// Append URL selector buttons to code blocks -appendUrlSelector(); - -// Update URLs on load -updateUrls(placeholderUrls, getUrls()); - -// Set active radio button on page load -setRadioButtons(getUrls()); - -//////////////////////////////////////////////////////////////////////////////// -////////////////////////// Modal window interactions /////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -// General modal window interactions are controlled in modals.js - -// Open the InfluxDB URL selector modal -$('.url-trigger').click(function (e) { - e.preventDefault(); - toggleModal('#influxdb-url-list'); -}); - -// Set the selected URL radio buttons to :checked -function setRadioButtons() { - currentUrls = getUrls(); - $('input[name="influxdb-cloud-url"][value="' + currentUrls.cloud + '"]').prop( - 'checked', - true - ); - $( - 'input[name="influxdb-serverless-url"][value="' + - currentUrls.serverless + - '"]' - ).prop('checked', true); - $('input[name="influxdb-oss-url"][value="' + currentUrls.oss + '"]').prop( - 'checked', - true - ); - $('input[name="influxdb-core-url"][value="' + currentUrls.core + '"]').prop( - 'checked', - true - ); - $('input[name="influxdb-enterprise-url"][value="' + currentUrls.enterprise + '"]').prop( - 'checked', - true - ); -} - -// Add checked to fake-radio if cluster is selected on page load -if ($('ul.clusters label input').is(':checked')) { - var group = $('ul.clusters label input:checked') - .parent() - .parent() - .parent() - .siblings(); - $('.fake-radio', group).addClass('checked'); -} - -// Select first cluster when region is clicked -$('p.region').click(function () { - if (!$('.fake-radio', this).hasClass('checked')) { - $('.fake-radio', this).addClass('checked'); - $('+ ul.clusters li:first label', this).trigger('click'); + // Set the selected URL radio buttons to :checked + function setRadioButtons() { + const currentUrls = getUrls(); + $( + 'input[name="influxdb-cloud-url"][value="' + currentUrls.cloud + '"]' + ).prop('checked', true); + $( + 'input[name="influxdb-serverless-url"][value="' + + currentUrls.serverless + + '"]' + ).prop('checked', true); + $('input[name="influxdb-oss-url"][value="' + currentUrls.oss + '"]').prop( + 'checked', + true + ); + $('input[name="influxdb-core-url"][value="' + currentUrls.core + '"]').prop( + 'checked', + true + ); + $( + 'input[name="influxdb-enterprise-url"][value="' + + currentUrls.enterprise + + '"]' + ).prop('checked', true); } -}); -// Remove checked class from fake-radio when another region is selected -$('.region-group').click(function () { - if (!$('.fake-radio', this).hasClass('checked')) { - $('.fake-radio', !this).removeClass('checked'); - $('.fake-radio', this).addClass('checked'); + // Add checked to fake-radio if cluster is selected on page load + if ($('ul.clusters label input').is(':checked')) { + var group = $('ul.clusters label input:checked') + .parent() + .parent() + .parent() + .siblings(); + $('.fake-radio', group).addClass('checked'); } -}); -// Update URLs and URL preference when selected/clicked in the modal -$('input[name="influxdb-oss-url"]').change(function () { - var newUrl = $(this).val(); - storeUrl('oss', newUrl, getUrls().oss); - updateUrls(getPrevUrls(), getUrls()); - setURLPreference('oss'); -}); -$('input[name="influxdb-oss-url"]').click(function () { - setURLPreference('oss'); -}); + // Select first cluster when region is clicked + $('p.region').click(function () { + if (!$('.fake-radio', this).hasClass('checked')) { + $('.fake-radio', this).addClass('checked'); + $('+ ul.clusters li:first label', this).trigger('click'); + } + }); -$('input[name="influxdb-cloud-url"]').change(function () { - var newUrl = $(this).val(); - storeUrl('cloud', newUrl, getUrls().cloud); - updateUrls(getPrevUrls(), getUrls()); -}); -$('input[name="influxdb-cloud-url"]').click(function () { - setURLPreference('cloud'); -}); + // Remove checked class from fake-radio when another region is selected + $('.region-group').click(function () { + if (!$('.fake-radio', this).hasClass('checked')) { + $('.fake-radio', !this).removeClass('checked'); + $('.fake-radio', this).addClass('checked'); + } + }); -$('input[name="influxdb-core-url"]').change(function () { - var newUrl = $(this).val(); - storeUrl('core', newUrl, getUrls().core); - updateUrls(getPrevUrls(), getUrls()); -}); + // Update URLs and URL preference when selected/clicked in the modal + $('input[name="influxdb-oss-url"]').change(function () { + var newUrl = $(this).val(); + storeUrl('oss', newUrl, getUrls().oss); + updateUrls(getPrevUrls(), getUrls()); + setURLPreference('oss'); + }); + $('input[name="influxdb-oss-url"]').click(function () { + setURLPreference('oss'); + }); -$('input[name="influxdb-enterprise-url"]').change(function () { - var newUrl = $(this).val(); - storeUrl('enterprise', newUrl, getUrls().enterprise); - updateUrls(getPrevUrls(), getUrls()); -}); + $('input[name="influxdb-cloud-url"]').change(function () { + var newUrl = $(this).val(); + storeUrl('cloud', newUrl, getUrls().cloud); + updateUrls(getPrevUrls(), getUrls()); + }); + $('input[name="influxdb-cloud-url"]').click(function () { + setURLPreference('cloud'); + }); -$('input[name="influxdb-serverless-url"]').change(function () { - var newUrl = $(this).val(); - storeUrl('serverless', newUrl, getUrls().serverless); - updateUrls(getPrevUrls(), getUrls()); -}); + $('input[name="influxdb-core-url"]').change(function () { + var newUrl = $(this).val(); + storeUrl('core', newUrl, getUrls().core); + updateUrls(getPrevUrls(), getUrls()); + }); -$('input[name="influxdb-dedicated-url"]').change(function () { - var newUrl = $(this).val(); - storeUrl('dedicated', newUrl, getUrls().dedicated); - updateUrls(getPrevUrls(), getUrls()); -}); + $('input[name="influxdb-enterprise-url"]').change(function () { + var newUrl = $(this).val(); + storeUrl('enterprise', newUrl, getUrls().enterprise); + updateUrls(getPrevUrls(), getUrls()); + }); -$('input[name="influxdb-clustered-url"]').change(function () { - var newUrl = $(this).val(); - storeUrl('clustered', newUrl, getUrls().clustered); - updateUrls(getPrevUrls(), getUrls()); -}); + $('input[name="influxdb-serverless-url"]').change(function () { + var newUrl = $(this).val(); + storeUrl('serverless', newUrl, getUrls().serverless); + updateUrls(getPrevUrls(), getUrls()); + }); -// Toggle preference tabs -function togglePrefBtns(el) { - preference = el.length ? el.attr('id').replace('pref-', '') : 'cloud'; - prefUrls = $('#' + preference + '-urls'); + $('input[name="influxdb-dedicated-url"]').change(function () { + var newUrl = $(this).val(); + storeUrl('dedicated', newUrl, getUrls().dedicated); + updateUrls(getPrevUrls(), getUrls()); + }); - el.addClass('active'); - el.siblings().removeClass('active'); - prefUrls.addClass('active').removeClass('inactive'); - prefUrls.siblings().addClass('inactive').removeClass('active'); - setURLPreference(preference); -} + $('input[name="influxdb-clustered-url"]').change(function () { + var newUrl = $(this).val(); + storeUrl('clustered', newUrl, getUrls().clustered); + updateUrls(getPrevUrls(), getUrls()); + }); -// Select preference tab on click -$('#pref-tabs .pref-tab').click(function () { - togglePrefBtns($(this)); -}); + // Populate the product-specific URL fields on page load + UNIQUE_URL_PRODUCTS.forEach(function (productEl) { + let productUrlCookie = getInfluxDBUrl(productEl); + $(`input#${productEl}-url-field`).val(productUrlCookie); + $(`#${productEl}-url-field`).val(productUrlCookie); + }); -// Select preference tab from local storage -function showPreference() { - var preference = getPreference('influxdb_url'); - prefTab = $('#pref-' + preference); - togglePrefBtns(prefTab); -} + // Toggle preference tabs + function togglePrefBtns(el) { + const preference = el.length ? el.attr('id').replace('pref-', '') : 'cloud'; + const prefUrls = $('#' + preference + '-urls'); -// Toggled preferred service on load -showPreference(); + el.addClass('active'); + el.siblings().removeClass('active'); + prefUrls.addClass('active').removeClass('inactive'); + prefUrls.siblings().addClass('inactive').removeClass('active'); + setURLPreference(preference); + } -//////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// Custom URLs ////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// + // Select preference tab on click + $('#pref-tabs .pref-tab').click(function () { + togglePrefBtns($(this)); + }); -// Validate custom URLs -function validateUrl(url) { - /** validDomain = (Named host | IPv6 host | IPvFuture host)(:Port)? **/ - var validDomain = new RegExp( - `([a-z0-9\-._~%]+` + - `|\[[a-f0-9:.]+\]` + - `|\[v[a-f0-9][a-z0-9\-._~%!$&'()*+,;=:]+\])` + - `(:[0-9]+)?` - ); + // Select preference tab from local storage + function showPreference() { + const preference = getPreference('influxdb_url'); + const prefTab = $('#pref-' + preference); + togglePrefBtns(prefTab); + } - if (!['dedicated', 'clustered'].includes(context())) { - // Validation for non-dedicated, non-clustered custom InfluxDB URLs - try { - new URL(url); - return { valid: true, error: '' }; - } catch (e) { - var validProtocol = /^http(s?)/; - var protocol = url.match(/http(s?):\/\//) + // Toggled preferred service on load + showPreference(); + + //////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////// Custom URLs ////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + + // Validate custom URLs + function validateUrl(url) { + /** validDomain = (Named host | IPv6 host | IPvFuture host)(:Port)? **/ + const validDomain = new RegExp( + `([a-z0-9\-._~%]+` + + `|\[[a-f0-9:.]+\]` + + `|\[v[a-f0-9][a-z0-9\-._~%!$&'()*+,;=:]+\])` + + `(:[0-9]+)?` + ); + + if (!IS_UNIQUE_URL_PRODUCT) { + // Validation for non-dedicated, non-clustered custom InfluxDB URLs + const validProtocol = /^http(s?)/; + const protocol = url.match(/http(s?):\/\//) ? url.match(/http(s?):\/\//)[0] : ''; - var domain = url.replace(protocol, ''); + const domain = url.replace(protocol, ''); + // First use the regex to check for an HTTP protocol and valid domain + // --JS URL validation can't differentiate host:port string from a protocol. if (validProtocol.test(protocol) == false) { return { valid: false, error: 'Invalid protocol, use http[s]' }; } else if (validDomain.test(domain) == false) { return { valid: false, error: 'Invalid domain' }; - } else if (e) { - return { valid: false, error: 'Invalid URL' }; + } else { + try { + new URL(url); + return { valid: true, error: '' }; + } catch (e) { + if (e instanceof TypeError) { + return { valid: false, error: 'Invalid URL' }; + } + } + } + } else { + // Validation for product-specific URLs + const includesProtocol = /^.*:\/\//; + const protocol = url.match(/^.*:\/\//) ? url.match(/^.*:\/\//)[0] : ''; + const domain = url.replace(protocol, ''); + + if (url.length === 0) { + return { valid: true, error: '' }; + } else if (includesProtocol.test(protocol) == true) { + return { valid: false, error: 'Do not include the protocol' }; + } else if (validDomain.test(domain) == false) { + return { valid: false, error: 'Invalid domain' }; + } else { + return { valid: true, error: '' }; } } - } else { - // Validation for product-specific URLs - var includesProtocol = /^.*:\/\//; - var protocol = url.match(/^.*:\/\//) ? url.match(/^.*:\/\//)[0] : ''; - var domain = url.replace(protocol, ''); + } - if (url.length === 0) { - return { valid: true, error: '' }; - } else if (includesProtocol.test(protocol) == true) { - return { valid: false, error: 'Do not include the protocol' }; - } else if (validDomain.test(domain) == false) { - return { valid: false, error: 'Invalid domain' }; + // Show validation errors + function showValidationMessage(validation) { + $('#custom-url').addClass('error'); + $('#custom-url').attr('data-message', validation.error); + } + + // Hide validation messages and replace the message attr with empty string + function hideValidationMessage() { + $('#custom-url').removeClass('error').attr('data-message', ''); + } + + // Set the custom URL local storage object and apply the change + // If the custom URL field is empty, it defaults to the context default + function applyCustomUrl() { + var custUrl = $('#custom-url-field').val(); + let urlValidation = validateUrl(custUrl); + if (custUrl.length > 0) { + if (urlValidation.valid) { + hideValidationMessage(); + storeCustomUrl(custUrl); + storeUrl(PRODUCT_CONTEXT, custUrl, getUrls()[PRODUCT_CONTEXT]); + updateUrls(getPrevUrls(), getUrls()); + } else { + showValidationMessage(urlValidation); + } } else { - return { valid: true, error: '' }; + removeCustomUrl(); + hideValidationMessage(); + $( + `input[name="influxdb-${PRODUCT_CONTEXT}-url"][value="${DEFAULT_URLS[PRODUCT_CONTEXT]}"]` + ).trigger('click'); } } -} -// Show validation errors -function showValidationMessage(validation) { - $('#custom-url').addClass('error'); - $('#custom-url').attr('data-message', validation.error); -} - -// Hide validation messages and replace the message attr with empty string -function hideValidationMessage() { - $('#custom-url').removeClass('error').attr('data-message', ''); -} - -// Set the custom URL local storage object and apply the change -// If the custom URL field is empty, it defaults to the context default -function applyCustomUrl() { - var custUrl = $('#custom-url-field').val(); - let urlValidation = validateUrl(custUrl); - if (custUrl.length > 0) { - if (urlValidation.valid) { + // Set the product URL local storage object and apply the change + // If the product URL field is empty, it defaults to the product default + function applyProductUrl(product) { + var productUrl = $(`#${product}-url-field`).val(); + let urlValidation = validateUrl(productUrl); + if (productUrl.length > 0) { + if (urlValidation.valid) { + hideValidationMessage(); + storeProductUrl(product, productUrl); + storeUrl(product, productUrl, getUrls()[product]); + updateUrls(getPrevUrls(), getUrls()); + } else { + showValidationMessage(urlValidation); + } + } else { + removeProductUrl(product); hideValidationMessage(); - storeCustomUrl(custUrl); - storeUrl(context(), custUrl, getUrls()[context()]); - updateUrls(getPrevUrls(), getUrls()); + } + } + + // Trigger radio button on custom URL field focus + $('input#custom-url-field').focus(function () { + $('input#custom[type="radio"]').trigger('click'); + }); + + // Update URLs and close modal when using 'enter' to exit custom URL field + $('#custom-url').submit(function (e) { + e.preventDefault(); + + let url = $('#custom-url-field').val() || ''; + + if (['dedicated', 'clustered'].includes(PRODUCT_CONTEXT)) { + url = $(`#${PRODUCT_CONTEXT}-url-field`).val() || ''; + } + + const urlValidation = validateUrl(url); + + if (url === '' || urlValidation.valid) { + if (!['dedicated', 'clustered'].includes(PRODUCT_CONTEXT)) { + applyCustomUrl(); + } else { + applyProductUrl(PRODUCT_CONTEXT); + } + $('#modal-close').trigger('click'); } else { showValidationMessage(urlValidation); } - } else { - removeCustomUrl(); - hideValidationMessage(); - $( - 'input[name="influxdb-${context()}-url"][value="' + defaultUrls[context()] + '"]' - ).trigger('click'); - } -} + }); -// Set the product URL local storage object and apply the change -// If the product URL field is empty, it defaults to the product default -function applyProductUrl(product) { - var productUrl = $(`#${product}-url-field`).val(); - let urlValidation = validateUrl(productUrl); - if (productUrl.length > 0) { + // List of elements that store custom URLs + var urlValueElements = [ + '#custom-url-field', + '#dedicated-url-field', + '#clustered-url-field', + ].join(); + + // Store the custom InfluxDB URL or product-specific URL when exiting the field + $(urlValueElements).blur(function () { + !['dedicated', 'clustered'].includes(PRODUCT_CONTEXT) + ? applyCustomUrl() + : applyProductUrl(PRODUCT_CONTEXT); + }); + + function handleUrlValidation() { + let url = $(urlValueElements).val(); + let urlValidation = validateUrl(url); if (urlValidation.valid) { hideValidationMessage(); - storeProductUrl(product, productUrl); - getUrls(product, productUrl, getUrls()[product]); - updateUrls(getPrevUrls(), getUrls()); } else { showValidationMessage(urlValidation); } - } else { - removeProductUrl(product); - hideValidationMessage(); } -} + // When in erred state, revalidate custom URL on keyup + $(document).on('keyup', urlValueElements, delay(handleUrlValidation, 500)); -// Trigger radio button on custom URL field focus -$('input#custom-url-field').focus(function (e) { - $('input#custom[type="radio"]').trigger('click'); -}); - -// Update URLs and close modal when using 'enter' to exit custom URL field -$('#custom-url').submit(function (e) { - e.preventDefault(); - - const productContext = context(); - let url = $('#custom-url-field').val() || ''; - - if (['dedicated', 'clustered'].includes(productContext)) { - url = $(`#${productContext}-url-field`).val() || ''; + // Populate the custom InfluxDB URL field on page load + var customUrlOnLoad = getInfluxDBUrl('custom'); + if (customUrlOnLoad != '') { + $('input#custom').val(customUrlOnLoad); + $('#custom-url-field').val(customUrlOnLoad); } - const urlValidation = validateUrl(url); + // Populate the product-specific URL fields on page load + var productsWithUniqueURLs = ['dedicated', 'clustered']; - if (url === '' || urlValidation.valid) { - if (!['dedicated', 'clustered'].includes(productContext)) { - applyCustomUrl(); - } else { - applyProductUrl(productContext); - } - $('#modal-close').trigger('click'); - } else { - showValidationMessage(urlValidation); - } -}); + productsWithUniqueURLs.forEach(function (productEl) { + const productUrlCookie = getInfluxDBUrl(productEl); + $(`input#${productEl}-url-field`).val(productUrlCookie); + $(`#${productEl}-url-field`).val(productUrlCookie); + }); -// List of elements that store custom URLs -var urlValueElements = [ - '#custom-url-field', - '#dedicated-url-field', - '#clustered-url-field', -].join(); + //////////////////////////////////////////////////////////////////////////////// + /////////////////////////// Dynamically update URLs //////////////////////////// + //////////////////////////////////////////////////////////////////////////////// -// Store the custom InfluxDB URL or product-specific URL when exiting the field -$(urlValueElements).blur(function () { - !['dedicated', 'clustered'].includes(context()) - ? applyCustomUrl() - : applyProductUrl(context()); -}); - -/** Delay execution of a function `fn` for a number of milliseconds `ms` - * e.g., delay a validation handler to avoid annoying the user. - */ -function delay(fn, ms) { - let timer = 0; - return function (...args) { - clearTimeout(timer); - timer = setTimeout(fn.bind(this, ...args), ms || 0); - }; -} - -function handleUrlValidation() { - let url = $(urlValueElements).val(); - let urlValidation = validateUrl(url); - if (urlValidation.valid) { - hideValidationMessage(); - } else { - showValidationMessage(urlValidation); + // Check if the referrerHost is one of the cloud URLs + // cloudUrls is built dynamically in layouts/partials/footer/javascript.html + if (CLOUD_URLS.includes(referrerHost)) { + storeUrl('cloud', referrerHost, getUrls().cloud); + updateUrls(getPrevUrls(), getUrls()); + setRadioButtons(); + setURLPreference('cloud'); + showPreference(); } } -// When in erred state, revalidate custom URL on keyup -$(document).on('keyup', urlValueElements, delay(handleUrlValidation, 500)); - -// Populate the custom InfluxDB URL field on page load -var customUrlOnLoad = getInfluxDBUrl('custom'); -if (customUrlOnLoad != '') { - $('input#custom').val(customUrlOnLoad); - $('#custom-url-field').val(customUrlOnLoad); -} - -// Populate the product-specific URL fields on page load -var productsWithUniqueURLs = ['dedicated', 'clustered']; - -productsWithUniqueURLs.forEach(function (productEl) { - productUrlCookie = getInfluxDBUrl(productEl); - $(`input#${productEl}-url-field`).val(productUrlCookie); - $(`#${productEl}-url-field`).val(productUrlCookie); -}); - -//////////////////////////////////////////////////////////////////////////////// -/////////////////////////// Dynamically update URLs //////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -// Extract the protocol and hostname of referrer -referrerMatch = document.referrer.match(/^(?:[^\/]*\/){2}[^\/]+/g); -referrerHost = referrerMatch ? referrerMatch[0] : ''; - -// Check if the referrerHost is one of the cloud URLs -// cloudUrls is built dynamically in layouts/partials/footer/javascript.html -if (cloudUrls.includes(referrerHost)) { - storeUrl('cloud', referrerHost, getUrls().cloud); - updateUrls(getPrevUrls(), getUrls()); - setRadioButtons(); - setURLPreference('cloud'); - showPreference(); -} diff --git a/assets/js/local-storage.js b/assets/js/local-storage.js index 229765238..9a981e35b 100644 --- a/assets/js/local-storage.js +++ b/assets/js/local-storage.js @@ -10,6 +10,7 @@ - messages: Messages (data/notifications.yaml) that have been seen (array) - callouts: Feature callouts that have been seen (array) */ +import * as pageParams from '@params'; // Prefix for all InfluxData docs local storage const storagePrefix = 'influxdata_docs_'; @@ -17,14 +18,14 @@ const storagePrefix = 'influxdata_docs_'; /* Initialize data in local storage with a default value. */ -initializeLocalStorage = (storageKey, defaultValue) => { - fullStorageKey = storagePrefix + storageKey; +function initializeStorageItem(storageKey, defaultValue) { + const fullStorageKey = storagePrefix + storageKey; // Check if the data exists before initializing the data if (localStorage.getItem(fullStorageKey) === null) { localStorage.setItem(fullStorageKey, defaultValue); } -}; +} /* //////////////////////////////////////////////////////////////////////////////// @@ -35,7 +36,7 @@ initializeLocalStorage = (storageKey, defaultValue) => { const prefStorageKey = storagePrefix + 'preferences'; // Default preferences -var defaultPrefObj = { +const defaultPrefObj = { api_lib: null, influxdb_url: 'cloud', sidebar_state: 'open', @@ -48,119 +49,113 @@ var defaultPrefObj = { Retrieve a preference from the preference key. If the key doesn't exist, initialize it with default values. */ -getPreference = prefName => { +function getPreference(prefName) { // Initialize preference data if it doesn't already exist if (localStorage.getItem(prefStorageKey) === null) { - initializeLocalStorage('preferences', JSON.stringify(defaultPrefObj)); + initializeStorageItem('preferences', JSON.stringify(defaultPrefObj)); } // Retrieve and parse preferences as JSON - prefString = localStorage.getItem(prefStorageKey); - prefObj = JSON.parse(prefString); + const prefString = localStorage.getItem(prefStorageKey); + const prefObj = JSON.parse(prefString); // Return the value of the specified preference return prefObj[prefName]; -}; +} // Set a preference in the preferences key -setPreference = (prefID, prefValue) => { - var prefString = localStorage.getItem(prefStorageKey); - let prefObj = JSON.parse(prefString); +function setPreference(prefID, prefValue) { + const prefString = localStorage.getItem(prefStorageKey); + const prefObj = JSON.parse(prefString); prefObj[prefID] = prefValue; localStorage.setItem(prefStorageKey, JSON.stringify(prefObj)); -}; +} // Return an object containing all preferences -getPreferences = () => JSON.parse(localStorage.getItem(prefStorageKey)); +function getPreferences() { + return JSON.parse(localStorage.getItem(prefStorageKey)); +} -/* //////////////////////////////////////////////////////////////////////////////// -///////////////////////////// INFLUXDATA DOCS URLS ///////////////////////////// +//////////// MANAGE INFLUXDATA DOCS URLS IN LOCAL STORAGE ////////////////////// //////////////////////////////////////////////////////////////////////////////// -*/ -const urlStorageKey = storagePrefix + 'urls'; -// Default URLs per product -var defaultUrls = { - oss: 'http://localhost:8086', - cloud: 'https://us-west-2-1.aws.cloud2.influxdata.com', - core: 'http://localhost:8181', - enterprise: 'http://localhost:8181', - serverless: 'https://us-east-1-1.aws.cloud2.influxdata.com', - dedicated: 'cluster-id.a.influxdb.io', - clustered: 'cluster-host.com', -}; +const defaultUrls = {}; +Object.entries(pageParams.influxdb_urls).forEach(([product, {providers}]) => { + defaultUrls[product] = providers.filter(provider => provider.name === 'Default')[0]?.regions[0]?.url; +}); -// Defines the default urls value -var defaultUrlsObj = { +export const DEFAULT_STORAGE_URLS = { oss: defaultUrls.oss, cloud: defaultUrls.cloud, serverless: defaultUrls.serverless, core: defaultUrls.core, enterprise: defaultUrls.enterprise, - dedicated: defaultUrls.dedicated, + dedicated: defaultUrls.cloud_dedicated, clustered: defaultUrls.clustered, prev_oss: defaultUrls.oss, prev_cloud: defaultUrls.cloud, prev_core: defaultUrls.core, prev_enterprise: defaultUrls.enterprise, prev_serverless: defaultUrls.serverless, - prev_dedicated: defaultUrls.dedicated, + prev_dedicated: defaultUrls.cloud_dedicated, prev_clustered: defaultUrls.clustered, custom: '', }; +const urlStorageKey = storagePrefix + 'urls'; + // Return an object that contains all InfluxDB urls stored in the urls key -getInfluxDBUrls = () => { +function getInfluxDBUrls() { // Initialize urls data if it doesn't already exist if (localStorage.getItem(urlStorageKey) === null) { - initializeLocalStorage('urls', JSON.stringify(defaultUrlsObj)); + initializeStorageItem('urls', JSON.stringify(DEFAULT_STORAGE_URLS)); } return JSON.parse(localStorage.getItem(urlStorageKey)); -}; +} // Get the current or previous URL for a specific product or a custom url -getInfluxDBUrl = product => { +function getInfluxDBUrl(product) { // Initialize urls data if it doesn't already exist if (localStorage.getItem(urlStorageKey) === null) { - initializeLocalStorage('urls', JSON.stringify(defaultUrlsObj)); + initializeStorageItem('urls', JSON.stringify(DEFAULT_STORAGE_URLS)); } // Retrieve and parse the URLs as JSON - urlsString = localStorage.getItem(urlStorageKey); - urlsObj = JSON.parse(urlsString); + const urlsString = localStorage.getItem(urlStorageKey); + const urlsObj = JSON.parse(urlsString); // Return the URL of the specified product return urlsObj[product]; -}; +} /* Set multiple product URLs in the urls key. Input should be an object where the key is the product and the value is the URL to set for that product. */ -setInfluxDBUrls = updatedUrlsObj => { - var urlsString = localStorage.getItem(urlStorageKey); - let urlsObj = JSON.parse(urlsString); +function setInfluxDBUrls(updatedUrlsObj) { + const urlsString = localStorage.getItem(urlStorageKey); + const urlsObj = JSON.parse(urlsString); - newUrlsObj = { ...urlsObj, ...updatedUrlsObj }; + const newUrlsObj = { ...urlsObj, ...updatedUrlsObj }; localStorage.setItem(urlStorageKey, JSON.stringify(newUrlsObj)); -}; +} // Set an InfluxDB URL to an empty string in the urls key -removeInfluxDBUrl = product => { - var urlsString = localStorage.getItem(urlStorageKey); - let urlsObj = JSON.parse(urlsString); +function removeInfluxDBUrl(product) { + const urlsString = localStorage.getItem(urlStorageKey); + const urlsObj = JSON.parse(urlsString); urlsObj[product] = ''; localStorage.setItem(urlStorageKey, JSON.stringify(urlsObj)); -}; +} /* //////////////////////////////////////////////////////////////////////////////// @@ -171,24 +166,24 @@ removeInfluxDBUrl = product => { const notificationStorageKey = storagePrefix + 'notifications'; // Default notifications -var defaultNotificationsObj = { +const defaultNotificationsObj = { messages: [], callouts: [], }; -getNotifications = () => { +function getNotifications() { // Initialize notifications data if it doesn't already exist if (localStorage.getItem(notificationStorageKey) === null) { - initializeLocalStorage('notifications', JSON.stringify(defaultNotificationsObj)); + initializeStorageItem('notifications', JSON.stringify(defaultNotificationsObj)); } // Retrieve and parse the notifications data as JSON - notificationString = localStorage.getItem(notificationStorageKey); - notificationObj = JSON.parse(notificationString); + const notificationString = localStorage.getItem(notificationStorageKey); + const notificationObj = JSON.parse(notificationString); // Return the notifications object return notificationObj; -}; +} /* Checks if a notification is read. Provide the notification ID and one of the @@ -200,12 +195,12 @@ getNotifications = () => { If the notification ID exists in the array assigned to the specified type, the notification has been read. */ -notificationIsRead = (notificationID, notificationType) => { - let notificationsObj = getNotifications(); - readNotifications = notificationsObj[`${notificationType}s`]; +function notificationIsRead(notificationID, notificationType) { + const notificationsObj = getNotifications(); + const readNotifications = notificationsObj[`${notificationType}s`]; return readNotifications.includes(notificationID); -}; +} /* Sets a notification as read. Provide the notification ID and one of the @@ -216,12 +211,28 @@ notificationIsRead = (notificationID, notificationType) => { The notification ID is added to the array assigned to the specified type. */ -setNotificationAsRead = (notificationID, notificationType) => { - let notificationsObj = getNotifications(); - let readNotifications = notificationsObj[`${notificationType}s`]; +function setNotificationAsRead(notificationID, notificationType) { + const notificationsObj = getNotifications(); + const readNotifications = notificationsObj[`${notificationType}s`]; readNotifications.push(notificationID); notificationsObj[notificationType + 's'] = readNotifications; localStorage.setItem(notificationStorageKey, JSON.stringify(notificationsObj)); +} + +// Export functions as a module and make the file backwards compatible for non-module environments until all remaining dependent scripts are ported to modules +export { + defaultUrls, + initializeStorageItem, + getPreference, + setPreference, + getPreferences, + getInfluxDBUrls, + getInfluxDBUrl, + setInfluxDBUrls, + removeInfluxDBUrl, + getNotifications, + notificationIsRead, + setNotificationAsRead, }; diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 000000000..9dff11f32 --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,141 @@ +// assets/js/main.js + +// If you need to pass parameters from the calling Hugo page, you can import them here like so: +// import * as pageParams from '@params'; + +/** Import modules that are not components. + * TODO: Refactor these into single-purpose component modules. + */ +// import * as codeblocksPreferences from './api-libs.js'; +// import * as datetime from './datetime.js'; +// import * as featureCallouts from './feature-callouts.js'; +import * as apiLibs from './api-libs.js'; +import * as codeControls from './code-controls.js'; +import * as contentInteractions from './content-interactions.js'; +import { delay } from './helpers.js'; +import { InfluxDBUrl } from './influxdb-url.js'; +import * as localStorage from './local-storage.js'; +import * as modals from './modals.js'; +import * as notifications from './notifications.js'; +import * as pageContext from './page-context.js'; +import * as pageFeedback from './page-feedback.js'; +import * as tabbedContent from './tabbed-content.js'; +import * as v3Wayfinding from './v3-wayfinding.js'; +// import * as homeInteractions from './home-interactions.js'; +// import { getUrls, getReferrerHost, InfluxDBUrl } from './influxdb-url.js'; +// import * as keybindings from './keybindings.js'; +// import * as listFilters from './list-filters.js'; +// import { Modal } from './modal.js'; +// import { showNotifications } from './notifications.js'; +// import ReleaseTOC from './release-toc.js'; +// import * as scroll from './scroll.js'; +// import { TabbedContent } from './tabbed-content.js'; + +/** Import component modules + * The component pattern organizes JavaScript, CSS, and HTML for a specific UI element or interaction: + * - HTML: my-component.html + * - CSS: my-component.css + * - JavaScript: my-component.js + * The JavaScript is ideally a single-purpose module that exports a single default function to initialize the component and handle any component interactions. + */ +import AskAITrigger from './ask-ai-trigger.js'; +import { CustomTimeTrigger } from './custom-timestamps.js'; +import { SearchButton } from './search-button.js'; +import { SidebarToggle } from './sidebar-toggle.js'; +import Theme from './theme.js'; +import ThemeSwitch from './theme-switch.js'; +// import CodeControls from './code-controls.js'; +// import ContentInteractions from './content-interactions.js'; +// import CustomTimestamps from './custom-timestamps.js'; +// import Diagram from './Diagram.js'; +// import FluxGroupKeysExample from './FluxGroupKeysExample.js'; +// import FluxInfluxDBVersionsModal from './flux-influxdb-versions.js'; +// import PageFeedback from './page-feedback.js'; +// import SearchInput from './SearchInput.js'; +// import Sidebar from './Sidebar.js'; +// import V3Wayfinding from './v3-wayfinding.js'; +// import VersionSelector from './VersionSelector.js'; + +// Expose libraries and components within a namespaced object (for backwards compatibility or testing) +// Expose libraries and components within a namespaced object (for backwards compatibility or testing) + + + +document.addEventListener('DOMContentLoaded', function () { + if (typeof window.influxdatadocs === 'undefined') { + window.influxdatadocs = {}; + } + + // Expose modules to the global object for debugging, testing, and backwards compatibility for non-ES6 modules. + window.influxdatadocs.delay = delay; + window.influxdatadocs.localStorage = window.LocalStorageAPI = localStorage; + window.influxdatadocs.pageContext = pageContext; + window.influxdatadocs.toggleModal = modals.toggleModal; + + // On content loaded, initialize (not-component-ready) UI interaction modules + // To differentiate these from component-ready modules, these modules typically export an initialize function that wraps UI interactions and event listeners. + modals.initialize(); + apiLibs.initialize(); + codeControls.initialize(); + contentInteractions.initialize(); + InfluxDBUrl(); + notifications.initialize(); + pageFeedback.initialize(); + tabbedContent.initialize(); + v3Wayfinding.initialize(); + + /** Initialize components + Component Structure: Each component is structured as a jQuery anonymous function that listens for the document ready state. + Initialization in main.js: Each component is called in main.js inside a jQuery document ready function to ensure they are initialized when the document is ready. + Note: These components should *not* be called directly in the HTML. + */ + const components = document.querySelectorAll('[data-component]'); + components.forEach((component) => { + const componentName = component.getAttribute('data-component'); + switch (componentName) { + case 'ask-ai-trigger': + AskAITrigger({ component }); + window.influxdatadocs[componentName] = AskAITrigger; + break; + case 'custom-time-trigger': + CustomTimeTrigger({ component }); + window.influxdatadocs[componentName] = CustomTimeTrigger; + break; + case 'search-button': + SearchButton({ component }); + window.influxdatadocs[componentName] = SearchButton; + break; + case 'sidebar-toggle': + SidebarToggle({ component }); + window.influxdatadocs[componentName] = SidebarToggle; + break; + case 'theme': + Theme({ component }); + window.influxdatadocs[componentName] = Theme; + break; + // CodeControls(); + // ContentInteractions(); + // CustomTimestamps(); + // Diagram(); + // FluxGroupKeysExample(); + // FluxInfluxDBVersionsModal(); + // InfluxDBUrl(); + // Modal(); + // PageFeedback(); + // ReleaseTOC(); + // SearchInput(); + // showNotifications(); + // Sidebar(); + // TabbedContent(); + // ThemeSwitch({}); + // V3Wayfinding(); + // VersionSelector(); + case 'theme-switch': + ThemeSwitch({ component }); + window.influxdatadocs[componentName] = ThemeSwitch; + break; + default: + console.warn(`Unknown component: ${componentName}`); + } + }); +}); diff --git a/assets/js/modals.js b/assets/js/modals.js index 9111f1945..017a1eb2b 100644 --- a/assets/js/modals.js +++ b/assets/js/modals.js @@ -2,28 +2,43 @@ /////////////////////// General modal window interactions ////////////////////// //////////////////////////////////////////////////////////////////////////////// -// Toggle the URL selector modal window -function toggleModal(modalID="") { - if ($(".modal").hasClass("open")) { - $(".modal").fadeOut(200).removeClass("open"); - $(".modal-content").delay(400).hide(0); +import $ from 'jquery'; + +function handleModalClick() { + // Open modal window on click + $('.modal-trigger').click(function (e) { + e.preventDefault(); + toggleModal(); + }); + + // Close modal window on click + $('#modal-close, .modal-overlay').click(function (e) { + e.preventDefault(); + toggleModal(); + + // Remove modal query param ('view') if it exists + const queryParams = new URLSearchParams(window.location.search); + const anchor = window.location.hash; + + if (queryParams.get('view') !== null) { + queryParams.delete('view'); + window.history.replaceState({}, '', `${location.pathname}${anchor}`); + } + }); +} + +function toggleModal(modalID = '') { + if ($('.modal').hasClass('open')) { + $('.modal').fadeOut(200).removeClass('open'); + $('.modal-content').delay(400).hide(0); } else { - $(".modal").fadeIn(200).addClass("open"); + $('.modal').fadeIn(200).addClass('open'); $(`${modalID}.modal-content`).show(); } } -// Close modal window on click -$("#modal-close, .modal-overlay").click(function(e) { - e.preventDefault() - toggleModal() - - // Remove modal query param ('view') if it exists - const queryParams = new URLSearchParams(window.location.search); - const anchor = window.location.hash; +function initialize() { + handleModalClick(); +} - if (queryParams.get('view') !== null) { - queryParams.delete('view'); - window.history.replaceState({}, '', `${location.pathname}${anchor}`); - }; -}) \ No newline at end of file +export { initialize, toggleModal }; diff --git a/assets/js/notifications.js b/assets/js/notifications.js index 2400f9052..5c7082f3a 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -5,6 +5,8 @@ IDs in the messages array are considered read and no longer appear to the user. */ +import $ from 'jquery'; + // Get notification ID function notificationID(el) { return $(el).attr('id'); @@ -39,7 +41,10 @@ function showNotifications() { var exclude = $(this).data('exclude').split(','); var pageInScope = inScope(window.location.pathname, scope); var pageExcluded = excludePage(window.location.pathname, exclude); - var notificationRead = notificationIsRead(notificationID(this), 'message'); + var notificationRead = window.LocalStorageAPI.notificationIsRead( + notificationID(this), + 'message' + ); if (pageInScope && !pageExcluded && !notificationRead) { $(this).show().animate({ right: 0, opacity: 1 }, 200, 'swing'); @@ -53,32 +58,43 @@ function hideNotification(el) { .closest('.notification') .animate({ height: 0, opacity: 0 }, 200, 'swing', function () { $(this).hide(); - setNotificationAsRead(notificationID(this), 'message'); + window.LocalStorageAPI.setNotificationAsRead( + notificationID(this), + 'message' + ); }); } -// Show notifications on page load -showNotifications(); +function initialize() { + // Show notifications on page load + showNotifications(); -// Hide a notification and set the notification as read -$('.close-notification').click(function (e) { - e.preventDefault(); - hideNotification(this); -}); + // Hide a notification and set the notification as read + $('.close-notification').click(function (e) { + e.preventDefault(); + hideNotification(this); + }); -$('.notification .show').click(function () { - $(this).closest('.notification').toggleClass('min'); -}); + $('.notification .show').click(function () { + $(this).closest('.notification').toggleClass('min'); + }); -// Notification element scroll position -const notificationsInitialPosition = parseInt( - $('#docs-notifications').css('top'), - 10 -); -$(window).scroll(function () { - var notificationPosition = - notificationsInitialPosition - scrollY > 10 - ? notificationsInitialPosition - scrollY - : 10; - $('#docs-notifications').css('top', notificationPosition); -}); + // Notification element scroll position + const notificationsInitialPosition = parseInt( + $('#docs-notifications').css('top'), + 10 + ); + $(window).scroll(function () { + var notificationPosition = + notificationsInitialPosition - scrollY > 10 + ? notificationsInitialPosition - scrollY + : 10; + $('#docs-notifications').css('top', notificationPosition); + }); +} + +export { + initialize, + showNotifications, + hideNotification, +} \ No newline at end of file diff --git a/assets/js/page-context.js b/assets/js/page-context.js new file mode 100644 index 000000000..fdb744966 --- /dev/null +++ b/assets/js/page-context.js @@ -0,0 +1,88 @@ +/** This module retrieves browser context information and site data for the + * current page, version, and product. + */ +import { products, influxdb_urls } from '@params'; + +function getCurrentProductData() { + const path = window.location.pathname; + const mappings = [ + { pattern: /\/influxdb\/cloud\//, product: products.cloud, urls: influxdb_urls.influxdb_cloud }, + { pattern: /\/influxdb3\/core/, product: products.influxdb3_core, urls: influxdb_urls.core }, + { pattern: /\/influxdb3\/enterprise/, product: products.influxdb3_enterprise, urls: influxdb_urls.enterprise }, + { pattern: /\/influxdb3\/cloud-serverless/, product: products.influxdb3_cloud_serverless, urls: influxdb_urls.cloud }, + { pattern: /\/influxdb3\/cloud-dedicated/, product: products.influxdb3_cloud_dedicated, urls: influxdb_urls.dedicated }, + { pattern: /\/influxdb3\/clustered/, product: products.influxdb3_clustered, urls: influxdb_urls.clustered }, + { pattern: /\/enterprise_v1\//, product: products.enterprise_influxdb, urls: influxdb_urls.oss }, + { pattern: /\/influxdb.*v1\//, product: products.influxdb, urls: influxdb_urls.oss }, + { pattern: /\/influxdb.*v2\//, product: products.influxdb, urls: influxdb_urls.oss }, + { pattern: /\/kapacitor\//, product: products.kapacitor, urls: influxdb_urls.oss }, + { pattern: /\/telegraf\//, product: products.telegraf, urls: influxdb_urls.oss }, + { pattern: /\/chronograf\//, product: products.chronograf, urls: influxdb_urls.oss }, + ]; + + for (const { pattern, product, urls } of mappings) { + if (pattern.test(path)) { + return { product, urls }; + } + } + + return 'other'; +} + +// Return the page context (cloud, serverless, oss/enterprise, dedicated, clustered, other) +function getContext() { + if (/\/influxdb\/cloud\//.test(window.location.pathname)) { + return 'cloud'; + } else if (/\/influxdb3\/core/.test(window.location.pathname)) { + return 'core'; + } else if (/\/influxdb3\/enterprise/.test(window.location.pathname)) { + return 'enterprise'; + } else if (/\/influxdb3\/cloud-serverless/.test(window.location.pathname)) { + return 'serverless'; + } else if (/\/influxdb3\/cloud-dedicated/.test(window.location.pathname)) { + return 'dedicated'; + } else if (/\/influxdb3\/clustered/.test(window.location.pathname)) { + return 'clustered'; + } else if ( + /\/(enterprise_|influxdb).*\/v[1-2]\//.test(window.location.pathname) + ) { + return 'oss/enterprise'; + } else { + return 'other'; + } +} + +// Store the host value for the current page +const currentPageHost = window.location.href.match(/^(?:[^/]*\/){2}[^/]+/g)[0]; + +function getReferrerHost() { + // Extract the protocol and hostname of referrer + const referrerMatch = document.referrer.match(/^(?:[^/]*\/){2}[^/]+/g); + return referrerMatch ? referrerMatch[0] : ''; +} + +const context = getContext(), + host = currentPageHost, + hostname = location.hostname, + path = location.pathname, + pathArr = location.pathname.split('/').slice(1, -1), + product = pathArr[0], + productData = getCurrentProductData(), + protocol = location.protocol, + referrer = document.referrer === '' ? 'direct' : document.referrer, + referrerHost = getReferrerHost(), + // TODO: Verify this still does what we want since the addition of InfluxDB 3 naming and the Core and Enterprise versions. + version = (/^v\d/.test(pathArr[1]) || pathArr[1]?.includes('cloud') ? pathArr[1].replace(/^v/, '') : "n/a") + +export { + context, + host, + hostname, + path, + product, + productData, + protocol, + referrer, + referrerHost, + version, +}; \ No newline at end of file diff --git a/assets/js/page-feedback.js b/assets/js/page-feedback.js index af2ea0cd2..eab4ebaa3 100644 --- a/assets/js/page-feedback.js +++ b/assets/js/page-feedback.js @@ -3,52 +3,58 @@ * buttons and modal. */ -// Collect data from the page path -const pathArr = location.pathname.split('/').slice(1, -1) -const pageData = { - host: location.hostname, - path: location.pathname, - product: pathArr[0], - version: (/^v\d/.test(pathArr[1]) || pathArr[1]?.includes('cloud') ? pathArr[1].replace(/^v/, '') : "n/a"), -} +import $ from 'jquery'; +import { hostname, path, product, protocol, version } from './page-context.js'; +import { toggleModal } from './modals.js'; // Hijack form submission and send feedback data to be stored. // Called by onSubmit in each feedback form. function submitFeedbackForm(formID) { - // Collect form data, structure as an object, and remove fname honeypot const formData = new FormData(document.forms[formID]); const formDataObj = Object.fromEntries(formData.entries()); - const {fname, ...feedbackData} = formDataObj; - + const { ...feedbackData } = formDataObj; + // Build lp fields from form data - let fields = ""; + let fields = ''; for (let key in feedbackData) { // Strip out newlines and escape double quotes if the field key is "feedback" - if (key == "feedback-text") { - fields += key + '="' + feedbackData[key].replace(/(\r\n|\n+|\r+)/gm, " ").replace(/(\")/gm, '\\"') + '",'; + if (key == 'feedback-text') { + fields += + key + + '="' + + feedbackData[key] + .replace(/(\r\n|\n+|\r+)/gm, ' ') + .replace(/(\")/gm, '\\"') + + '",'; } else { - fields += key + "=" + feedbackData[key] + ","; - } + fields += key + '=' + feedbackData[key] + ','; + } } - fields = fields.substring(0, fields.length -1); + fields = fields.substring(0, fields.length - 1); // Build lp using page data and the fields string - const lp = `feedback,host=${pageData.host},path=${pageData.path},product=${pageData.product},version=${pageData.version} ${fields}` + const lp = `feedback,host=${hostname},path=${path},product=${product},version=${version} ${fields}`; // Use a honeypot form field to detect a bot // If the value of the honeypot field is greater than 0, the submitter is a bot function isBot() { const honeypot = formData.get('fname'); - return (honeypot.length > 0) + return honeypot.length > 0; } - + // If the submitter is not a bot, send the feedback data if (!isBot()) { - xhr = new XMLHttpRequest(); - xhr.open('POST', 'https://j32dswat7l.execute-api.us-east-1.amazonaws.com/prod'); + const xhr = new XMLHttpRequest(); + xhr.open( + 'POST', + 'https://j32dswat7l.execute-api.us-east-1.amazonaws.com/prod' + ); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - xhr.setRequestHeader('Access-Control-Allow-Origin', `${location.protocol}//${location.host}`); + xhr.setRequestHeader( + 'Access-Control-Allow-Origin', + `${protocol}//${location.host}` + ); xhr.setRequestHeader('Content-Type', 'text/plain; charset=utf-8'); xhr.setRequestHeader('Accept', 'application/json'); xhr.send(lp); @@ -70,28 +76,43 @@ function submitLifeCycle() { // Called by onclick in the page-feedback modal submit button. function submitLifeCycleAndClose() { submitFeedbackForm('pagefeedbacktext'); - $('.modal #page-feedback .loader-wrapper').css('display', 'flex').hide().fadeIn(200); - $('.modal #page-feedback #thank-you').css('display', 'flex').hide().delay(800).fadeIn(200); - $('.modal #page-feedback textarea').css('box-shadow', 'none') + $('.modal #page-feedback .loader-wrapper') + .css('display', 'flex') + .hide() + .fadeIn(200); + $('.modal #page-feedback #thank-you') + .css('display', 'flex') + .hide() + .delay(800) + .fadeIn(200); + $('.modal #page-feedback textarea').css('box-shadow', 'none'); $('.modal #page-feedback .loader-wrapper').delay(1000).hide(0); - setTimeout(function() {toggleModal()}, 1800); + setTimeout(function () { + toggleModal(); + }, 1800); return false; } -//////////////////////////////// Event triggers //////////////////////////////// +function initialize() { + //////////////////////////////// Event triggers //////////////////////////////// -// Submit page feedback (yes/no) on radio select and trigger life cycle -$('#pagefeedback input[type=radio]').change(function() { - $('form#pagefeedback').submit(); - submitLifeCycle() -}) + // Submit page feedback (yes/no) on radio select and trigger life cycle + $('#pagefeedback input[type=radio]').change(function () { + $('form#pagefeedback').submit(); + submitLifeCycle(); + }); -// Toggle the feedback modal when user selects that the page is not helpful -$('#pagefeedback #not-helpful input[type=radio]').click(function() { - setTimeout(function() {toggleModal('#page-feedback')}, 400); -}) + // Toggle the feedback modal when user selects that the page is not helpful + $('#pagefeedback #not-helpful input[type=radio]').click(function () { + setTimeout(function () { + toggleModal('#page-feedback'); + }, 400); + }); -// Toggle the feedback modal when user selects that the page is not helpful -$('.modal #no-thanks').click(function() { - toggleModal(); -}) \ No newline at end of file + // Toggle the feedback modal when user selects that the page is not helpful + $('.modal #no-thanks').click(function () { + toggleModal(); + }); +} + +export { initialize }; diff --git a/assets/js/search-button.js b/assets/js/search-button.js new file mode 100644 index 000000000..139e12474 --- /dev/null +++ b/assets/js/search-button.js @@ -0,0 +1,10 @@ +import { toggleSidebar } from './sidebar-toggle.js'; + +export function SearchButton({ component }) { + component.querySelector('[data-action="toggle"]') + .addEventListener('click', () => { + toggleSidebar('sidebar-open'); + document.getElementById('algolia-search-input').focus(); + return false; + }); +} \ No newline at end of file diff --git a/assets/js/sidebar-toggle.js b/assets/js/sidebar-toggle.js index 2c20b3ddf..4db64db79 100644 --- a/assets/js/sidebar-toggle.js +++ b/assets/js/sidebar-toggle.js @@ -1,8 +1,10 @@ /* - Copied and pasted this script for CSS swaps w/ cookies from + Portions of this code come from CSS swaps w/ cookies from http://www.thesitewizard.com/javascripts/change-style-sheets.shtml */ +import * as localStorage from './local-storage.js'; + // *** TO BE CUSTOMISED *** var sidebar_state_preference_name = 'sidebar_state'; var sidebar_state_duration = 30; @@ -11,7 +13,7 @@ var style_domain = 'docs.influxdata.com'; // *** END OF CUSTOMISABLE SECTION *** // You do not need to customise anything below this line -function toggleSidebar (toggle_state) { +function toggleSidebar(toggle_state) { // You may use this script on your site free of charge provided // you do not remove this notice or the URL below. Script from // http://www.thesitewizard.com/javascripts/change-style-sheets.shtml @@ -30,16 +32,30 @@ function toggleSidebar (toggle_state) { link_tag[i].disabled = false; } } - setPreference( + localStorage.setPreference( sidebar_state_preference_name, toggle_state.replace(/sidebar-/, '') ); } } -function setSidebarState () { - var toggle_state = `sidebar-${getPreference(sidebar_state_preference_name)}`; +function setSidebarState() { + var toggle_state = `sidebar-${localStorage.getPreference(sidebar_state_preference_name)}`; if (toggle_state !== undefined) { toggleSidebar(toggle_state); } } + +function SidebarToggle({ component }) { + const current_state = component.getAttribute('data-state'); + component + .querySelector('[data-action="toggle"]') + .addEventListener('click', () => { + toggleSidebar(`sidebar-${current_state}`); + return false; + }); + + setSidebarState(); +} + +export { setSidebarState, toggleSidebar, SidebarToggle }; diff --git a/assets/js/tabbed-content.js b/assets/js/tabbed-content.js index 536301828..3d00ddd92 100644 --- a/assets/js/tabbed-content.js +++ b/assets/js/tabbed-content.js @@ -5,7 +5,10 @@ * smoothscroll when clicked. The whitelist is defined in content-interactions.js. **/ -function tabbedContent (container, tab, content) { +import $ from 'jquery'; +import { scrollToAnchor } from './content-interactions.js'; + +function tabbedContent(container, tab, content) { // Add the active class to the first tab in each tab group, // in case it wasn't already set in the markup. $(container).each(function () { @@ -30,10 +33,7 @@ function tabbedContent (container, tab, content) { }); } -tabbedContent('.code-tabs-wrapper', '.code-tabs p a', '.code-tab-content'); -tabbedContent('.tabs-wrapper', '.tabs p a', '.tab-content'); - -function getTabQueryParam () { +function getTabQueryParam() { const queryParams = new URLSearchParams(window.location.search); return $('