| // Licensed under the Apache License, Version 2.0 (the 'License'); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| $(document).ready(function() { |
| function getElementData(event) { |
| const clickedLangSwitchEl = event.target; |
| const elPreviousOffsetFromViewPort = clickedLangSwitchEl.getBoundingClientRect().top; |
| return { |
| elPreviousOffsetFromViewPort, |
| clickedLangSwitchEl, |
| } |
| } |
| |
| function setScrollToNewPosition(clickedElementData) { |
| const { elPreviousOffsetFromViewPort, clickedLangSwitchEl } = clickedElementData; |
| const elCurrentHeight = window.pageYOffset + clickedLangSwitchEl.getBoundingClientRect().top; |
| $('html, body').scrollTop(elCurrentHeight - elPreviousOffsetFromViewPort); |
| } |
| |
| const loadTime = new Date(); |
| |
| function Switcher(conf) { |
| const name = conf["name"]; |
| |
| return { |
| ...conf, |
| "uniqueValues": new Set(), |
| "selector": `[class^=${name}-]:not(.no-toggle)`, // Ex: [class^=language-]:not(.no-toggle) |
| "wrapper": `${name}-switcher`, // Parent wrapper-class. |
| "localStorageKey": name, |
| |
| /** |
| * @desc Generate bootstrapped like nav template, |
| showing supported types in nav. |
| * @param array $types - list of supported types. |
| * @return string - html template, which is bootstrapped nav tabs. |
| */ |
| "navHtml": function (values) { |
| let lists = ""; |
| let classes = ""; |
| |
| for (const value of values) { |
| const title = this.valueToTabTitle(value); |
| classes += ` ${name}-${value}`; |
| lists += `<li class="langSwitch-content" data-value="${value}">${title}</li>`; |
| } |
| |
| // Ex: language-switcher language-java language-py language-go |
| return `<div class="${this.wrapper + classes}"><ul class="nav nav-tabs">${lists}</ul></div>`; |
| }, |
| |
| "valueToTabTitle": function (value) { |
| switch (value) { |
| case 'py': return 'Python'; |
| case 'sql': return 'SQL'; |
| case 'scio': return 'SCIO'; |
| case 'typescript': return 'TypeScript'; |
| } |
| |
| return value.charAt(0).toUpperCase() + value.slice(1); |
| }, |
| |
| /** |
| * @desc Add Navigation tabs on top of parent code blocks. |
| */ |
| "addTabs": function() { |
| var _self = this; |
| |
| // Iterate over all content blocks to insert tabs before. |
| // If multiple content blocks go in a row, only insert tabs before the first one. |
| $("div" + _self.selector).each(function() { // Ex: div[class^=language-]:not(.no-toggle) |
| if ($(this).prev().is("div" + _self.selector)) { |
| return; // Not the first one. |
| } |
| |
| const values = _self.findNextSiblingsValues($(this)); |
| for (const value of values) { |
| _self.uniqueValues.add(value); |
| } |
| |
| const tabsHtml = _self.navHtml(values); |
| $(this).before(tabsHtml); |
| |
| // Add 'language-XXX' classes to the Playground iframe so it can be hidden |
| // when selecting a language it does not have. |
| let iframeWrapper = $(this).parent().parent().children('.code-snippet-playground'); |
| if (iframeWrapper.length > 0) { |
| for (const value of values) { |
| iframeWrapper.addClass(`${name}-${value}`) |
| } |
| } |
| }); |
| }, |
| |
| /** |
| * @desc Search next sibling and if it's also a code block, then store |
| its type and move on to the next element. It will keep |
| looking until there is no direct code block descendant left. |
| * @param object $el - jQuery object, from where to start searching. |
| * @param array $lang - list to hold types, found while searching. |
| * @return array - list of types found. |
| */ |
| "findNextSiblingsValues": function(el, lang) { |
| if (!el.is("div" + this.selector)) { |
| return []; |
| } |
| |
| const prefix = `${name}-`; |
| for (const cls of el[0].classList) { |
| if (cls.startsWith(prefix)) { |
| return [ |
| cls.replace(prefix, ""), |
| ...this.findNextSiblingsValues(el.next()), |
| ]; |
| } |
| } |
| |
| return []; |
| }, |
| |
| "bindEvents": function() { |
| var _self = this; |
| $(`.${_self.wrapper} li`).click(function(event) { |
| localStorage.setItem(_self.localStorageKey, $(this).data("value")); |
| |
| // Set scroll to new position because Safari and Firefox |
| // can't do it automatically, only Chrome under the hood |
| // detects the correct position of viewport |
| const clickedLangSwitchData = getElementData(event); |
| _self.toggle(false); |
| setScrollToNewPosition(clickedLangSwitchData); |
| }); |
| }, |
| |
| "toggle": function(isInitial) { |
| let value = localStorage.getItem(this.localStorageKey) || this.default; |
| let hasTabForValue = $(`.${this.wrapper} li[data-value="${value}"]`).length > 0; // Ex: .language-switcher li[data-value="java"] |
| |
| if (!hasTabForValue) { |
| // if there's a code block for the default language, |
| // set the preferred language to the default language |
| if (this.uniqueValues.has(this.default)) { |
| value = this.default; |
| } else { |
| // otherwise set the preferred language to the first available |
| // language, so we don't have a page with no code blocks |
| value = [...this.uniqueValues][0]; |
| } |
| } |
| |
| $(`.${this.wrapper} li[data-value="${value}"]`).addClass("active"); |
| $(`.${this.wrapper} li[data-value!="${value}"]`).removeClass("active"); |
| |
| // Hiding all sets of tabs and all playground iframes. |
| // Then showing those having a tab for the new language. |
| $(this.selector).hide(); // Ex: [class^=language-]:not(.no-toggle) |
| if (name === 'language') { |
| $('.code-snippet-playground').hide(); |
| } |
| $(`.${name}-${value}`).show(); |
| |
| // Showing the main switch at the top of the page. |
| $("nav" + this.selector).show(); |
| |
| // make sure that runner and shell snippets are still visible after changing language |
| $("code" + this.selector).show(); |
| |
| //add refresh method because html elements are added/deleted after changing language |
| $('[data-spy="scroll"]').each(function () { |
| $(this).scrollspy('refresh'); |
| }); |
| |
| if (this.onChanged) { |
| this.onChanged(value, isInitial); |
| } |
| }, |
| "render": function(wrapper) { |
| this.addTabs(); |
| this.bindEvents(); |
| this.toggle(true); |
| }, |
| }; |
| } |
| |
| Switcher({ |
| "name": "language", |
| "default": "java", |
| |
| "onChanged": function (lang, isInitial) { |
| if (isInitial) { |
| this.onInit(lang); |
| } else { |
| this.onChangedAfterLoaded(lang); |
| } |
| }, |
| |
| /** |
| * @desc Called after language is determined for the first time. |
| * Modifies the iframes' URLs by adding the language so we don't need to |
| * send messages to them. This is cheap when the page only started loading. |
| */ |
| "onInit": function (lang) { |
| const playgroundIframeContainers = document.getElementsByClassName("code-snippet-playground"); |
| const sdk = this.langToSdk(lang); |
| |
| for (const div of playgroundIframeContainers) { |
| const src = div.dataset.src; |
| const width = div.dataset.width; |
| const height = div.dataset.height; |
| |
| const url = new URL(src); |
| const searchParams = new URLSearchParams(url.search); |
| searchParams.set("sdk", sdk); |
| url.search = searchParams.toString(); |
| |
| const iframe = document.createElement('iframe'); |
| iframe.src = url.href; |
| iframe.width = width; |
| iframe.height = height; |
| iframe.className = 'playground'; |
| iframe.allow = 'clipboard-write'; |
| |
| div.appendChild(iframe); |
| } |
| }, |
| |
| /** |
| * @desc Called when the user switched the language tab manually. |
| */ |
| "onChangedAfterLoaded": function (lang) { |
| const playgroundIframes = $(".code-snippet-playground iframe").get(); |
| const message = { |
| type: "SetSdk", |
| sdk: this.langToSdk(lang), |
| }; |
| |
| const _self = this; |
| let attempts = 30; |
| |
| // If another cycle of sending these messages is running, stop it. |
| clearInterval(this.interval); |
| |
| const sendMessage = function () { |
| for (const iframe of playgroundIframes) { |
| iframe.contentWindow.postMessage(message, '*'); |
| } |
| |
| if (attempts-- === 0) { |
| clearInterval(_self.interval); |
| } |
| }; |
| |
| if (!this.areFramesLoaded()) { |
| // The guess is that the iframes may not have loaded yet. |
| // If we just send a message, Flutter may have not yet set its listener. |
| // So send the message with intervals in hope some of them are received. |
| // Playground ignores duplicate messages in a row. |
| this.interval = setInterval(sendMessage, 1000); |
| } |
| |
| sendMessage(); |
| }, |
| |
| "langToSdk": function (lang) { |
| switch (lang) { |
| case "py": return "python"; |
| } |
| return lang; |
| }, |
| |
| /** |
| * @desc A rough guess if the embedded iframes are loaded, timer-based. |
| * Experiments hint that ~10 seconds is sufficient on slowest devices. |
| */ |
| "areFramesLoaded": function () { |
| const millisecondsAgo = new Date() - loadTime; |
| return millisecondsAgo >= 30000; |
| }, |
| }).render(); |
| |
| Switcher({"name": "runner", "default": "direct"}).render(); |
| Switcher({"name": "tab"}).render(); |
| Switcher({"name": "shell", "default": "unix"}).render(); |
| Switcher({"name": "version"}).render(); |
| }); |