Pull request / build (pull_request) Successful in 47s
A bilingual (EN/RU) static page in site/, split into index.html, styles.css, boot.js and main.js, with a favicon.svg. Documents placement modes, per-mode look, the Caps Lock shortcut and a privacy policy — the last is meant to double as the Partner Center privacy policy URL. STORE_URL is still a placeholder in both download buttons until the Store listing exists.
297 lines
9.8 KiB
JavaScript
297 lines
9.8 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
/* ------------------------------------------------------ small helpers */
|
|
|
|
var root = document.documentElement;
|
|
|
|
function remember(key, value) {
|
|
try { localStorage.setItem(key, value); } catch (error) { /* private mode: never mind */ }
|
|
}
|
|
|
|
/* ------------------------------------------------------- the language */
|
|
|
|
var description = document.querySelector('meta[name="description"]');
|
|
var TITLE_EN = document.title;
|
|
var TITLE_RU = "CursorLang — раскладка клавиатуры у курсора";
|
|
var DESCRIPTION_EN = description.getAttribute("content");
|
|
var DESCRIPTION_RU = description.getAttribute("data-content-ru");
|
|
|
|
function applyLanguage(lang) {
|
|
if (lang !== "ru") { lang = "en"; }
|
|
|
|
root.setAttribute("data-lang", lang);
|
|
root.setAttribute("lang", lang);
|
|
document.title = lang === "ru" ? TITLE_RU : TITLE_EN;
|
|
description.setAttribute("content", lang === "ru" ? DESCRIPTION_RU : DESCRIPTION_EN);
|
|
|
|
var labelled = document.querySelectorAll("[data-label-en]");
|
|
for (var i = 0; i < labelled.length; i++) {
|
|
labelled[i].setAttribute("aria-label", labelled[i].getAttribute("data-label-" + lang));
|
|
}
|
|
|
|
var buttons = document.querySelectorAll("[data-set-lang]");
|
|
for (var j = 0; j < buttons.length; j++) {
|
|
buttons[j].setAttribute("aria-pressed",
|
|
buttons[j].getAttribute("data-set-lang") === lang ? "true" : "false");
|
|
}
|
|
|
|
applyLook();
|
|
}
|
|
|
|
document.addEventListener("click", function (event) {
|
|
var button = event.target.closest("[data-set-lang]");
|
|
if (!button) { return; }
|
|
|
|
var lang = button.getAttribute("data-set-lang");
|
|
applyLanguage(lang);
|
|
remember("cursorlang.lang", lang);
|
|
});
|
|
|
|
/* ---------------------------------------------------------- the theme */
|
|
|
|
document.getElementById("themeBtn").addEventListener("click", function () {
|
|
var pinned = root.getAttribute("data-theme");
|
|
var dark = pinned
|
|
? pinned === "dark"
|
|
: window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
|
|
var next = dark ? "light" : "dark";
|
|
root.setAttribute("data-theme", next);
|
|
remember("cursorlang.theme", next);
|
|
});
|
|
|
|
/* ----------------------------------------------------- the menu button */
|
|
|
|
var header = document.querySelector("header");
|
|
var menuBtn = document.getElementById("menuBtn");
|
|
|
|
function setMenu(open) {
|
|
header.setAttribute("data-menu", open ? "open" : "closed");
|
|
menuBtn.setAttribute("aria-expanded", open ? "true" : "false");
|
|
}
|
|
|
|
menuBtn.addEventListener("click", function () {
|
|
setMenu(header.getAttribute("data-menu") !== "open");
|
|
});
|
|
|
|
document.addEventListener("click", function (event) {
|
|
if (header.getAttribute("data-menu") !== "open") { return; }
|
|
|
|
if (event.target.closest("nav.links a") || !event.target.closest("header")) {
|
|
setMenu(false);
|
|
}
|
|
});
|
|
|
|
document.addEventListener("keydown", function (event) {
|
|
if (event.key !== "Escape" || header.getAttribute("data-menu") !== "open") { return; }
|
|
|
|
setMenu(false);
|
|
menuBtn.focus();
|
|
});
|
|
|
|
window.matchMedia("(min-width: 901px)").addEventListener("change", function (event) {
|
|
if (event.matches) { setMenu(false); }
|
|
});
|
|
|
|
// Back to top, without leaving a fragment behind in the address bar
|
|
document.addEventListener("click", function (event) {
|
|
var back = event.target.closest('a[href="#top"]');
|
|
if (!back) { return; }
|
|
|
|
event.preventDefault();
|
|
window.scrollTo({ top: 0 });
|
|
history.replaceState(null, "", location.pathname + location.search);
|
|
});
|
|
|
|
/* ------------------------------------------- the border under the bar */
|
|
|
|
var onScroll = function () {
|
|
header.classList.toggle("scrolled", window.scrollY > 4);
|
|
};
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
onScroll();
|
|
|
|
/* ------------------------------------------------------- the demo above */
|
|
|
|
var demoText = document.getElementById("demoText");
|
|
var demoChip = document.getElementById("demoChip");
|
|
var indicator = document.getElementById("demoIndicator");
|
|
|
|
// A sentence typed in two layouts, the way one is
|
|
var SCRIPT = [
|
|
{ layout: "EN", indicator: "ENG", text: "Deploy is green, " },
|
|
{ layout: "RU", indicator: "РУС", text: "можно мержить" }
|
|
];
|
|
|
|
var stillMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
var timers = [];
|
|
var typing = false;
|
|
var onScreen = true;
|
|
|
|
function later(fn, delay) {
|
|
timers.push(setTimeout(fn, delay));
|
|
}
|
|
|
|
function stopTyping() {
|
|
for (var i = 0; i < timers.length; i++) { clearTimeout(timers[i]); }
|
|
timers = [];
|
|
typing = false;
|
|
}
|
|
|
|
function showStill() {
|
|
demoText.textContent = SCRIPT[0].text + SCRIPT[1].text;
|
|
demoChip.textContent = SCRIPT[1].layout;
|
|
demoChip.classList.remove("out");
|
|
demoChip.hidden = false;
|
|
indicator.textContent = SCRIPT[1].indicator;
|
|
}
|
|
|
|
function showChip(text) {
|
|
demoChip.textContent = text;
|
|
demoChip.classList.remove("out");
|
|
demoChip.hidden = false;
|
|
|
|
later(function () { demoChip.classList.add("out"); }, 1100);
|
|
later(function () { demoChip.hidden = true; }, 1260);
|
|
}
|
|
|
|
function typeSegment(index, done) {
|
|
if (index >= SCRIPT.length) { done(); return; }
|
|
|
|
var segment = SCRIPT[index];
|
|
indicator.textContent = segment.indicator;
|
|
showChip(segment.layout);
|
|
|
|
var position = 0;
|
|
var step = function () {
|
|
if (position >= segment.text.length) {
|
|
later(function () { typeSegment(index + 1, done); }, 420);
|
|
return;
|
|
}
|
|
|
|
demoText.textContent += segment.text.charAt(position);
|
|
position++;
|
|
later(step, 52 + Math.random() * 58);
|
|
};
|
|
|
|
later(step, 620);
|
|
}
|
|
|
|
function run() {
|
|
demoText.textContent = "";
|
|
demoChip.hidden = true;
|
|
|
|
typeSegment(0, function () {
|
|
later(run, 2600);
|
|
});
|
|
}
|
|
|
|
function renderDemo() {
|
|
if (stillMotion.matches) {
|
|
stopTyping();
|
|
showStill();
|
|
return;
|
|
}
|
|
|
|
if (document.hidden || !onScreen) {
|
|
stopTyping();
|
|
return;
|
|
}
|
|
|
|
if (typing) { return; }
|
|
|
|
typing = true;
|
|
run();
|
|
}
|
|
|
|
stillMotion.addEventListener("change", renderDemo);
|
|
document.addEventListener("visibilitychange", renderDemo);
|
|
|
|
new IntersectionObserver(function (entries) {
|
|
onScreen = entries[0].isIntersecting;
|
|
renderDemo();
|
|
}).observe(document.querySelector(".stage"));
|
|
|
|
renderDemo();
|
|
|
|
/* ------------------------------------------------ the preview below */
|
|
|
|
var chip = document.getElementById("previewChip");
|
|
var offset = document.getElementById("offset");
|
|
var fontSize = document.getElementById("fontSize");
|
|
var opacity = document.getElementById("opacity");
|
|
var bgColor = document.getElementById("bgColor");
|
|
var fgColor = document.getElementById("fgColor");
|
|
var duration = document.getElementById("duration");
|
|
|
|
var offsetVal = document.getElementById("offsetVal");
|
|
var fontSizeVal = document.getElementById("fontSizeVal");
|
|
var opacityVal = document.getElementById("opacityVal");
|
|
var durationVal = document.getElementById("durationVal");
|
|
|
|
function applyLook() {
|
|
var onTheLeft = chip.classList.contains("left");
|
|
|
|
chip.style.fontSize = fontSize.value + "px";
|
|
chip.style.opacity = (opacity.value / 100).toFixed(2);
|
|
chip.style.background = bgColor.value;
|
|
chip.style.color = fgColor.value;
|
|
chip.style.marginLeft = onTheLeft ? "0px" : offset.value + "px";
|
|
chip.style.marginRight = onTheLeft ? offset.value + "px" : "0px";
|
|
|
|
offsetVal.textContent = offset.value;
|
|
fontSizeVal.textContent = fontSize.value;
|
|
opacityVal.textContent = opacity.value + "%";
|
|
durationVal.textContent = duration.value + (root.getAttribute("data-lang") === "ru" ? " мс" : " ms");
|
|
}
|
|
|
|
var inputs = [offset, fontSize, opacity, bgColor, fgColor, duration];
|
|
for (var k = 0; k < inputs.length; k++) {
|
|
inputs[k].addEventListener("input", applyLook);
|
|
}
|
|
|
|
document.getElementById("lookForm").addEventListener("submit", function (event) {
|
|
event.preventDefault();
|
|
});
|
|
|
|
document.getElementById("side").addEventListener("click", function (event) {
|
|
var button = event.target.closest("[data-side]");
|
|
if (!button) { return; }
|
|
|
|
var buttons = this.querySelectorAll("[data-side]");
|
|
for (var i = 0; i < buttons.length; i++) {
|
|
buttons[i].setAttribute("aria-pressed", buttons[i] === button ? "true" : "false");
|
|
}
|
|
|
|
var left = button.getAttribute("data-side") === "left";
|
|
chip.classList.toggle("left", left);
|
|
chip.classList.toggle("right", !left);
|
|
applyLook();
|
|
});
|
|
|
|
var previewTimers = [];
|
|
document.getElementById("previewBtn").addEventListener("click", function () {
|
|
for (var i = 0; i < previewTimers.length; i++) { clearTimeout(previewTimers[i]); }
|
|
previewTimers = [];
|
|
|
|
chip.classList.add("out");
|
|
|
|
previewTimers.push(setTimeout(function () {
|
|
chip.classList.remove("out");
|
|
|
|
previewTimers.push(setTimeout(function () {
|
|
chip.classList.add("out");
|
|
|
|
previewTimers.push(setTimeout(function () {
|
|
chip.classList.remove("out");
|
|
}, 900));
|
|
}, Number(duration.value)));
|
|
}, 420));
|
|
});
|
|
|
|
/* ------------------------------------------------------------ the start */
|
|
|
|
applyLanguage(root.getAttribute("data-lang"));
|
|
}());
|