WebOrbiton
v1.3.0

AtheraStudio SyncDrop

413 lines · 13.2 KB
  1. /*!
  2. * Orbitarium Design v1.1.0
  3. * https://orbitariumdesign.eu
  4. * Orbitarium Design License, https://orbitariumdesign.eu/license.html
  5. */
  6. (function () {
  7. "use strict";
  8. ​
  9. function store(key, value) {
  10. try {
  11. if (value === null) localStorage.removeItem(key);
  12. else localStorage.setItem(key, value);
  13. } catch (e) {
  14. // storage blocked (private mode etc.), ignore
  15. }
  16. }
  17. ​
  18. function read(key) {
  19. try {
  20. return localStorage.getItem(key);
  21. } catch (e) {
  22. return null;
  23. }
  24. }
  25. ​
  26. // theme: class="orb-dark" on <html>, saved as "orb-theme"
  27. var Theme = {
  28. KEY: "orb-theme",
  29. ​
  30. get: function () {
  31. return read(Theme.KEY);
  32. },
  33. ​
  34. apply: function (mode) {
  35. var root = document.documentElement;
  36. root.classList.toggle("orb-dark", mode === "dark");
  37. root.setAttribute("data-orb-theme", mode);
  38. document.querySelectorAll("[data-orb-theme-toggle]").forEach(function (btn) {
  39. btn.setAttribute("aria-pressed", mode === "dark" ? "true" : "false");
  40. });
  41. var meta = document.querySelector('meta[name="theme-color"]');
  42. if (meta) meta.setAttribute("content", mode === "dark" ? "#050506" : "#f8fafc");
  43. },
  44. ​
  45. init: function () {
  46. var saved = Theme.get();
  47. var prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
  48. Theme.apply(saved || (prefersDark ? "dark" : "light"));
  49. },
  50. ​
  51. toggle: function () {
  52. var next = document.documentElement.classList.contains("orb-dark") ? "light" : "dark";
  53. Theme.apply(next);
  54. store(Theme.KEY, next);
  55. }
  56. };
  57. ​
  58. // accent: [data-orb-accent-set="violet"] sets <html data-orb-accent>
  59. var Accent = {
  60. KEY: "orb-accent",
  61. ​
  62. current: function () {
  63. return document.documentElement.getAttribute("data-orb-accent") || "default";
  64. },
  65. ​
  66. apply: function (name) {
  67. var root = document.documentElement;
  68. if (!name || name === "default") root.removeAttribute("data-orb-accent");
  69. else root.setAttribute("data-orb-accent", name);
  70. Accent.sync();
  71. },
  72. ​
  73. sync: function () {
  74. var cur = Accent.current();
  75. document.querySelectorAll("[data-orb-accent-set]").forEach(function (btn) {
  76. btn.setAttribute("aria-pressed", btn.getAttribute("data-orb-accent-set") === cur ? "true" : "false");
  77. });
  78. },
  79. ​
  80. set: function (name) {
  81. Accent.apply(name);
  82. store(Accent.KEY, !name || name === "default" ? null : name);
  83. },
  84. ​
  85. init: function () {
  86. var saved = read(Accent.KEY);
  87. if (saved) Accent.apply(saved);
  88. else Accent.sync();
  89. ​
  90. document.addEventListener("click", function (e) {
  91. var btn = e.target.closest("[data-orb-accent-set]");
  92. if (btn) Accent.set(btn.getAttribute("data-orb-accent-set"));
  93. });
  94. }
  95. };
  96. ​
  97. // modal: [data-orb-modal-open="id"], [data-orb-modal-close]
  98. var Modal = {
  99. _active: null,
  100. _opener: null,
  101. ​
  102. open: function (id) {
  103. var el = document.getElementById(id);
  104. if (!el) return;
  105. Modal._opener = document.activeElement;
  106. el.classList.add("is-open");
  107. el.setAttribute("aria-hidden", "false");
  108. document.body.style.overflow = "hidden";
  109. var focusable = el.querySelector("input, select, textarea, [href], button:not(.orb-modal-close)") ||
  110. el.querySelector("button");
  111. if (focusable) focusable.focus({ preventScroll: true });
  112. Modal._active = el;
  113. },
  114. ​
  115. close: function (el) {
  116. if (!el) return;
  117. el.classList.remove("is-open");
  118. el.setAttribute("aria-hidden", "true");
  119. document.body.style.overflow = "";
  120. if (Modal._active === el) Modal._active = null;
  121. if (Modal._opener && Modal._opener.focus) Modal._opener.focus({ preventScroll: true });
  122. Modal._opener = null;
  123. },
  124. ​
  125. closeActive: function () {
  126. if (Modal._active) Modal.close(Modal._active);
  127. },
  128. ​
  129. init: function () {
  130. document.addEventListener("click", function (e) {
  131. var opener = e.target.closest("[data-orb-modal-open]");
  132. if (opener) {
  133. Modal.open(opener.getAttribute("data-orb-modal-open"));
  134. return;
  135. }
  136. var closer = e.target.closest("[data-orb-modal-close]");
  137. if (closer) {
  138. Modal.close(closer.closest(".orb-modal-overlay"));
  139. return;
  140. }
  141. // backdrop click
  142. if (e.target.classList && e.target.classList.contains("orb-modal-overlay")) {
  143. Modal.close(e.target);
  144. }
  145. });
  146. ​
  147. document.addEventListener("keydown", function (e) {
  148. if (e.key === "Escape") Modal.closeActive();
  149. });
  150. }
  151. };
  152. ​
  153. // mobile header menu
  154. var Menu = {
  155. set: function (header, open) {
  156. if (!header) return;
  157. header.classList.toggle("is-menu-open", open);
  158. var t = header.querySelector("[data-orb-menu-toggle]");
  159. if (t) t.setAttribute("aria-expanded", open ? "true" : "false");
  160. },
  161. ​
  162. closeAll: function () {
  163. document.querySelectorAll(".orb-site-header.is-menu-open").forEach(function (h) {
  164. Menu.set(h, false);
  165. });
  166. },
  167. ​
  168. init: function () {
  169. document.addEventListener("click", function (e) {
  170. var toggle = e.target.closest("[data-orb-menu-toggle]");
  171. if (toggle) {
  172. var header = toggle.closest(".orb-site-header");
  173. Menu.set(header, !header.classList.contains("is-menu-open"));
  174. return;
  175. }
  176. var link = e.target.closest(".orb-site-nav a");
  177. if (link || !e.target.closest(".orb-site-header")) Menu.closeAll();
  178. });
  179. ​
  180. document.addEventListener("keydown", function (e) {
  181. if (e.key === "Escape") Menu.closeAll();
  182. });
  183. ​
  184. if (window.matchMedia) {
  185. var mq = window.matchMedia("(min-width: 769px)");
  186. var onChange = function (ev) { if (ev.matches) Menu.closeAll(); };
  187. if (mq.addEventListener) mq.addEventListener("change", onChange);
  188. else if (mq.addListener) mq.addListener(onChange);
  189. }
  190. }
  191. };
  192. ​
  193. // nav sheet: [data-orb-nav-more] + [data-orb-nav-sheet]
  194. var NavSheet = {
  195. init: function () {
  196. var trigger = document.querySelector("[data-orb-nav-more]");
  197. var sheet = document.querySelector("[data-orb-nav-sheet]");
  198. if (!trigger || !sheet) return;
  199. ​
  200. var backdrop = document.createElement("div");
  201. backdrop.className = "orb-nav-sheet-backdrop";
  202. backdrop.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:70;display:none;";
  203. document.body.appendChild(backdrop);
  204. ​
  205. function openSheet() {
  206. sheet.classList.add("open");
  207. backdrop.style.display = "block";
  208. trigger.classList.add("open");
  209. trigger.setAttribute("aria-expanded", "true");
  210. }
  211. ​
  212. function closeSheet() {
  213. sheet.classList.remove("open");
  214. backdrop.style.display = "none";
  215. trigger.classList.remove("open");
  216. trigger.setAttribute("aria-expanded", "false");
  217. }
  218. ​
  219. trigger.addEventListener("click", function () {
  220. sheet.classList.contains("open") ? closeSheet() : openSheet();
  221. });
  222. backdrop.addEventListener("click", closeSheet);
  223. document.addEventListener("keydown", function (e) {
  224. if (e.key === "Escape") closeSheet();
  225. });
  226. }
  227. };
  228. ​
  229. // .orb-reveal fades in once it scrolls into view
  230. var Reveal = {
  231. init: function () {
  232. var items = document.querySelectorAll(".orb-reveal");
  233. if (!items.length) return;
  234. ​
  235. if (!("IntersectionObserver" in window)) {
  236. items.forEach(function (el) { el.classList.add("is-visible"); });
  237. return;
  238. }
  239. ​
  240. var observer = new IntersectionObserver(
  241. function (entries) {
  242. entries.forEach(function (entry) {
  243. if (entry.isIntersecting) {
  244. entry.target.classList.add("is-visible");
  245. observer.unobserve(entry.target);
  246. }
  247. });
  248. },
  249. { threshold: 0.1, rootMargin: "0px 0px -24px 0px" }
  250. );
  251. ​
  252. items.forEach(function (el) { observer.observe(el); });
  253. }
  254. };
  255. ​
  256. // dropdown
  257. var Dropdown = {
  258. closeAll: function (except) {
  259. document.querySelectorAll("[data-orb-dropdown-menu].is-open").forEach(function (m) {
  260. if (m === except) return;
  261. m.classList.remove("is-open");
  262. var t = m.parentElement && m.parentElement.querySelector("[data-orb-dropdown-trigger]");
  263. if (t) t.setAttribute("aria-expanded", "false");
  264. });
  265. },
  266. ​
  267. init: function () {
  268. document.querySelectorAll("[data-orb-dropdown-trigger]").forEach(function (t) {
  269. t.setAttribute("aria-haspopup", "true");
  270. t.setAttribute("aria-expanded", "false");
  271. });
  272. ​
  273. document.addEventListener("click", function (e) {
  274. var trigger = e.target.closest("[data-orb-dropdown-trigger]");
  275. if (trigger) {
  276. var menu = trigger.parentElement.querySelector("[data-orb-dropdown-menu]");
  277. if (!menu) return;
  278. var willOpen = !menu.classList.contains("is-open");
  279. Dropdown.closeAll(menu);
  280. menu.classList.toggle("is-open", willOpen);
  281. trigger.setAttribute("aria-expanded", willOpen ? "true" : "false");
  282. return;
  283. }
  284. var item = e.target.closest(".orb-dropdown-item");
  285. if (item || !e.target.closest("[data-orb-dropdown-menu]")) Dropdown.closeAll();
  286. });
  287. ​
  288. document.addEventListener("keydown", function (e) {
  289. if (e.key === "Escape") Dropdown.closeAll();
  290. });
  291. }
  292. };
  293. ​
  294. // tabs, also works as a segmented control when tabs have no panels
  295. var Tabs = {
  296. select: function (tab) {
  297. var list = tab.closest('[role="tablist"]');
  298. if (!list) return;
  299. list.querySelectorAll('[role="tab"]').forEach(function (t) {
  300. var on = t === tab;
  301. t.setAttribute("aria-selected", on ? "true" : "false");
  302. t.setAttribute("tabindex", on ? "0" : "-1");
  303. var id = t.getAttribute("aria-controls");
  304. var panel = id && document.getElementById(id);
  305. if (panel) panel.hidden = !on;
  306. });
  307. tab.dispatchEvent(new CustomEvent("orb:tab", { bubbles: true, detail: { tab: tab } }));
  308. },
  309. ​
  310. init: function () {
  311. document.querySelectorAll('[data-orb-tabs] [role="tablist"]').forEach(function (list) {
  312. var sel = list.querySelector('[role="tab"][aria-selected="true"]') || list.querySelector('[role="tab"]');
  313. if (sel) Tabs.select(sel);
  314. });
  315. ​
  316. document.addEventListener("click", function (e) {
  317. var tab = e.target.closest('[data-orb-tabs] [role="tab"]');
  318. if (tab) Tabs.select(tab);
  319. });
  320. ​
  321. document.addEventListener("keydown", function (e) {
  322. var tab = e.target.closest && e.target.closest('[data-orb-tabs] [role="tab"]');
  323. if (!tab) return;
  324. var keys = ["ArrowRight", "ArrowLeft", "Home", "End"];
  325. if (keys.indexOf(e.key) < 0) return;
  326. var tabs = Array.prototype.slice.call(tab.closest('[role="tablist"]').querySelectorAll('[role="tab"]'));
  327. var i = tabs.indexOf(tab);
  328. var n = e.key === "Home" ? 0
  329. : e.key === "End" ? tabs.length - 1
  330. : e.key === "ArrowRight" ? (i + 1) % tabs.length
  331. : (i - 1 + tabs.length) % tabs.length;
  332. e.preventDefault();
  333. tabs[n].focus();
  334. Tabs.select(tabs[n]);
  335. });
  336. }
  337. };
  338. ​
  339. // toast: Orbitarium.toast("Saved", { tone: "success" }) or [data-orb-toast]
  340. var Toast = {
  341. _stack: null,
  342. ​
  343. show: function (message, opts) {
  344. opts = opts || {};
  345. if (!Toast._stack || !document.body.contains(Toast._stack)) {
  346. Toast._stack = document.createElement("div");
  347. Toast._stack.className = "orb-toast-stack";
  348. Toast._stack.setAttribute("role", "status");
  349. Toast._stack.setAttribute("aria-live", "polite");
  350. document.body.appendChild(Toast._stack);
  351. }
  352. var el = document.createElement("div");
  353. el.className = "orb-toast" + (opts.tone ? " orb-tone-" + opts.tone : "");
  354. el.textContent = message;
  355. el.addEventListener("click", function () { Toast.dismiss(el); });
  356. Toast._stack.appendChild(el);
  357. ​
  358. var all = Toast._stack.querySelectorAll(".orb-toast:not(.is-leaving)");
  359. if (all.length > 4) Toast.dismiss(all[0]);
  360. ​
  361. var dur = opts.duration == null ? 3200 : opts.duration;
  362. if (dur > 0) setTimeout(function () { Toast.dismiss(el); }, dur);
  363. return el;
  364. },
  365. ​
  366. dismiss: function (el) {
  367. if (!el || el.classList.contains("is-leaving")) return;
  368. el.classList.add("is-leaving");
  369. setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 220);
  370. },
  371. ​
  372. init: function () {
  373. document.addEventListener("click", function (e) {
  374. var t = e.target.closest("[data-orb-toast]");
  375. if (t) Toast.show(t.getAttribute("data-orb-toast"), { tone: t.getAttribute("data-orb-toast-tone") });
  376. });
  377. }
  378. };
  379. ​
  380. window.Orbitarium = {
  381. version: "1.1.0",
  382. theme: Theme,
  383. accent: Accent,
  384. modal: Modal,
  385. tabs: Tabs,
  386. toast: Toast.show,
  387. dismissToast: Toast.dismiss
  388. };
  389. ​
  390. function init() {
  391. Theme.init();
  392. Accent.init();
  393. Modal.init();
  394. Menu.init();
  395. NavSheet.init();
  396. Reveal.init();
  397. Dropdown.init();
  398. Tabs.init();
  399. Toast.init();
  400. ​
  401. document.addEventListener("click", function (e) {
  402. var toggle = e.target.closest("[data-orb-theme-toggle]");
  403. if (toggle) Theme.toggle();
  404. });
  405. }
  406. ​
  407. if (document.readyState === "loading") {
  408. document.addEventListener("DOMContentLoaded", init);
  409. } else {
  410. init();
  411. }
  412. })();
  413. ​