blob: ff6fdb0253a8e9d64e64ca81f6928eb454de2a72 [file]
// 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();
});