User:Pyb/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)
  • Edge: Hold Ctrl while clicking Refresh, or 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]]
 */
$.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 = wbEntityId;

   $.each(wb.entity.claims || [], function (_, claim) {
      if (claim.getMainSnak().getPropertyId() === self.SEX)
         root.gender = claim.getMainSnak().getValue().getNumericId();
   });


   // 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 = wb.Snak.newFromJSON(dads[0].mainsnak);

               if (dad.getValue().getType() === 'wikibase-entityid')
               {
                  self.family[cur].father = 'Q' + dad.getValue().getNumericId();

                  self.family[cur * 2].id = 'Q' + dad.getValue().getNumericId();
                  self.family[cur * 2].gender = self.MALE;
               }
            }

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

               if (mum.getValue().getType() === 'wikibase-entityid')
               {
                  self.family[cur].mother = 'Q' + mum.getValue().getNumericId();

                  self.family[cur * 2 + 1].id = 'Q' + mum.getValue().getNumericId();
                  self.family[cur * 2 + 1].gender = self.FEMALE;
               }
            }

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

               if (gen.getValue().getType() === 'wikibase-entityid')
               {
                  self.family[cur].gender = gen.getValue().getNumericId();
               }
            }

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

                  if (sp.getValue().getType() === 'wikibase-entityid')
                  {
                     self.family[cur].spouses.push('Q' + sp.getValue().getNumericId());
                  }
               });
            }

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

                  if (child.getValue().getType() === 'wikibase-entityid')
                  {
                     self.family[cur].children.push('q' + child.getValue().getNumericId());
                  }
               });
            }

            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'
];

var 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 () {
      $(function () {
         var tool = mw.util.addPortletLink('p-tb', '#', (ancestry_i18n[opts.lang || wgUserLanguage] || ancestry_i18.en).link, 't-ancestry');

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