Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

script.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. /***************************/
  2. /* INITIALIZATION REGISTRY */
  3. /***************************/
  4. /* Polyfill for requestIdleCallback in Apple and Microsoft browsers. */
  5. if (!window.requestIdleCallback) {
  6. window.requestIdleCallback = (fn) => { setTimeout(fn, 0); };
  7. }
  8. /* TBC. */
  9. GW.initializersDone = { };
  10. GW.initializers = { };
  11. function registerInitializer(name, tryEarly, precondition, fn) {
  12. GW.initializersDone[name] = false;
  13. GW.initializers[name] = fn;
  14. let wrapper = function () {
  15. if (GW.initializersDone[name]) return;
  16. if (!precondition()) {
  17. if (tryEarly) {
  18. setTimeout(() => requestIdleCallback(wrapper, { timeout: 1000 }), 50);
  19. } else {
  20. document.addEventListener("readystatechange", wrapper, { once: true });
  21. }
  22. return;
  23. }
  24. GW.initializersDone[name] = true;
  25. fn();
  26. };
  27. if (tryEarly) {
  28. requestIdleCallback(wrapper, { timeout: 1000 });
  29. } else {
  30. document.addEventListener("readystatechange", wrapper, { once: true });
  31. requestIdleCallback(wrapper);
  32. }
  33. }
  34. function forceInitializer(name) {
  35. if (GW.initializersDone[name]) return;
  36. GW.initializersDone[name] = true;
  37. GW.initializers[name]();
  38. }
  39. /***********/
  40. /* COOKIES */
  41. /***********/
  42. /* Sets a cookie. */
  43. function setCookie(name, value, days) {
  44. var expires = "";
  45. if (!days) days = 36500;
  46. if (days) {
  47. var date = new Date();
  48. date.setTime(date.getTime() + (days*24*60*60*1000));
  49. expires = "; expires=" + date.toUTCString();
  50. }
  51. document.cookie = name + "=" + (value || "") + expires + "; path=/";
  52. }
  53. /* Reads the value of named cookie.
  54. Returns the cookie as a string, or null if no such cookie exists. */
  55. function readCookie(name) {
  56. var nameEQ = name + "=";
  57. var ca = document.cookie.split(';');
  58. for(var i = 0; i < ca.length; i++) {
  59. var c = ca[i];
  60. while (c.charAt(0)==' ') c = c.substring(1, c.length);
  61. if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
  62. }
  63. return null;
  64. }
  65. /****************************************************/
  66. /* CSS CLASS MANIPULATION (polyfill for .classList) */
  67. /****************************************************/
  68. Element.prototype.addClass = function(className) {
  69. if (!this.hasClass(className))
  70. this.className = (this.className + " " + className).trim();
  71. }
  72. Element.prototype.addClasses = function(classNames) {
  73. let elementClassNames = this.className.trim().split(/\s/);
  74. classNames.forEach(className => {
  75. if (!this.hasClass(className))
  76. elementClassNames.push(className);
  77. });
  78. this.className = elementClassNames.join(" ");
  79. }
  80. Element.prototype.removeClass = function(className) {
  81. this.className = this.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), "$1").trim();
  82. if (this.className == "") this.removeAttribute("class");
  83. }
  84. Element.prototype.removeClasses = function(classNames) {
  85. classNames.forEach(className => {
  86. this.className = this.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), "$1").trim();
  87. });
  88. if (this.className == "") this.removeAttribute("class");
  89. }
  90. Element.prototype.hasClass = function(className) {
  91. return (new RegExp("(^|\\s+)" + className + "(\\s+|$)")).test(this.className);
  92. }
  93. Element.prototype.toggleClass = function(className, on) {
  94. if ((typeof on == "undefined" && !this.hasClass(className)) ||
  95. on == true) {
  96. this.addClass(className);
  97. } else {
  98. this.removeClass(className);
  99. }
  100. }
  101. /********************/
  102. /* QUERYING THE DOM */
  103. /********************/
  104. function queryAll(selector, context) {
  105. context = context || document;
  106. // Redirect simple selectors to the more performant function
  107. if (/^(#?[\w-]+|\.[\w-.]+)$/.test(selector)) {
  108. switch (selector.charAt(0)) {
  109. case '#':
  110. // Handle ID-based selectors
  111. let element = document.getElementById(selector.substr(1));
  112. return element ? [ element ] : [ ];
  113. case '.':
  114. // Handle class-based selectors
  115. // Query by multiple classes by converting the selector
  116. // string into single spaced class names
  117. var classes = selector.substr(1).replace(/\./g, ' ');
  118. return [].slice.call(context.getElementsByClassName(classes));
  119. default:
  120. // Handle tag-based selectors
  121. return [].slice.call(context.getElementsByTagName(selector));
  122. }
  123. }
  124. // Default to `querySelectorAll`
  125. return [].slice.call(context.querySelectorAll(selector));
  126. }
  127. function query(selector, context) {
  128. let all = queryAll(selector, context);
  129. return (all.length > 0) ? all[0] : null;
  130. }
  131. Object.prototype.queryAll = function (selector) {
  132. return queryAll(selector, this);
  133. }
  134. Object.prototype.query = function (selector) {
  135. return query(selector, this);
  136. }
  137. /*******************************/
  138. /* EVENT LISTENER MANIPULATION */
  139. /*******************************/
  140. /* Adds an event listener to a button (or other clickable element), attaching
  141. it to both ‘click’ and ‘keyup’ events (for use with keyboard navigation).
  142. Optionally also attaches the listener to the ‘mousedown’ event, making the
  143. element activate on mouse down instead of mouse up. */
  144. Element.prototype.addActivateEvent = function(func, includeMouseDown) {
  145. let ael = this.activateEventListener = (event) => { if (event.button === 0 || event.key === ' ') func(event) };
  146. if (includeMouseDown) this.addEventListener("mousedown", ael);
  147. this.addEventListener("click", ael);
  148. this.addEventListener("keyup", ael);
  149. }
  150. /* Removes event listener from a clickable element, automatically detaching it
  151. from all relevant event types. */
  152. Element.prototype.removeActivateEvent = function() {
  153. let ael = this.activateEventListener;
  154. this.removeEventListener("mousedown", ael);
  155. this.removeEventListener("click", ael);
  156. this.removeEventListener("keyup", ael);
  157. }
  158. /* Adds a scroll event listener to the page. */
  159. function addScrollListener(fn, name) {
  160. let wrapper = (event) => {
  161. requestAnimationFrame(() => {
  162. fn(event);
  163. document.addEventListener("scroll", wrapper, { once: true, passive: true });
  164. });
  165. }
  166. document.addEventListener("scroll", wrapper, { once: true, passive: true });
  167. // Retain a reference to the scroll listener, if a name is provided.
  168. if (typeof name != "undefined")
  169. GW[name] = wrapper;
  170. }
  171. /************************/
  172. /* ACTIVE MEDIA QUERIES */
  173. /************************/
  174. /* This function provides two slightly different versions of its functionality,
  175. depending on how many arguments it gets.
  176. If one function is given (in addition to the media query and its name), it
  177. is called whenever the media query changes (in either direction).
  178. If two functions are given (in addition to the media query and its name),
  179. then the first function is called whenever the media query starts matching,
  180. and the second function is called whenever the media query stops matching.
  181. If you want to call a function for a change in one direction only, pass an
  182. empty closure (NOT null!) as one of the function arguments.
  183. There is also an optional fifth argument. This should be a function to be
  184. called when the active media query is canceled.
  185. */
  186. function doWhenMatchMedia(mediaQuery, name, ifMatchesOrAlwaysDo, otherwiseDo = null, whenCanceledDo = null) {
  187. if (typeof GW.mediaQueryResponders == "undefined")
  188. GW.mediaQueryResponders = { };
  189. let mediaQueryResponder = (event, canceling = false) => {
  190. if (canceling) {
  191. GWLog(`Canceling media query “${name}”`);
  192. if (whenCanceledDo != null)
  193. whenCanceledDo(mediaQuery);
  194. } else {
  195. let matches = (typeof event == "undefined") ? mediaQuery.matches : event.matches;
  196. GWLog(`Media query “${name}” triggered (matches: ${matches ? "YES" : "NO"})`);
  197. if (otherwiseDo == null || matches) ifMatchesOrAlwaysDo(mediaQuery);
  198. else otherwiseDo(mediaQuery);
  199. }
  200. };
  201. mediaQueryResponder();
  202. mediaQuery.addListener(mediaQueryResponder);
  203. GW.mediaQueryResponders[name] = mediaQueryResponder;
  204. }
  205. /* Deactivates and discards an active media query, after calling the function
  206. that was passed as the whenCanceledDo parameter when the media query was
  207. added.
  208. */
  209. function cancelDoWhenMatchMedia(name) {
  210. GW.mediaQueryResponders[name](null, true);
  211. for ([ key, mediaQuery ] of Object.entries(GW.mediaQueries))
  212. mediaQuery.removeListener(GW.mediaQueryResponders[name]);
  213. GW.mediaQueryResponders[name] = null;
  214. }
  215. /****************/
  216. /* MISC HELPERS */
  217. /****************/
  218. /* Returns the passed object if it’s truthy, or a newly created HTMLElement.
  219. Æ(x) is the element analogue of (x||{}).
  220. */
  221. function Æ(x) {
  222. return x || document.createElement(null);
  223. }
  224. /* If top of element is not at or above the top of the screen, scroll it into
  225. view. */
  226. Element.prototype.scrollIntoViewIfNeeded = function() {
  227. GWLog("scrollIntoViewIfNeeded");
  228. let rect = this.getBoundingClientRect();
  229. if ((rect.bottom > window.innerHeight && rect.top > 0) ||
  230. rect.top < 0) {
  231. this.scrollIntoView(true);
  232. }
  233. }
  234. /* Return the currently selected text, as HTML (rather than unstyled text).
  235. */
  236. function getSelectionHTML() {
  237. var container = document.createElement("div");
  238. container.appendChild(window.getSelection().getRangeAt(0).cloneContents());
  239. return container.innerHTML;
  240. }
  241. /* Return the value of a GET (i.e., URL) parameter.
  242. */
  243. function getQueryVariable(variable) {
  244. var query = window.location.search.substring(1);
  245. var vars = query.split("&");
  246. for (var i = 0; i < vars.length; i++) {
  247. var pair = vars[i].split("=");
  248. if (pair[0] == variable)
  249. return pair[1];
  250. }
  251. return false;
  252. }
  253. /* Given an element or a selector, removes that element (or the element
  254. identified by the selector).
  255. If multiple elements match the selector, only the first is removed.
  256. */
  257. function removeElement(elementOrSelector, ancestor = document) {
  258. if (typeof elementOrSelector == "string") elementOrSelector = ancestor.query(elementOrSelector);
  259. if (elementOrSelector) elementOrSelector.parentElement.removeChild(elementOrSelector);
  260. }
  261. /* Returns true if the string begins with the given prefix.
  262. */
  263. String.prototype.hasPrefix = function (prefix) {
  264. return (this.lastIndexOf(prefix, 0) === 0);
  265. }
  266. /* Toggles whether the page is scrollable.
  267. */
  268. function togglePageScrolling(enable) {
  269. if (!enable) {
  270. window.addEventListener('keydown', GW.scrollingDisabledKeyDown = (event) => {
  271. let forbiddenKeys = [ " ", "Spacebar", "ArrowUp", "ArrowDown", "Up", "Down" ];
  272. if (forbiddenKeys.contains(event.key) &&
  273. event.target == document.body) {
  274. event.preventDefault();
  275. }
  276. });
  277. } else {
  278. window.removeEventListener('keydown', GW.scrollingDisabledKeyDown);
  279. }
  280. }
  281. /* Copies a string to the clipboard.
  282. */
  283. function copyTextToClipboard(string) {
  284. let scratchpad = query("#scratchpad");
  285. scratchpad.value = string;
  286. scratchpad.select();
  287. document.execCommand("copy");
  288. }
  289. /* Returns the next element sibling of the element, wrapping around to the
  290. first child of the parent if the element is the last child.
  291. Returns the element itself, if it has no siblings or no parent.
  292. */
  293. Element.prototype.nextElementSiblingCyclical = function() {
  294. if (this.parentElement == null) return this;
  295. return this.parentElement.children[(Array.prototype.indexOf.call(this.parentElement.children, this) + 1) % this.parentElement.children.length];
  296. }
  297. /* Returns the previous element sibling of the element, wrapping around to the
  298. last child of the parent if the element is the first child.
  299. Returns the element itself, if it has no siblings or no parent.
  300. */
  301. Element.prototype.previousElementSiblingCyclical = function() {
  302. if (this.parentElement == null) return this;
  303. return this.parentElement.children[(Array.prototype.indexOf.call(this.parentElement.children, this) - 1) % this.parentElement.children.length];
  304. }
  305. /********************/
  306. /* DEBUGGING OUTPUT */
  307. /********************/
  308. function GWLog (string) {
  309. if (GW.loggingEnabled == true || (GW.loggingEnabled == null && localStorage.getItem("logging-enabled") == "true"))
  310. console.log(string);
  311. }
  312. GW.enableLogging = (permanently = false) => {
  313. if (permanently)
  314. localStorage.setItem("logging-enabled", "true");
  315. else
  316. GW.loggingEnabled = true;
  317. };
  318. GW.disableLogging = (permanently = false) => {
  319. if (permanently)
  320. localStorage.removeItem("logging-enabled");
  321. else
  322. GW.loggingEnabled = false;
  323. };
  324. /**********/
  325. /* NAV UI */
  326. /**********/
  327. /* Hide the site nav UI on scroll down; show it on scroll up.
  328. Called by the ‘updateSiteNavUIStateScrollListener’ scroll listener.
  329. */
  330. function updateSiteNavUIState(event) {
  331. GWLog("updateSiteNavUIState");
  332. let newScrollTop = window.pageYOffset || document.documentElement.scrollTop;
  333. GW.scrollState.unbrokenDownScrollDistance = (newScrollTop > GW.scrollState.lastScrollTop) ?
  334. (GW.scrollState.unbrokenDownScrollDistance + newScrollTop - GW.scrollState.lastScrollTop) :
  335. 0;
  336. GW.scrollState.unbrokenUpScrollDistance = (newScrollTop < GW.scrollState.lastScrollTop) ?
  337. (GW.scrollState.unbrokenUpScrollDistance + GW.scrollState.lastScrollTop - newScrollTop) :
  338. 0;
  339. GW.scrollState.lastScrollTop = newScrollTop;
  340. // Hide site nav UI when scrolling a full page down.
  341. if (GW.scrollState.unbrokenDownScrollDistance > window.innerHeight) {
  342. if (!GW.scrollState.siteNavUI[0].hasClass("hidden")) toggleSiteNavUI();
  343. }
  344. // Make site nav UI translucent when scrolling down.
  345. GW.scrollState.siteNavUI.forEach(element => {
  346. if (GW.scrollState.unbrokenDownScrollDistance > 0) element.addClass("translucent-on-scroll");
  347. else element.removeClass("hidden");
  348. });
  349. // On desktop, show site nav UI when scrolling a full page up, or to the
  350. // the top of the page.
  351. // On mobile, show site nav UI on ANY scroll up.
  352. if (GW.mediaQueries.mobileNarrow.matches) {
  353. if (GW.scrollState.unbrokenUpScrollDistance > 0 || GW.scrollState.lastScrollTop <= 0)
  354. showSiteNavUI();
  355. } else if ( GW.scrollState.unbrokenUpScrollDistance > window.innerHeight
  356. || GW.scrollState.lastScrollTop == 0) {
  357. showSiteNavUI();
  358. }
  359. }
  360. function toggleSiteNavUI() {
  361. GWLog("toggleSiteNavUI");
  362. GW.scrollState.siteNavUI.forEach(element => {
  363. element.toggleClass("hidden");
  364. element.removeClass("translucent-on-scroll");
  365. });
  366. }
  367. function showSiteNavUI() {
  368. GWLog("showSiteNavUI");
  369. GW.scrollState.siteNavUI.forEach(element => {
  370. element.removeClass("hidden");
  371. element.removeClass("translucent-on-scroll");
  372. });
  373. }
  374. /******************/
  375. /* INITIALIZATION */
  376. /******************/
  377. registerInitializer('earlyInitialize', true, () => (query("#content") != null), () => {
  378. GWLog("INITIALIZER earlyInitialize");
  379. // Check to see whether we’re on a mobile device (which we define as a touchscreen^W narrow viewport).
  380. // GW.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
  381. // GW.isMobile = ('ontouchstart' in document.documentElement);
  382. GW.mediaQueries = {
  383. mobileNarrow: matchMedia("(max-width: 520px)"),
  384. mobileWide: matchMedia("(max-width: 900px)"),
  385. mobileMax: matchMedia("(max-width: 960px)"),
  386. hover: matchMedia("only screen and (hover: hover) and (pointer: fine)")
  387. };
  388. GW.isMobile = GW.mediaQueries.mobileMax.matches;
  389. GW.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
  390. GW.siteNavUISelector = "main nav";
  391. });
  392. /*************************/
  393. /* POST-LOAD ADJUSTMENTS */
  394. /*************************/
  395. registerInitializer('pageLayoutFinished', false, () => (document.readyState == "complete"), () => {
  396. forceInitializer('earlyInitialize');
  397. GWLog("INITIALIZER pageLayoutFinished");
  398. // We pre-query the relevant elements, so we don’t have to run queryAll on
  399. // every firing of the scroll listener.
  400. GW.scrollState = {
  401. "lastScrollTop": window.pageYOffset || document.documentElement.scrollTop,
  402. "unbrokenDownScrollDistance": 0,
  403. "unbrokenUpScrollDistance": 0,
  404. "siteNavUI": queryAll(GW.siteNavUISelector),
  405. };
  406. addScrollListener(updateSiteNavUIState, "updateSiteNavUIStateScrollListener");
  407. GW.scrollState.siteNavUI.forEach(element => {
  408. element.addEventListener("mouseover", () => { showSiteNavUI(); });
  409. });
  410. // Wrap all images in a span.
  411. document.querySelectorAll("main > nav ~ * img").forEach(image => {
  412. let wrapper = document.createElement("span");
  413. wrapper.classList.add("img");
  414. image.parentElement.insertBefore(wrapper, image);
  415. wrapper.appendChild(image);
  416. wrapper.classList.add(...image.classList);
  417. image.classList.remove(...image.classList);
  418. });
  419. imageFocusSetup();
  420. focusImageSpecifiedByURL();
  421. });