Crom-based Interwiki Demo (Sigma-9)

<div id="stylesheets" style="display:none">
<style>
body {
  margin: 0;
  padding: 0;
  font-size: 0.8rem;
}
aside {
  font-family: Arial, Helvetica, sans-serif;
  background: #f8f1f1;
  padding: 8px;
  border-radius: 5px;
  box-sizing: border-box;
  width: 200px;
}
h2 {
  color: #93394f;
  margin: 8px;
  margin-bottom: 5px;
  font-weight: bold;
  font-size: 0.8rem;
  padding-bottom: 3px;
  border-bottom: 2px solid currentColor;
}
ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
li {
  margin: 0;
}
a, a:visited {
  display: block;
  padding: 5px 8px;
  border-radius: 3px;
  color: #93394f;
  text-decoration: none;
 
  transition: background-color 200ms cubic-bezier(.4,0,.2,1)
}
a:hover, a:focus-visible {
  text-decoration: underline;
  background-color: #ffc0c0;
  outline: 0;
 
  transition-duration: 75ms;
}
.primary {
  font-weight: bold;
}
.waiting {
  display: none;
}
</style>
</div>
 
<div id="resize-frame" style="display:none"></div>
 
<aside class="waiting">
  <h2>In other languages</h2>
  <ul id="list"></ul>
</aside>
 
<script>
"use strict";
 
const stylesheets = [];
let isInterwikiVisible = false;
 
window.addStylesheet = function addStylesheet(url, layer) {
  stylesheets.push({ url: url, layer: layer });
  stylesheets.sort(function (a, b) {
    return a.layer - b.layer;
  });
 
  // Easier this way, but really inefficient, I assume.
  document.querySelector("#stylesheets").innerHTML = stylesheets.reduce(function (acc, stylesheet) {
    return acc + '<link rel="stylesheet" type="text/css" href="' + stylesheet.url + '" onload="updateFrameHeight();">\n';
  }, "");
}
 
// A mapping from shortname to wiki baseUrl.
const WIKIS = {
  "en": "http://scp-wiki.wikidot.com",
  "int": "http://scp-int.wikidot.com",
  "ru": "http://scp-ru.wikidot.com",
  "ko": "http://scpko.wikidot.com",
  "cn": "http://scp-wiki-cn.wikidot.com",
  "fr": "http://fondationscp.wikidot.com",
  "pl": "http://scp-pl.wikidot.com",
  "es": "http://lafundacionscp.wikidot.com",
  "th": "http://scp-th.wikidot.com",
  "jp": "http://scp-jp.wikidot.com",
  "de": "http://scp-wiki-de.wikidot.com",
  "it": "http://fondazionescp.wikidot.com",
  "uk": "http://scp-ukrainian.wikidot.com",
  "pt": "http://scp-pt-br.wikidot.com",
  "cs": "http://scp-cs.wikidot.com",
  "zh-tr": "http://scp-zh-tr.wikidot.com",
  "vn": "http://scp-vn.wikidot.com",
  "el": "http://scp-el.wikidot.com",
  "idn": "http://scp-idn.wikidot.com",
 
  "wl": "http://wanderers-library.wikidot.com",
  "wl-pl": "http://wanderers-library-pl.wikidot.com",
  "wl-jp": "http://wanderers-library-jp.wikidot.com",
  "wl-ko": "http://wanderers-library-ko.wikidot.com",
  "wl-cs": "http://wanderers-library-cs.wikidot.com"
};
 
// A mapping from wiki to language name. English gets two!
// The last two in this list are unofficial.
const LANGUAGES = {
  "http://scp-wiki.wikidot.com": "English",
  "http://scp-int.wikidot.com": "English - <em>International</em>",
  "http://scp-ru.wikidot.com": "Русский",
  "http://scpko.wikidot.com": "한국어",
  "http://scp-wiki-cn.wikidot.com": "汉语",
  "http://fondationscp.wikidot.com": "Français",
  "http://scp-pl.wikidot.com": "Polski",
  "http://lafundacionscp.wikidot.com": "Español",
  "http://scp-th.wikidot.com": "ไทย",
  "http://scp-jp.wikidot.com": "日本語",
  "http://scp-wiki-de.wikidot.com": "Deutsch",
  "http://fondazionescp.wikidot.com": "Italiano",
  "http://scp-ukrainian.wikidot.com": "Українська",
  "http://scp-pt-br.wikidot.com": "Português",
  "http://scp-cs.wikidot.com": "Čeština",
  "http://scp-zh-tr.wikidot.com": "漢語",
  "http://scp-vn.wikidot.com": "Tiếng Việt",
  "http://scp-el.wikidot.com": "Ελληνικά",
  "http://scp-idn.wikidot.com": "Bahasa Indonesia",
 
  "http://wanderers-library.wikidot.com": "English",
  "http://wanderers-library-pl.wikidot.com": "Polski",
  "http://wanderers-library-jp.wikidot.com": "日本語",
  "http://wanderers-library-ko.wikidot.com": "한국어",
  "http://wanderers-library-cs.wikidot.com": "Čeština",
};
 
