pxt-core
Version:
Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors
426 lines (381 loc) • 15.3 kB
JavaScript
function handleEnterKey(e) {
var charCode = (typeof e.which == "number") ? e.which : e.keyCode
if (charCode === 13 || charCode === 32) { // Enter or Space key
e.preventDefault();
e.currentTarget.click();
}
}
function describePlural(value, unit) {
return value + " " + unit + (value == 1 ? "" : "s")
}
function describetime(now, other) {
var seconds = now - other
if (isNaN(seconds)) return ""
var timeString
if (seconds < 0)
return "now"
else if (seconds < 10)
return "a few seconds ago"
else if (seconds < 60)
return " " + describePlural(Math.floor(seconds), "second") + " ago"
else if (seconds < 2 * 60)
return "a minute ago"
else if (seconds < 60 * 60)
return " " + describePlural(Math.floor(seconds / 60), "minute") + " ago"
else if (seconds < 2 * 60 * 60)
return "an hour ago";
else if (seconds < 60 * 60 * 24)
return " " + describePlural(Math.floor(seconds / 60 / 60), "hour") + " ago"
else if (seconds < 60 * 60 * 24 * 30)
return " " + describePlural(Math.floor(seconds / 60 / 60 / 24), "day") + " ago"
else if (seconds < 60 * 60 * 24 * 365)
return " " + describePlural(Math.floor(seconds / 60 / 60 / 24 / 30), "month") + " ago"
else
return " " + describePlural(Math.floor(seconds / 60 / 60 / 24 / 365), "year") + " ago"
}
function isIE() {
return /trident/i.test(navigator.userAgent);
}
function dirAuto($el) {
if ($el) {
if (!isIE())
$el.attr('dir', 'auto');
else {
var dir = /^[\s\.;:(+0-9]*[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/i.test($el.text()) ? "rtl" : "ltr";
$el.attr('dir', dir);
}
}
return $el;
}
function searchSubmit(form) {
if (pxt && pxt.tickEvent) pxt.tickEvent("docs.search", { 'source': form.id }, { interactiveConsent: true })
}
function scrollActiveHeaderIntoView() {
var activeHeaders = document.getElementsByClassName("header active");
for (var i = 0; i < activeHeaders.length; ++i) {
var activeHeader = activeHeaders.item(i);
if (activeHeader.scrollIntoView)
activeHeader.scrollIntoView()
}
}
function setupSidebar() {
// do not use pxt.appTarget in this function
$('#togglesidebar').on('keydown', handleEnterKey);
$('.ui.sidebar')
.sidebar({
dimPage: false,
onShow: function () {
togglesidebar.setAttribute("aria-expanded", "true");
$(".sidebar .focused").focus();
scrollActiveHeaderIntoView();
},
onHidden: function () {
togglesidebar.setAttribute("aria-expanded", "false");
}
})
.sidebar(
'attach events', '#togglesidebar'
);
$('.ui.dropdown')
.dropdown();
$('.ui.accordion')
.accordion({
closeNested: true,
duration: 50,
selector: {
trigger: '> .title'
}
});
var accordions = document.getElementsByClassName("ui accordion");
for (var i = 0; i < accordions.length; i++) {
var nodes = accordions.item(i).getElementsByClassName("title");
for (var j = 0; j < nodes.length; j++) {
var menuItem = nodes.item(j);
var hrefNode = menuItem.getElementsByTagName("a").item(0);
var iNode = menuItem.getElementsByTagName("i").item(0);
iNode.onclick = function (e) {
if (hrefNode.hasAttribute("aria-expanded") && hrefNode.getAttribute("aria-expanded") === "true") {
hrefNode.setAttribute("aria-expanded", "false");
} else {
hrefNode.setAttribute("aria-expanded", "true");
}
};
if (!hrefNode) {
hrefNode = menuItem;
menuItem.setAttribute("tabindex", "0");
}
hrefNode.onkeydown = function (e) {
var charCode = (typeof e.which == "number") ? e.which : e.keyCode
if (charCode === 39) { // Right key
$(e.target.parentElement.parentElement).accordion("open", 0);
e.target.setAttribute("aria-expanded", "true");
} else if (charCode === 37) { // Left key
$(e.target.parentElement.parentElement).accordion("close", 0);
e.target.setAttribute("aria-expanded", "false");
}
};
}
}
var searchIcons = document.getElementsByClassName("search link icon");
for (var i = 0; i < searchIcons.length; i++) {
searchIcons.item(i).onkeydown = handleEnterKey;
}
}
function setupSemantic() {
// do not use pxt.appTarget in this function
// don't show related videos
$.fn.embed.settings.sources.youtube.url = '//www.youtube.com/embed/{id}?rel=0'
//This is an adapted version of the original template code in Semantic UI
$.fn.embed.settings.templates.placeholder = function (image, icon) {
var html = '';
if (icon) {
html += '<i class="' + icon.replace(/[^\w ]*/g, '') + ' icon"></i>';
}
if (image) {
//Remove the timestamp from the YouTube source URL
image = image.replace(/\#t=([0-9]+m)?([0-9]+s)?/, "");
html += `<div class="placeholder" style="
background-image: url(${encodeURI(image)});
background-size: cover;
background-position: 50% 50%;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
"></div>`
}
return html;
};
//Again, this is a slightly modified version of the original Semantic UI source to support timestamped YouTube videos
$.fn.embed.settings.templates.iframe = function (url, parameters) {
var src = url;
//The following if statement is all that is different from the original implementation
var matches = src.match(/\#t=(([0-9]+)m)?(([0-9]+)s)?/);
if (matches) {
var minutes = matches[2] != undefined ? parseInt(matches[2]) : 0;
var seconds = matches[4] != undefined ? parseInt(matches[4]) : 0;
var param = "start=" + (minutes * 60 + seconds).toString();
if (parameters) {
parameters = param + "&" + parameters;
}
else {
parameters = param;
}
src = src.replace(/\#t=([0-9]+m)?([0-9]+s)?/, "");
}
if (parameters) {
src += (/\?/.test(url) ? '&' : '?') + parameters;
}
return ''
+ '<iframe src="' + encodeURI(src) + '"'
+ ' width="100%" height="100%"'
+ ' frameborder="0" scrolling="no" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>'
;
};
$('.ui.embed').embed();
$('.md-video-link').on("click", function () {
var name = $(this).data("playerurl") || $(this).data("videosrc");
$(this).find("img").remove();
$(this).find("svg").remove();
var outer = $('<div />', {
"class": 'embed-responsive embed-responsive-16by9'
});
outer.appendTo($(this));
$('<iframe>', {
class: 'embed-responsive-item',
src: name,
frameborder: 0,
scrolling: 'no'
}).appendTo(outer);
});
$('#printbtn').on("click", function () {
window.print();
})
$('#translatebtn').on("click", function () {
window.location.href = window.location.href.replace(/#.*/, '') + (window.location.href.indexOf('?') > -1 ? "&" : "?") + "translate=1"
})
if (/browsers$/i.test(window.location.href))
$('.ui.footer').append($('<div class="ui center aligned small container"/>').text('user agent: ' + navigator.userAgent))
}
function setupBlocklyAsync() {
var promise = Promise.resolve();
if (pxt.appTarget.appTheme && pxt.appTarget.appTheme.extendFieldEditors) {
var opts = {};
promise = promise.then(function () {
return pxt.BrowserUtils.loadScriptAsync("fieldeditors.js")
}).then(function () {
return pxt.editor.initFieldExtensionsAsync(opts)
}).then(function (res) {
if (res.fieldEditors)
res.fieldEditors.forEach(function (fi) {
pxt.blocks.registerFieldEditor(fi.selector, fi.editor, fi.validator);
})
})
}
// backward compatibility: load editor
if (pxt.appTarget.versions &&
pxt.semver.strcmp(pxt.appTarget.versions.pxt, "3.9.0") < 0 &&
pxt.appTarget.appTheme && pxt.appTarget.appTheme.extendEditor) {
var opts = {};
promise = promise.then(function () {
return pxt.BrowserUtils.loadScriptAsync(pxt.webConfig.commitCdnUrl + "editor.js")
}).then(function () {
return pxt.editor.initExtensionsAsync(opts)
}).then(function (res) {
if (res.fieldEditors)
res.fieldEditors.forEach(function (fi) {
pxt.blocks.registerFieldEditor(fi.selector, fi.editor, fi.validator);
})
})
}
return promise;
}
function renderSnippets() {
if (typeof ksRunnerReady === "undefined") return; // probably in pxt docs
var path = window.location.href.split('/').pop().split(/[?#]/)[0];
ksRunnerReady(function () {
setupBlocklyAsync()
.then(function () {
const options = pxt.runner.defaultClientRenderOptions();
options.snippetReplaceParent = true;
options.simulator = true;
options.showEdit = true;
options.hex = true;
options.hexName = path;
return pxt.runner.renderAsync(options);
});
});
}
function languageOption(code) {
var locale = pxt.Util.allLanguages[code];
var headerEl = document.createElement('div');
headerEl.className = 'header';
headerEl.textContent = locale.localizedName;
var descriptionEl = document.createElement('div');
descriptionEl.className = 'description tall';
descriptionEl.textContent = locale.englishName;
var contentEl = document.createElement('div');
contentEl.className = 'content';
contentEl.appendChild(headerEl);
contentEl.appendChild(descriptionEl);
var cardEl = document.createElement('div');
cardEl.className = 'ui card link card-selected langoption';
cardEl.dataset.lang = code;
cardEl.setAttribute('role', 'option');
cardEl.setAttribute('aria-label', locale.englishName);
cardEl.setAttribute('tabindex', '0');
cardEl.appendChild(contentEl);
return cardEl;
}
function setupLangPicker() {
if (typeof ksRunnerReady === "undefined") {
// probably in pxt docs
removeLangPicker();
return;
}
ksRunnerReady(function () {
buildLangPicker();
});
}
function buildLangPicker() {
var appTheme = pxt && pxt.appTarget && pxt.appTarget.appTheme;
if (appTheme && appTheme.availableLocales && appTheme.selectLanguage) {
var modalContainer = document.querySelector("#langmodal");
var initialLang = pxt && pxt.Util.normalizeLanguageCode(pxt.BrowserUtils.getCookieLang())[0];
var localesContainer = document.querySelector("#availablelocales");
if (!localesContainer || !modalContainer) {
var langPicker = document.querySelector('#langpicker');
if (langPicker) langPicker.remove();
return;
}
appTheme.availableLocales.forEach(function(locale) {
var card = languageOption(locale);
localesContainer.appendChild(card);
});
/**
* In addition to normal focus trap, need to explicitly hide these
* elements when the modal is open so screen readers do not allow user
* to escape modal while in scan mode.
*
* Ideally, aria-modal && role="dialog" should make the screen reader
* handle this (https://www.w3.org/TR/wai-aria-1.1/#aria-modal)
* but none seem to :)
**/
var identifiersToHide = [
"#docs",
"#top-bar",
"#side-menu"
];
modalContainer.className += ` ${appTheme.availableLocales.length > 4 ? "large" : "small"}`;
$(modalContainer).modal({
onShow: function() {
for (var id of identifiersToHide) {
var divToHide = document.querySelector(id);
if (divToHide)
divToHide.setAttribute("aria-hidden", "true");
}
$(document).off("focusin.focusJail");
$(document).on("focusin.focusJail", function(event) {
if (event.target !== modalContainer && !$.contains(modalContainer, event.target)) {
modalContainer.focus();
}
});
},
onHide: function() {
for (var id of identifiersToHide) {
var divToHide = document.querySelector(id);
if (divToHide)
divToHide.removeAttribute("aria-hidden");
}
$(document).off("focusin.focusJail");
}
});
var langPicker = document.querySelector("#langpicker");
langPicker.onclick = function() {
$(modalContainer).modal('show');
}
langPicker.onkeydown = handleEnterKey;
var closeIcon = modalContainer.querySelector(".closeIcon");
closeIcon.onclick = function() {
$(modalContainer).modal('hide');
}
closeIcon.onkeydown = handleEnterKey;
var buttons = modalContainer.querySelectorAll(".ui.button");
for (var i = 0; i < buttons.length; i++) {
buttons[i].onkeydown = handleEnterKey;
}
var langOptions = modalContainer.querySelectorAll(".langoption");
for (var i = 0; i < langOptions.length; i++) {
var currentOption = langOptions[i];
currentOption.onclick = function(e) {
var langId = e.currentTarget.dataset.lang;
if (!pxt.Util.allLanguages[langId]) {
return;
}
pxt.BrowserUtils.setCookieLang(langId, /** docs **/ true);
if (langId !== initialLang) {
pxt.tickEvent("menu.lang.changelang", { lang: langId, docs: "true" });
location.reload();
} else {
pxt.tickEvent(`menu.lang.samelang`, { lang: langId, docs: "true" });
$('.ui.modal').modal('hide');
}
}
currentOption.onkeydown = handleEnterKey;
}
} else {
// remove the locale picker and modal if unavailable in this editor
removeLangPicker();
}
}
function removeLangPicker() {
document.querySelector("#langpicker").remove();
document.querySelector("#langmodal").remove();
}
$(document).ready(function () {
setupSidebar();
setupSemantic();
renderSnippets();
setupLangPicker();
});