User:Hardwigg/wdTableView.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.
;(function() {
  const utils = {
    // from https://github.com/wikimedia/mediawiki-extensions-Wikibase/blob/587f8a41c359dad8dbc47335ea1968a762063d27/repo/resources/wikibase.ui.entityViewInit.js#L114
    getUserLanguages() {
      var userLanguages = mw.config.get( 'wbUserSpecifiedLanguages' ),
        isUlsDefined = mw.uls && $.uls && $.uls.data,
        languages;

      if ( !userLanguages.length && isUlsDefined ) {
        languages = mw.uls.getFrequentLanguageList().slice( 1, 4 );
      } else {
        languages = userLanguages.slice();
        languages.splice( $.inArray( mw.config.get( 'wgUserLanguage' ), userLanguages ), 1 );
      }

      languages.unshift( mw.config.get( 'wgUserLanguage' ) );

      return languages;
    }
  };

  const listView = {}; {
    listView.sgvClass = 'dc-display-list';
    {listView.css = `
.wikibase-statementgroupview.dc-display-list .wikibase-statementview {
  width: auto;
  padding: 0 2mm;
}

.wikibase-statementgroupview.dc-display-list .wikibase-statementview.wb-edit {
  width: 100%;
}
`;
    }
    listView.selected = function($statementGroupView) {
      mw.util.addCSS(listView.css);
      $statementGroupView.addClass(listView.sgvClass);
    }
  };

  const tableView = {}; {
    tableView.sgvClass = 'dc-display-table';
    {tableView.css = `
  .dc-display-table .wikibase-statementlistview-listview {
      position: relative;
      display: grid;
      grid-template-columns: 1fr;
  }

  .dc-display-table .wikibase-statementview {
      width: auto;
      grid-column: 1;
  }

  .dc-display-table .dc-grid-header {
      font-weight: bold;
      text-align: center;
      padding: 1mm;
      grid-row: 1;
      position: sticky;
      top: 0;

      border-top: 1px solid #AAA;
      border-bottom: 1px solid #AAA;
      background: white;
      z-index: 2;
  }

  .dc-display-table .dc-grid-header~.dc-grid-header {
      border-left: 1px solid #AAA;
  }

  .dc-display-table .dc-add-header {
      display: flex;
  }

  .dc-display-table .dc-add-header.pre-add .dc-add-header-plus {
    flex: 1;
  }

  .dc-display-table .dc-add-header.pre-add input,
  .dc-display-table .dc-add-header.pre-add .dc-add-header-x {
    display: none;
  }
  .dc-display-table .dc-add-header.pre-add {
    width: 30px;
  }

  .dc-display-table .dc-add-header.adding {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    padding-left: 10px;
  }
  .dc-display-table .dc-add-header.adding .dc-add-header-plus {
    display: none;
  }
  .dc-display-table .dc-add-header.adding .dc-add-header-x {
    width: 30px;
  }
  .dc-display-table .dc-add-header.adding input {
    flex: 1;
    min-weight: 40px;
  }

  .dc-display-table .dc-extra-cell {
    padding: 1mm;
  }

  .dc-display-table.dc-quals-as-cols .wikibase-statementview:not(.wb-edit) .wikibase-statementview-qualifiers {
    display: none;
  }

  .dc-display-table.dc-quals-as-cols .dc-grid-header.dc-qual-col {
    border-left-style: dotted;
  }

  .dc-display-table .wikibase-statementview-mainsnak {
    max-width: none;
  }

  .dc-display-table .wikibase-statementview-references-container {
    display:block;
  }
  `};
    tableView.templates = {}; {
      {tableView.templates.DEFAULT_TABLE_HEADERS = `
  <div class="dc-grid-header">Statement</div>
  <div class="dc-grid-header dc-add-header pre-add">
    <a class="dc-add-header-plus">+</a>
    <input name="dc-new-col">
    <a class="dc-add-header-x" >&times;</a>
  </div>`;}

      tableView.templates.NEW_COL_SPARQL = (qid, curPid, itemPid) => {
        return `SELECT
        ?x ?type
        ?timeTime ?timePrecision ?timeTimezone ?timeCalendarModel
        ?quantityAmount ?quantityUnit
        ?monolingualtextText ?monolingualtextLanguage
        ?itemId
        ?string
        WHERE {
          wd:${qid}  p:${curPid}/ps:${curPid} ?x.
          ?x p:${itemPid} ?s.
      
          OPTIONAL {
            ?s psv:${itemPid} [
              a wikibase:TimeValue;
              wikibase:timeValue ?timeTime;
              wikibase:timePrecision ?timePrecision;
              wikibase:timeTimezone ?timeTimezone;
              wikibase:timeCalendarModel ?timeCalendarModel;
            ].
            BIND("time" as ?type).
          }
          OPTIONAL {
            ?s psv:${itemPid} [
              a wikibase:QuantityValue;
              wikibase:quantityAmount ?quantityAmount;
              wikibase:quantityUnit   ?quantityUnit;
            ].
            BIND("quantity" as ?type).
          }
          OPTIONAL {
            ?s ps:${itemPid} ?monolingualtextText.
            Filter(lang(?monolingualtextText))
            BIND(lang(?monolingualtextText) as ?monolingualtextLanguage)
            BIND("monolingualtext" as ?type).
          }
          OPTIONAL {
            ?s ps:${itemPid} ?itemId.
            FILTER(isURI(?itemId))
            BIND("item" as ?type).
          }
          OPTIONAL {
            ?s ps:${itemPid} ?string.
            BIND("string" as ?type).
          }
        #   SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
        }`;
      }
    }
    tableView.utils = {
      entityFormatter: (function() {
        const userLanguages = utils.getUserLanguages();
    
        const repoConfig = mw.config.get( 'wbRepo' );
        const repoApiUrl = repoConfig.url + repoConfig.scriptPath + '/api.php';
        const mwApi = wb.api.getLocationAgnosticMwApi( repoApiUrl );
        const repoApi = new wb.api.RepoApi( mwApi );
    
        const formatterFactory = new wb.formatters.ApiValueFormatterFactory(
          new wb.api.FormatValueCaller(
            repoApi,
            wb.dataTypeStore
          ),
          userLanguages[0]
        );
        const htmlDataValueEntityIdFormatter = formatterFactory.getFormatter( null, null, 'text/html' );
    
        const parserStore = wb.parsers.getStore( repoApi );
        const entityIdParser = new ( parserStore.getParser( wb.datamodel.EntityId.TYPE ) )( { lang: userLanguages[ 0 ] } );
        return new wb.entityIdFormatter.CachingEntityIdHtmlFormatter(
          new wb.entityIdFormatter.DataValueBasedEntityIdHtmlFormatter(entityIdParser, htmlDataValueEntityIdFormatter)
        );
      })(),
      dedup(array, hash_fn=null, overwrite=false) {
        const map = new Map();
        for (let x of array) {
          const hash = hash_fn ? hash_fn(x) : x;
          if (map.has(hash) && overwrite) map.set(hash, x);
          else map.set(hash, x);
        }

        return map.values();
      },
      mapValues(obj, fn) {
        const result = {};
        for (let key in obj) {
          result[key] = fn(obj[key]);
        }
        return result;
      },
      renderValue($container, propertyId, data) {
        /**
           * @return {string[]} An ordered list of languages the user wants to use, the first being her
           *                    preferred language, and thus the UI language (currently wgUserLanguage).
           */
          function getUserLanguages() {
            var userLanguages = mw.config.get( 'wbUserSpecifiedLanguages' ),
              isUlsDefined = mw.uls && $.uls && $.uls.data,
              languages;
      
            if ( !userLanguages.length && isUlsDefined ) {
              languages = mw.uls.getFrequentLanguageList().slice( 1, 4 );
            } else {
              languages = userLanguages.slice();
              languages.splice( $.inArray( mw.config.get( 'wgUserLanguage' ), userLanguages ), 1 );
            }
      
            languages.unshift( mw.config.get( 'wgUserLanguage' ) );
      
            return languages;
          }
      
      
          /**
           * @param {wikibase.api.RepoApi} repoApi
           * @param {string} languageCode The language code of the ui language
           * @return {wikibase.store.CachingEntityStore}
           */
          function buildEntityStore( repoApi, languageCode ) {
            return new wb.store.CachingEntityStore(
              new wb.store.ApiEntityStore(
                repoApi,
                new wb.serialization.EntityDeserializer(),
                [ languageCode ]
              )
            );
          }
      
      
        var currentRevision, revisionStore, entityChangersFactory,
              viewFactoryArguments, ViewFactoryFactory, viewFactory, entityView,
              repoConfig = mw.config.get( 'wbRepo' ),
              repoApiUrl = repoConfig.url + repoConfig.scriptPath + '/api.php',
              mwApi = wb.api.getLocationAgnosticMwApi( repoApiUrl ),
              repoApi = new wb.api.RepoApi( mwApi ),
              userLanguages = getUserLanguages(),
              entityStore = buildEntityStore( repoApi, userLanguages[ 0 ] ),
              contentLanguages = new wikibase.WikibaseContentLanguages(),
              formatterFactory = new wb.formatters.ApiValueFormatterFactory(
                new wb.api.FormatValueCaller(
                  repoApi,
                  wb.dataTypeStore
                ),
                userLanguages[ 0 ]
              );
      
        var vvb = new wb.ValueViewBuilder (
          wb.experts.getStore(wb.dataTypeStore), // expertStore,
            formatterFactory,
            wb.parsers.getStore( repoApi ), //parserStore,
            userLanguages[ 0 ],
            null, // messageProvider,
            userLanguages,
            null, // vocabularyLookupApiUrl,
            null // commonsApiUrl
          )
          
      
        // var data = dataValues.MonolingualTextValue.newFromJSON({text: "FOO", language: "en"});
        // vvb.initValueView($el, wb.dataTypeStore.getDataType('monolingualtext'), data, 'P1705').value(data);
      
        // var typedData = dataValues.newDataValue(type, data);
        const type = wb.dataTypeStore.getDataType(data.getType());
        vvb.initValueView($container, type, data, propertyId).value(data);
      }
    };

    tableView.selected = function($statementGroupView) {
      mw.util.addCSS(tableView.css);
      const qualsAsCols = true;
      $statementGroupView.addClass(tableView.sgvClass);

      const $lv = $statementGroupView.find('.wikibase-statementlistview-listview');
      $lv.prepend(tableView.templates.DEFAULT_TABLE_HEADERS);

      if (qualsAsCols) {
        $statementGroupView.addClass('dc-quals-as-cols');

        // Get a unique list of all the qualifiers
        const $props = $statementGroupView.find(".wikibase-statementview-qualifiers .wikibase-snakview-property a");
        const uniqProps = tableView.utils.dedup($props, a => a.title);

        // create a column for each qualifier
        const propTitles = [];
        for (let propEl of uniqProps) {
          propTitles.push(propEl.title);
          const colIndex = createQualifierColumn($statementGroupView, $(propEl));
        }

        // copy each qualifier statement to the column
        // iterate through statements
        const statementViewEls = $statementGroupView.find('.wikibase-statementview');
        for (let i = 0; i < statementViewEls.length; i++) {
          const $sv = $(statementViewEls[i]);
          const props = new Map(propTitles.map(t => [t, []]));

          // iterate through qualifiers
          let lastPropTitle = null;
          for (let snakViewEl of $sv.find('.wikibase-statementview-qualifiers .wikibase-snakview')) {
            const $snakView = $(snakViewEl);
            let $prop = $snakView.find('.wikibase-snakview-property');
            let a = $prop.find('a');
            let propTitle = a.length ? a.attr('title') : lastPropTitle;
            props.get(propTitle).push($snakView.find('.wikibase-snakview-value-container')[0]);
            lastPropTitle = propTitle;
          }
          
          // add the cells
          for (let j = 0; j < propTitles.length; j++) {
            const propTitle = propTitles[j];
            const $cell = $(`<div class="dc-extra-cell dc-qual-vals-cell" style="grid-column:${j+2}; grid-row: ${i+2}"></div>`);
            const $values = props.get(propTitle).map($);
            if (!$values.length) {
              $cell.html('-');
            }
            for (let $val of $values) {
              $cell.append($val.clone(false, true));
            }
            $cell.insertAfter($sv);
          }
        }
      }

      // Let's display references as a column too
      const refsColIndex = createReferencesHeader($statementGroupView);
      // copy refs out
      const statementViewEls = $statementGroupView.find('.wikibase-statementview');
      for (let i = 0; i < statementViewEls.length; i++) {
        const $sv = $(statementViewEls[i]);
        const $refs = $sv.find('.wikibase-statementview-references-container');
        const $cell = $(`<div class="dc-extra-cell dc-ref-cell" style="grid-column:${refsColIndex}; grid-row: ${i+2}"></div>`);
        const $refsCopy = $($refs[0].cloneNode(false));
        $refsCopy.append($refs.find('.wikibase-statementview-references-heading'));
        $cell.append($refsCopy);
        $cell.insertAfter($sv);
      }

      const repoConfig = mw.config.get('wbRepo');
      const repoApiUrl = repoConfig.url + repoConfig.scriptPath + '/api.php'
      $lv.find('.dc-add-header input')
        .entityselector({url: repoApiUrl, type:'property'})
        .on('entityselectorselected', async (ev, pid) => {
          const $addHeader = $statementGroupView.find('.dc-add-header');
      
          // insert column data
          await insertColumn(ev, pid, $addHeader);
      
          // Stop adding
          $addHeader.removeClass('adding');
          $addHeader.addClass('pre-add');
          $addHeader.find('input').val('');
        });

      $('.wikibase-statementgroupview').on('click', '.dc-add-header-plus', ev => {
        // swap out the plus for the input dialog and a cancel
        const $addHeader = $(ev.target).closest('.dc-add-header');
        $addHeader.removeClass('pre-add');
        $addHeader.addClass('adding');
        $addHeader.find('input').focus();
      });

      $('.wikibase-statementgroupview').on('click', '.dc-add-header-x', ev => {
        // Undo swap
        const $addHeader = $(ev.target).closest('.dc-add-header');
        $addHeader.removeClass('adding');
        $addHeader.addClass('pre-add');
      });

      function createReferencesHeader($statementGroupView) {
        const $lv = $statementGroupView.find('.wikibase-statementlistview-listview');
        const colIndex = $statementGroupView.find('.dc-grid-header').length;
        const $prev = $statementGroupView.find('.dc-grid-header').last();
        const $el = $(`<div class="dc-grid-header dc-refs-col" style="grid-column:${colIndex}">References</div>`).insertAfter($prev);
        return colIndex;
      }

      function createQualifierColumn($statementGroupView, $prop) {
        const $lv = $statementGroupView.find('.wikibase-statementlistview-listview');

        // Create header
        const colIndex = $statementGroupView.find('.dc-grid-header').length;
        const $prev = $statementGroupView.find('.dc-grid-header:first-child,.dc-grid-header.dc-qualifier-header').last();
        const $el = $(`<div class="dc-grid-header dc-qual-col" style="grid-column:${colIndex}"></div>`).insertAfter($prev);
        $el.append($prop.clone(false, true));
        return colIndex;
      }

      async function insertColumn(ev, colPid, $addHeader) {
        const {entityFormatter, renderValue, mapValues} = tableView.utils;

        const colIndex = $(ev.target)
          .closest('.wikibase-statementgroupview')
          .find('.dc-grid-header')
          .length;
        // insert column header
        $(`<div class="dc-grid-header" style="grid-column:${colIndex}">${await entityFormatter.format(colPid)}</div>`).insertBefore($addHeader);
    
        const pid = $(ev.target)
          .closest('.wikibase-statementgroupview')
          .data('property-id');
    
          const sparql = tableView.templates.NEW_COL_SPARQL(mw.config.get('wbEntityId'), pid, colPid);
          const resp = await fetch('https://query.wikidata.org/sparql?format=json&query=' + encodeURIComponent(sparql))
            .then(r => r.json());
          
          
          const cleanedBindings = resp.results.bindings
          .map(b => mapValues(b, v => v.value))
          .map(b => {
            if (!b.type) return [b.x, null, null];
        
            switch (b.type) {
              case "time":
                return [b.x, b.type, {
                  time: b.timeTime,
                  precision: parseFloat(b.timePrecision),
                  timezone: parseFloat(b.timeTimezone),
                  calendarmodel: b.timeCalendarModel,
                  before: 0,
                  after: 0
                }];
              case "quantity":
                return [b.x, b.type, {
                  amount: parseFloat(b.quantityAmount),
                  unit: b.quantityUnit.endsWith("Q199") ? "1" : b.quantityUnit
                }];
              case "monolingualtext":
                return [b.x, b.type, {
                  text: b.monolingualtextText,
                  language: b.monolingualtextLanguage
                }];
              case "item":
                return [b.x, "wikibase-entityid", {
                  "entity-type": "item",
                  "numeric-id": parseFloat(b.itemId.match(/\d+$/)[0]),
                  id: b.itemId.match(/Q\d+$/)[0]
                }];
              case "string":
                return [b.x, b.type, b.string];
              default:
                throw new Error("NOT IMPLEMENTED");
            }
          });
        
          // Group results by key
          const resultsByKey = new Map();
          for (let [key, type, datavalue] of cleanedBindings) {
            key = key.split('/')[4];
            if (!datavalue) continue;
            const typedValue = dataValues.newDataValue(type, datavalue);
            if (resultsByKey.has(key)) resultsByKey.get(key).push(typedValue);
            else resultsByKey.set(key, [typedValue]);
          }
        
          // try to render!
          const statements = JSON.parse(mw.config.get('wbEntity')).claims[pid];
            
          const $lv = $(ev.target).closest('.wikibase-statementgroupview').find('.wikibase-statementlistview-listview');
          for (let i = 0; i < statements.length; i++) {
            const s = statements[i];
            const qid = s.mainsnak.datavalue.value.id;
            if (resultsByKey.has(qid)) {
              const $cell = $(`<div class="dc-extra-cell" style="grid-column:${colIndex}; grid-row: ${i+2}"></div>`);
              $lv.append($cell);
              const renderedItems = resultsByKey.get(qid)
                .map(data => {
                  const $div = $('<div/>');
                  $div.appendTo($cell);
                  renderValue($div, colPid, data);
                });
            }
            else {
              $lv.append(`<div class="dc-extra-cell" style="grid-column:${colIndex}; grid-row: ${i+2}">-</div>`);
            }
          }
      }
    };
  }

  const HTML_TEMPLATES = {
    LayoutOptions() {
      return `<div class="dc-statementgroupview-layout-options">
      [ <a href="javascript:;">List</a> | <a href="javascript:;">Table</a> ]
      </div>`;
    }
  };
  $('.wikibase-statementgroupview-property').append($(HTML_TEMPLATES.LayoutOptions()));

  $('.dc-statementgroupview-layout-options a').click((ev) => {
    const $el = $(ev.target);
    const $statementGroupView = $el.closest('.wikibase-statementgroupview');

    const viewsByName = {
      "Table": tableView,
      "List": listView
    }

    const selectedViewName = $el.text();
    const view = viewsByName[selectedViewName];
    if (view) view.selected($statementGroupView);
  });
  console.log("wdTableView Initialized.")
})();