const TITLES = {
  "http://scp-wiki.wikidot.com": "In other languages",
  "http://scp-int.wikidot.com": "In other languages",
  "http://scp-ru.wikidot.com": "На других языках",
  "http://scpko.wikidot.com": "다른 언어",
  "http://scp-wiki-cn.wikidot.com": "其他语言",
  "http://fondationscp.wikidot.com": "Dans d'autres langues",
  "http://scp-pl.wikidot.com": "W innych językach",
  "http://lafundacionscp.wikidot.com": "En otros idiomas",
  "http://scp-th.wikidot.com": "ภาษาอื่น",
  "http://scp-jp.wikidot.com": "他言語版",
  "http://scp-wiki-de.wikidot.com": "In anderen Sprachen",
  "http://fondazionescp.wikidot.com": "In altre lingue",
  "http://scp-ukrainian.wikidot.com": "Іншими мовами",
  "http://scp-pt-br.wikidot.com": "Em outros idiomas",
  "http://scp-cs.wikidot.com": "V jiných jazycích",
  "http://scp-zh-tr.wikidot.com": "其他語言",
  "http://scp-vn.wikidot.com": "In other languages",
  "http://scp-el.wikidot.com": "In other languages",
  "http://scp-idn.wikidot.com": "In other languages",
 
  "http://wanderers-library.wikidot.com": "In other languages",
  "http://wanderers-library-pl.wikidot.com": "W innych językach",
  "http://wanderers-library-jp.wikidot.com": "他言語版",
  "http://wanderers-library-ko.wikidot.com": "다른 언어",
  "http://wanderers-library-cs.wikidot.com": "V jiných jazycích",
}
 
// The static GraphQL query string.
// Translations in Crom are directional, so if this page isn't the original translation,
// you'd have to fetch that first, and get all of its translations. Maybe there's some
// cool ordering we could do with that, IDK.
const QUERY = "  \
  query InterwikiQuery($url: URL!) { \
    page(url: $url) { \
      translations { \
        url \
      } \
      translationOf { \
        url \
        translations { \
          url \
        } \
      } \
    } \
  } \
";
 
// You have to escape query params, but not with a hash.
const parts  = window.location.hash.slice(1).split('|');
const siteDomain = parts[0];
const wikiShortName = parts[1];
const pagename = parts[2];
 
const url = WIKIS[wikiShortName || "en"] + pagename;
 
// set the title based on the language of the url
const title = TITLES[
  Object.keys(TITLES).filter(function (lang) {
    return url.indexOf(lang) === 0;
  })[0] || "http://scp-wiki.wikidot.com"
];
document.querySelector("h2").innerHTML = title;
 
// Fuck it, we're just going to reuse wikidot's iframe sizing garbage
let previousHeight = 0;
function updateFrameHeight() {
  if (!isInterwikiVisible) {
    showInterwiki();
    return;
  }
  const pagename = url.replace(/^http:\/\/[^/]+\//i, '');
  const height = document.body.scrollHeight ? document.body.scrollHeight + 1 : 0;
  if (height !== previousHeight) {
    document.getElementById("resize-frame").innerHTML = (
      '<iframe src="http://' + siteDomain + '/common--javascript/resize-iframe.html#' +
      height + '/' + pagename +
      '"></iframe>'
    );
    previousHeight = height;
  }
}
 
let debounceTimer = null;
function showInterwiki() {
    if (debounceTimer !== null) {
      clearTimeout(debounceTimer);
    }
    debounceTimer = setTimeout(function () {
      document.querySelector(".waiting").classList.remove("waiting");
      isInterwikiVisible = true;
      debounceTimer = null;
      updateFrameHeight();
    }, 800); // Number from personal testing, we want to debounce as little as possible
}
 
function request() {
  // Fetch translations from Crom
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "https://api.crom.avn.sh/graphql", true);
  xhr.setRequestHeader("Content-Type", "application/json");
  xhr.onreadystatechange = function() {
    if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
        addTranslations(JSON.parse(this.responseText));
    }
  }
  xhr.send(JSON.stringify({ query: QUERY, variables: { url: url } }));
}
 
