User:Ch1902/ancestry.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
 * Internationalisation for ancestry widget
 */
var ancestry_i18n = {
  en: {
    link: "Show Ancestry",
    unknown: "Unknown",
    title: "Family Ancestry",
    loading: "Loading ancestry...",
    remaining: "%d remaining",
    jump: "Load ancestry of: ",
    select: "Select...",
    spouses: "Spouses",
    children: "Children",
    none: "None",
  },
  fr: {
    link: "Généalogie",
    unknown: "Inconnu",
    title: "Arbre généalogique",
    loading: "Chargement...",
    remaining: "%d à charger",
    jump: "Chargez : ",
    select: "Sélectionnez...",
    spouses: "Conjoints",
    children: "Enfants",
    none: "Aucun",
  },
  id: {
    link: "Lihat Silsilah",
    unknown: "Tidak diketahui",
    title: "Silsilah Keluarga",
    loading: "Memuat silsilah...",
    remaining: "tinggal %d lagi",
    jump: "Silsilah dari: ",
    select: "Pilih...",
    spouses: "Pasangan",
    children: "Anak",
    none: "Tidak ada",
  },
  min: {
    link: "Lihek Silsilah",
    unknown: "Indak diketahui",
    title: "Silsilah Kaluargo",
    loading: "Mamuek silsilah...",
    remaining: "tingga %d lai",
    jump: "Silsilah dari: ",
    select: "Piliah...",
    spouses: "Pasangan",
    children: "Anak",
    none: "Indak ado",
  },
};

/**
 * jQuery UI widget for displaying and navigating family
 * pedigree charts of person items on wikidata
 *
 * @author [[User:Ch1902]]
 */
function loadWidget() {
  $.widget("wd.ancestry", {
    i18n: ancestry_i18n,

    DAD: "P22",
    MUM: "P25",
    KID: "P40",
    SPOUSE: "P26",
    SEX: "P21",
    MALE: 6581097,
    FEMALE: 6581072,

    options: {
      levels: 4,
      lang: "en",
      boxsize: 165,
      truncate: 22,
    },

    firstRun: true,
    dialog: null,
    family: {},
    deferred: null,
    counter: 1,
    history: {},

    _create: function () {
      var self = this,
        root = this._blankItem(),
        lang = this.options.lang;

      // clamp(min, max, i)
      this.options.levels = Math.max(2, Math.min(6, this.options.levels));

      // set tree root
      root.id = mw.config.get("wbEntityId");

      $(":wikibase-statementview").each(function () {
        var statementview = $.data(this, "statementview"),
          statement = statementview.value(),
          claim = statement.getClaim();

        if (claim.getMainSnak().getPropertyId() === self.SEX)
          root.gender = +claim
            .getMainSnak()
            .getValue()
            .getSerialization()
            .slice(1);
      });

      // dialog
      this.dialog = $("<div/>").dialog({
        draggable: true,
        modal: true,
        title: (this.i18n[this.options.lang] || this.i18n.en).title,
        autoOpen: false,
        dialogClass: "ancestry-dialog",
      });

      this.dialog.append($("<div/>").addClass("ancestry-content"));
      this.dialog.append(
        $("<div/>").addClass("ancestry-status").html("&nbsp;")
      );

      // events
      this.dialog.on("click", "td:has(a)", function (e) {
        self._showOtherRelations(e);
      });

      // custom css
      mw.util.addCSS(
        ".ancestry-dialog td.person { min-width: " +
          this.options.boxsize +
          "px; max-width: " +
          this.options.boxsize +
          "px; }" +
          ".ancestry-dialog span.unknown { font-style: italic; }" +
          ".ancestry-dialog .ui-dialog-content { text-align: center; background: #FCFCFC }" +
          ".ancestry-dialog table { margin: 0px auto; }" +
          ".ancestry-dialog .ancestry-status { text-align: left; }" +
          ".ancestry-dialog td.g0 { background: lightgrey }" +
          ".ancestry-dialog td.g" +
          self.MALE +
          " { background: #BADDFF }" +
          ".ancestry-dialog td.g" +
          self.FEMALE +
          " { background: #FFBADE }" +
          "/*.ancestry-dialog select { width: 180px; }*/"
      );

      this.element.on("click", function (e) {
        e.preventDefault();

        if (self.firstRun) {
          self.firstRun = false;
          self._toggleDialog(true);
          self._loadNewRoot(root);
        } else {
          self._toggleDialog(!self.dialog.is(":visible"));
        }

        return false;
      });
    },

    _showLoader: function () {
      var msg = (this.i18n[this.options.lang] || this.i18n.en).remaining;

      this._setDialogContent(false); // empty

      this._setDialogContent("loading");
      this._setDialogContent(
        msg.replace(
          /\%d/,
          '<strong id="ancestry-remaining">' +
            Math.pow(2, this.options.levels) +
            "</strong>"
        )
      );
      this._setDialogContent("<br><br>");
      this._setDialogContent($.createSpinner({ size: "large", type: "block" }));
    },

    _sizeDialogToTree: function () {
      var w = 2 * 10 + this.options.boxsize / 2,
        h = 20 * (Math.pow(2, this.options.levels - 1) / 2 - 2);

      w += this.options.boxsize * 0.75 * this.options.levels - 1;
      w += (this.options.levels - 1) * 5;

      for (var l = this.options.levels - 1; l > 0; l--)
        h += 25 * Math.pow(2, l);

      h += 40 + 30 + 25; // title + status + padding

      this.dialog.dialog("option", "minWidth", w);
      this.dialog.dialog("option", "minHeight", h);
      this.dialog.dialog("option", "position", {
        my: "center center",
        at: "center center",
        of: window,
      });
    },

    _toggleDialog: function (show) {
      this.dialog.dialog(show ? "open" : "close");
    },

    _setDialogContent: function (content) {
      this._doDialogContent(".ancestry-content", content);
    },

    _setDialogStatus: function (content) {
      this._doDialogContent(".ancestry-status", content);
    },

    _doDialogContent: function (selector, content) {
      var self = this;

      if (content === false) {
        // empty
        this.dialog.find(selector).empty();
      } else if ($.type(content) === "array") {
        $.each(content, function (_, value) {
          self._doDialogContent(selector, value);
        });
      } else if ($.type(content) === "object") {
        this.dialog.find(selector).append(content);
      } else {
        this.dialog
          .find(selector)
          .append(
            (this.i18n[this.options.lang] || this.i18n.en)[content] || content
          );
      }
    },

    _initFamilyData: function (root) {
      this.family = {
        1: root,
      };
    },

    _loadNewRoot: function (root) {
      root.spouses = [];
      root.children = [];

      this._initFamilyData(root);
      this._showLoader();
      this._setDialogStatus(false);

      this.deferred = null;
      this.counter = 1;

      this._loadAncestry();
    },

    _loadAncestry: function () {
      var self = this;

      this.deferred = new $.Deferred();

      this.deferred.done(function () {
        self._loadAhnentafel().done(function (data) {
          self._sizeDialogToTree();
          self._showFamilyTree($(data.parse.text["*"]));
        });
      });

      this._loadFamilyTree();
    },

    _showFamilyTree: function (tpl) {
      var i = 1,
        max = Math.pow(2, this.options.levels);

      for (i = 1; i < max; i++) {
        var span = tpl.find('span:contains("%' + i + '$s")'),
          cell = span.parents("td"),
          peep = this.family[i],
          label = peep.id ? $("<a/>") : $("<em/>");

        if (peep.id)
          label.attr({ href: mw.util.getUrl(peep.id.toUpperCase()) });

        if (
          peep.label.length > this.options.truncate &&
          this.options.truncate > 0
        )
          label.html(
            peep.label.substr(0, this.options.truncate - 1) + "&hellip;"
          );
        else label.html(peep.label);

        label.attr({ title: peep.label });
        cell.addClass("g" + peep.gender).data("person", peep);

        cell.empty().append(label).addClass("person");
      }

      this._setDialogContent(false); // empty
      this._setDialogContent(tpl);
      this._toggleDialog(true);
    },

    _loadFamilyTree: function () {
      var self = this,
        lang = this.options.lang,
        max = Math.pow(2, this.options.levels),
        cur = this.counter,
        person = this.family[cur];

      if (cur >= max || this.deferred.state() !== "pending") {
        if (this.deferred.state() !== "resolved") this.deferred.resolve();

        return;
      }

      this._doDialogContent("#ancestry-remaining", false);
      this._doDialogContent("#ancestry-remaining", max - cur);

      if (person.id !== false) {
        $.ajax({
          url: mw.util.wikiScript("api"),
          type: "GET",
          dataType: "json",
          data: {
            action: "wbgetentities",
            format: "json",
            languages: lang,
            ids: person.id,
            props: "labels|claims",
          },
          success: function (data) {
            var entity = data.entities[person.id],
              labels = entity.labels || {},
              claims = entity.claims || {},
              dads = claims[self.DAD] || [],
              mums = claims[self.MUM] || [],
              sex = claims[self.SEX] || [],
              spouses = claims[self.SPOUSE] || [],
              kids = claims[self.KID] || [];

            self.family[cur].label =
              (labels[lang] && labels[lang].value) || person.id.toUpperCase();

            self.family[cur * 2] = self._blankItem();
            self.family[cur * 2 + 1] = self._blankItem();

            if (dads.length === 1) {
              var dad = dads[0].mainsnak.datavalue;

              if (dad.type === "wikibase-entityid") {
                self.family[cur].father = "Q" + dad.value["numeric-id"];

                self.family[cur * 2].id = "Q" + dad.value["numeric-id"];
                self.family[cur * 2].gender = self.MALE;
              }
            }

            if (mums.length === 1) {
              var mum = mums[0].mainsnak.datavalue;

              if (mum.type === "wikibase-entityid") {
                self.family[cur].mother = "Q" + mum.value["numeric-id"];

                self.family[cur * 2 + 1].id = "Q" + mum.value["numeric-id"];
                self.family[cur * 2 + 1].gender = self.FEMALE;
              }
            }

            // only if needed
            if (self.family[cur].gender === 0 && sex.length) {
              var gen = sex[0].mainsnak.datavalue;

              if (gen.type === "wikibase-entityid") {
                self.family[cur].gender = gen.value["numeric-id"];
              }
            }

            if (spouses.length) {
              $.each(spouses, function (_, spouse) {
                var sp = spouse.mainsnak.datavalue;

                if (sp.type === "wikibase-entityid") {
                  self.family[cur].spouses.push("Q" + sp.value["numeric-id"]);
                }
              });
            }

            if (kids.length) {
              $.each(kids, function (_, kid) {
                var child = kid.mainsnak.datavalue;

                if (child.type === "wikibase-entityid") {
                  self.family[cur].children.push(
                    "q" + child.value["numeric-id"]
                  );
                }
              });
            }

            self.counter++;
            self._loadFamilyTree(); // recurse
          },
          fail: function () {
            self.family[cur * 2] = self._blankItem();
            self.family[cur * 2 + 1] = self._blankItem();

            self.counter++;
            self._loadFamilyTree(); // recurse
          },
        });
      } else {
        this.family[cur * 2] = this._blankItem();
        this.family[cur * 2 + 1] = this._blankItem();

        this.counter++;
        this._loadFamilyTree(); // recurse
      }
    },

    _showOtherRelations: function (e) {
      var self = this,
        cell = $(e.currentTarget),
        data = cell.data(),
        person = data.person,
        spouses = person.spouses || [],
        kids = person.children || [],
        holder,
        lang = this.options.lang,
        dropdown,
        kgroup,
        sgroup;

      // allow click throughs
      if (e.ctrlKey || e.shiftKey || e.altKey) return true;

      e.preventDefault();

      // cached
      if (data.status) {
        this._setDialogStatus(false);
        this._setDialogStatus(data.status);

        return false;
      }

      // holds selected person each time
      dropdown = $("<select/>")
        .append($("<option/>").text((this.i18n[lang] || this.i18n.en).select))
        .append(
          $("<option/>")
            .attr("value", JSON.stringify(person))
            .text(person.label)
        )
        .on("click", "option", function (e) {
          var val = $(this).val();

          if (val) {
            self._loadNewRoot(JSON.parse(val));
          }
        });

      sgroup = $("<optgroup/>")
        .attr("label", (this.i18n[lang] || this.i18n.en).spouses)
        .appendTo(dropdown);
      kgroup = $("<optgroup/>")
        .attr("label", (this.i18n[lang] || this.i18n.en).children)
        .appendTo(dropdown);

      if (spouses.length < 1)
        sgroup.append(
          "<option>" + (this.i18n[lang] || this.i18n.en).none + "</option>"
        );

      if (kids.length < 1)
        kgroup.append(
          "<option>" + (this.i18n[lang] || this.i18n.en).none + "</option>"
        );

      holder = $("<div/>").text((this.i18n[lang] || this.i18n.en).jump);

      if (spouses.length > 0 || kids.length > 0) {
        $.when(this._loadOtherRelations(spouses.concat(kids))).done(function (
          data
        ) {
          $.each(spouses, function (_, spouse) {
            var option,
              ent = data.entities[spouse],
              labels = ent.labels || {},
              item = self._blankItem();

            if (data.entities[spouse]) {
              item.label =
                (labels[lang] && labels[lang].value) || spouse.toUpperCase();
              item.id = spouse;

              option = $("<option/>").text(item.label);
              option.attr("value", JSON.stringify(item));
              option.appendTo(sgroup);
            }
          });

          $.each(kids, function (_, kid) {
            var option,
              ent = data.entities[kid],
              labels = ent.labels || {},
              item = self._blankItem();

            if (data.entities[kid]) {
              item.label =
                (labels[lang] && labels[lang].value) || kid.toUpperCase();
              item.id = kid;

              option = $("<option/>").text(item.label);
              option.attr("value", JSON.stringify(item));
              option.appendTo(kgroup);
            }
          });
        });
      }

      holder.append(dropdown);

      cell.data("status", holder);

      this._setDialogStatus(false);
      this._setDialogStatus(holder);

      return false;
    },

    _loadOtherRelations: function (ids) {
      return $.ajax({
        url: mw.util.wikiScript("api"),
        type: "GET",
        dataType: "json",
        data: {
          action: "wbgetentities",
          languages: this.options.lang,
          format: "json",
          props: "info|labels",
          ids: ids.join("|"),
        },
      });
    },

    _loadAhnentafel: function () {
      // use CORS because text.length might be > 255 so use POST
      return $.ajax({
        url: "//en.wikipedia.org/w/api.php",
        type: "POST",
        dataType: "json",
        data: {
          origin: window.location.protocol + "//" + window.location.host,
          action: "parse",
          format: "json",
          prop: "text",
          disablepp: 1,
          text: this._buildAhnentafel(),
        },
      });
    },

    _buildAhnentafel: function () {
      var i,
        num,
        max = Math.pow(2, this.options.levels),
        tpl = "{{ahnentafel-compact" + this.options.levels + "";

      tpl +=
        "|style=font-size: 90%; line-height: 110%;|border=1|boxstyle=padding-top: 2px; padding-bottom: 2px;";

      // just to replace later
      for (i = 1; i < max; i++) {
        tpl += "|" + i + '= <span class="person">%' + i + "$s</span>";
      }

      tpl += "}}";

      return tpl;
    },

    _blankItem: function () {
      var i18n = this.i18n,
        lang = this.options.lang;

      return {
        id: false,
        label: (i18n[lang] || i18n.en).unknown,
        father: false,
        mother: false,
        gender: 0,
        spouses: [],
        children: [],
      };
    },

    _setOption: function (option, value) {
      switch (option) {
        case "levels":
          value = Math.max(2, Math.min(6, value));
          break;

        case "root":
          var root = this._blankItem();
          root.id = value;

          this._loadNewRoot(root);
          break;
      }

      this._super(option, value);
    },
  });
}

var ancestry_deps = [
  "jquery",
  "jquery.ui",
  "jquery.ui",
  "jquery.spinner",
  "wikibase.view.ControllerViewFactory",
];

var wgUserLanguage = mw.config.get("wgUserLanguage"),
  opts =
    typeof ancestry_opts === "object"
      ? ancestry_opts
      : { lang: wgUserLanguage };

if (
  mw.config.get("wgNamespaceNumber") === 0 &&
  mw.config.get("wgAction") === "view"
) {
  mw.loader.using(ancestry_deps, function () {
    loadWidget();
    $(function () {
      var tool = mw.util.addPortletLink(
        "p-tb",
        "#",
        (ancestry_i18n[opts.lang || wgUserLanguage] || ancestry_i18n.en).link,
        "t-ancestry"
      );

      $(tool).find("a").ancestry(opts);
    });
  });
}