function addTranslations(response) {
  const data = response.data;
  const errors = response.errors;
 
  if (errors && errors.length > 0) {
    // Good enough for me
    console.error(errors);
    return;
  }
 
  let translationUrls = [].concat(
    data.page.translationOf
      ? [data.page.translationOf.url].concat(
           data.page.translationOf.translations.map(function (t) { return t.url })
         )
      : [],
    data.page.translations.map(function (t) { return t.url })
  );
 
  // Exclude self from the list
  translationUrls = translationUrls.filter(function (u) { return u !== url });
 
  // innerHTML abuse incoming
  const list = document.querySelector("#list");
  list.innerHTML = "";
 
  Object.keys(LANGUAGES).forEach(function (origin) {
    // Get the URL that matches the language
    const translationUrl = translationUrls.filter(function(t) { return t.indexOf(origin) === 0 })[0];
    if (!translationUrl) return;
 
    // Turn HTTP into HTTPS cause why not, Wikidot is going to redirect us back anyway
    const isPrimary = data.page.translationOf && data.page.translationOf.url === translationUrl;
    const element = (
      '<li' +
      (isPrimary ? ' class="primary"' : '') +
      '><a target="_parent" href="' +
      translationUrl.replace(/^http/, 'https') + '">' + LANGUAGES[origin] +
      '</a></li>'
    );
    if (isPrimary) {
      list.innerHTML = element + list.innerHTML;
    } else {
      list.innerHTML += element;
    }
  });
 
  // Finally, display the thing.
  if (translationUrls.length > 0) {
    showInterwiki();
  }
}
 
request();
</script>

<script>
"use strict";
 
const parts = window.location.hash.slice(1).split(",");
 
let attemptCount = 0;
(function attachStyles() {
  const frames = window.parent.frames;
  for (let i = 0; i < frames.length; i++) {
    try {
      if (typeof frames[i].addStylesheet === 'function') {
        frames[i].addStylesheet(parts[0], parts[1]);
        return;
      }
    } catch (e) {
      // Likely a cross-origin iframe.
    }
  }
  if (attemptCount < 3) {
    attemptCount++;
    setTimeout(attachStyles, 1000);
  }
})();
</script>

<script>
"use strict";
 
const parts = window.location.hash.slice(1).split(",");
 
let attemptCount = 0;
(function attachStyles() {
  const frames = window.parent.frames;
  for (let i = 0; i < frames.length; i++) {
    try {
      if (typeof frames[i].addStylesheet === 'function') {
        frames[i].addStylesheet(parts[0], parts[1]);
        return;
      }
    } catch (e) {
      // Likely a cross-origin iframe.
    }
  }
  if (attemptCount < 3) {
    attemptCount++;
    setTimeout(attachStyles, 1000);
  }
})();
</script>

First stylesheet (Base Sigma-9)

body {
    margin: 0;
    padding: 5px;
    font-size: 0.80em;
}
/* width: 217px; */
aside {
    font-family: Verdana, Arial, Helvetica, sans-serif;
    background: #fff;
    margin: 0;
    padding: 10px;
        border: 1px solid #600;
        border-radius: 10px;
        box-shadow: 0 2px 6px rgba(102,0,0,.5);
    box-sizing: border-box;
    width: 16.25em;
}
h2 {
    color: #600;
    border-bottom: solid 1px #600;
    padding-left: 15px;
    margin-top: 10px;
    margin-bottom: 5px;
    font-size: 8pt;
    font-weight: bold;
}
ul {
    list-style-type: none;
    padding: 0;
}
li {
    position: relative;
    margin: 2px 0;
}
li:before {
    content: "■";
    font-size: 7px;
    color: #b01;
    position: relative;
    margin: 0 7px 0 5px;
    bottom: 3px;
}
li.primary:before {
  content: "⭐";
  margin: 0 3px;
}
li a, li a:visited {
    font-weight: bold;
    color: #b01;
    text-decoration: none;
    background: transparent;
}
li a:hover {
    text-decoration: underline;
}
@media (min-width: 14.375em) {
    .interwiki {
        width: 17em;
    }
}
.waiting {
  display: none;
}

Override stylesheet (Adds hover animations)

li {
  transition: transform 0.1s ease-out;
}
li:hover {
  transform: translateX(10px);
}