Jump to content

MediaWiki:Gadget-templating.js: Difference between revisions

From National Library of Greece
No edit summary
No edit summary
Line 1: Line 1:
/* eslint-env browser, es6 */
/* global mw, $ */
/* global mw, $ */
mw.loader.using(['mediawiki.api']).done(function () {


mw.loader.using(['mediawiki.api']).then(function () {
  // ===== 0) CONFIG =====
   (async function () {
  var SEARCH_QUERY = 'haswbstatement:P1=Q196450'; // exactly what works in Special:Search
    const api = new mw.Api();
   var WbTemplates = {};
    const userLang = mw.config.get('wgUserLanguage') || 'en';
  var api = new mw.Api();
  var userLang = mw.config.get('wgUserLanguage') || 'en';


    // ---- 1) CONFIG: how we find your template items ----
  // ===== 1) Small helpers (ES5-safe) =====
     // Use exactly what works in the search box:
  function chunk(arr, n) {
     const SEARCH_QUERY = 'haswbstatement:P1=Q196450';
    var out = [], i;
     // If needed, you can hardcode Q-IDs instead and skip searchTemplates().
    for (i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n));
    return out;
  }
  function getBestLabel(labels) {
    if (!labels) return undefined;
    if (labels[userLang] && labels[userLang].value) return labels[userLang].value;
    for (var k in labels) {
      if (labels[k] && labels[k].value) return labels[k].value;
    }
    return undefined;
  }
  function isPlaceholderSnak(snak) {
    if (!snak || snak.snaktype !== 'value' || !snak.datavalue) return false;
    var dt = snak.datatype;
    var v = snak.datavalue.value;
    if (dt === 'string' || dt === 'external-id' || dt === 'url') return v === '_';
    if (dt === 'monolingualtext') return v && v.text === '_';
    return false;
  }
  function cleanTemplateLabel(raw) {
    if (!raw) return '';
     return raw.replace(/^RDA\s*/i, '').replace(/\s*Template$/i, '').trim();
  }
  function slugify(s) {
     if (!s) return '';
     // simple slug (ASCII only)
    return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
  }


    // ---- 2) HELPERS used by the builder ----
  // ===== 2) API: search → entities → property meta =====
    const chunk = (arr, n) => { const out=[]; for (let i=0;i<arr.length;i+=n) out.push(arr.slice(i,i+n)); return out; };
  function searchTemplates(query) {
    const getBestLabel = (labels) =>
     return api.get({
      (labels?.[userLang]?.value) || (labels && Object.values(labels)[0]?.value) || undefined;
       action: 'query',
 
       list: 'search',
     // decide if "_" means "placeholder" for string-like types
       srsearch: query,
    const isPlaceholderSnak = (snak) => {
       srbackend: 'CirrusSearch',
       if (!snak || snak.snaktype !== 'value' || !snak.datavalue) return false;
      srqiprofile: 'wikibase',
       const dt = snak.datatype;
       srlimit: 50,
       const v  = snak.datavalue.value;
       formatversion: 2
       if (dt === 'string' || dt === 'external-id' || dt === 'url') return v === '_';
     }).then(function (res) {
       if (dt === 'monolingualtext') return v?.text === '_';
      var hits = (res.query && res.query.search) || [];
       return false;
      var ids = [];
     };
       for (var i = 0; i < hits.length; i++) {
 
        var m = /Q\d+$/.exec(hits[i].title);
    const cleanTemplateLabel = (raw) => (raw || '')
        if (m) ids.push(m[0]);
       .replace(/^RDA\s*/i, '')
       }
      .replace(/\s*Template$/i, '')
       return ids;
      .trim();
    });
 
  }
    const slugify = (s) => (s || '')
      .normalize('NFKD')
      .replace(/[\u0300-\u036f]/g, '')
       .toLowerCase()
       .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '');


     // ---- 3) BUILDER: search → fetch entities → fetch property meta → transform → WbTemplates ----
  function fetchEntities(ids) {
     async function searchTemplates(query) {
    var out = {};
       const res = await api.get({
     var batches = chunk(ids, 50);
         action: 'query',
    var d = $.Deferred();
         list: 'search',
     (function next(i) {
         srsearch: query,
       if (i >= batches.length) { d.resolve(out); return; }
        // make API behave like Special:Search
      api.get({
        srbackend: 'CirrusSearch',
         action: 'wbgetentities',
        srqiprofile: 'wikibase',
         ids: batches[i].join('|'),
         srlimit: 50,
         props: 'labels|claims',
        formatversion: 2
         languages: userLang
       });
       }).then(function (res) {
      return (res.query?.search || [])
        $.extend(out, res.entities || {});
         .map(h => (h.title.match(/Q\d+$/) || [])[0])
         next(i + 1);
        .filter(Boolean);
      }).fail(d.reject);
    }
    })(0);
    return d.promise();
  }


    async function fetchEntities(ids) {
  function fetchPropertyMeta(entityMap) {
      const map = {};
    var pids = {};
      for (const group of chunk(ids, 50)) {
    for (var qid in entityMap) {
        const res = await api.get({
      var claims = entityMap[qid].claims || {};
          action: 'wbgetentities',
       for (var pid in claims) pids[pid] = true;
          ids: group.join('|'),
          props: 'labels|claims',
          languages: userLang
        });
        Object.assign(map, res.entities || {});
       }
      return map;
     }
     }
 
     var ids = Object.keys(pids);
     async function fetchPropertyMeta(entityMap) {
    var out = {};
      const pids = new Set();
    var batches = chunk(ids, 50);
      Object.values(entityMap).forEach(e => Object.keys(e.claims || {}).forEach(pid => pids.add(pid)));
    var d = $.Deferred();
      const map = {};
    (function next(i) {
       for (const group of chunk([...pids], 50)) {
      if (i >= batches.length) { d.resolve(out); return; }
        const res = await api.get({
       api.get({
          action: 'wbgetentities',
        action: 'wbgetentities',
          ids: group.join('|'),
        ids: batches[i].join('|'),
          props: 'labels|datatype',
        props: 'labels|datatype',
          languages: userLang
        languages: userLang
        });
      }).then(function (res) {
         Object.assign(map, res.entities || {});
         $.extend(out, res.entities || {});
       }
        next(i + 1);
       }).fail(d.reject);
    })(0);
    return d.promise().then(function () {
       // normalize
       // normalize
       const meta = {};
       var meta = {};
       for (const [pid, ent] of Object.entries(map)) {
       for (var pid in out) {
        var ent = out[pid] || {};
         meta[pid] = {
         meta[pid] = {
           id: pid,
           id: pid,
           label: getBestLabel(ent.labels) || pid,
           label: getBestLabel(ent.labels) || pid,
           datatype: ent.datatype // e.g., 'wikibase-item', 'string', 'globe-coordinate'
           datatype: ent.datatype
         };
         };
       }
       }
       return meta;
       return meta;
     }
     });
  }


    function transformToTemplate(entity, propMeta) {
  // ===== 3) Transform to your WbTemplates shape =====
      const rawLabel = getBestLabel(entity.labels) || entity.id;
  function transformToTemplate(entity, propMeta) {
      const label   = cleanTemplateLabel(rawLabel);
    var rawLabel = getBestLabel(entity.labels) || entity.id;
    var label = cleanTemplateLabel(rawLabel);


      const prefilledClaims = [];
    var prefilledClaims = [];
      const userDefinedClaims = [];
    var userDefinedClaims = [];


      for (const [pid, statements] of Object.entries(entity.claims || {})) {
    var claims = entity.claims || {};
        if (!Array.isArray(statements) || !statements.length) continue;
    for (var pid in claims) {
        const prop = propMeta[pid] || { id: pid, label: pid, datatype: 'string' };
      var statements = claims[pid];
      if (!statements || !statements.length) continue;


        // first non-placeholder statement (if any)
      var prop = propMeta[pid] || { id: pid, label: pid, datatype: 'string' };
        const firstNonPlaceholder = statements
          .map(st => st.mainsnak)
          .find(ms => ms && ms.snaktype === 'value' && !isPlaceholderSnak(ms));


        if (firstNonPlaceholder) {
      // find first non-placeholder
          // NOTE: to keep compatibility with your prefillStatement (which assumes wikibase-item),
      var firstNonPlaceholder = null;
          // we only prefill non-placeholder values for wikibase-item. Others go to userDefined.
      for (var i = 0; i < statements.length; i++) {
          if (prop.datatype === 'wikibase-item') {
        var ms = statements[i] && statements[i].mainsnak;
            const v = firstNonPlaceholder.datavalue?.value; // {id: 'Qxx'}
        if (ms && ms.snaktype === 'value' && !isPlaceholderSnak(ms)) {
            prefilledClaims.push({
           firstNonPlaceholder = ms;
              property: { id: prop.id, label: prop.label, datatype: prop.datatype },
           break;
              value: { mainVal: { id: v?.id, datatype: 'wikibase-item' } }
            });
          } else {
            userDefinedClaims.push({ property: { id: prop.id, label: prop.label }, datatype: prop.datatype });
          }
        } else {
           // all statements are "_" placeholders → user fills it
           userDefinedClaims.push({ property: { id: prop.id, label: prop.label }, datatype: prop.datatype });
         }
         }
       }
       }
       return { key: slugify(label), label, prefilledClaims, userDefinedClaims };
 
       if (firstNonPlaceholder && prop.datatype === 'wikibase-item') {
        var v = firstNonPlaceholder.datavalue && firstNonPlaceholder.datavalue.value;
        prefilledClaims.push({
          property: { id: prop.id, label: prop.label, datatype: prop.datatype },
          value: { mainVal: { id: v && v.id, datatype: 'wikibase-item' } }
        });
      } else {
        userDefinedClaims.push({
          property: { id: prop.id, label: prop.label },
          datatype: prop.datatype
        });
      }
     }
     }


     async function buildWbTemplatesFromQuery(query) {
     return { key: slugify(label), label: label, prefilledClaims: prefilledClaims, userDefinedClaims: userDefinedClaims };
      const qids = await searchTemplates(query);
  }
 
  function buildWbTemplatesFromQuery(query) {
    return searchTemplates(query).then(function (qids) {
       if (!qids.length) return {};
       if (!qids.length) return {};
       const entities = await fetchEntities(qids);
       return fetchEntities(qids).then(function (entities) {
      const propMeta = await fetchPropertyMeta(entities);
        return fetchPropertyMeta(entities).then(function (propMeta) {
 
          var map = {};
      const map = {};
          for (var eid in entities) {
      Object.values(entities).forEach(e => {
            var tpl = transformToTemplate(entities[eid], propMeta);
        const tpl = transformToTemplate(e, propMeta);
            map[tpl.key] = {
        map[tpl.key] = { label: tpl.label, prefilledClaims: tpl.prefilledClaims, userDefinedClaims: tpl.userDefinedClaims };
              label: tpl.label,
              prefilledClaims: tpl.prefilledClaims,
              userDefinedClaims: tpl.userDefinedClaims
            };
          }
          return map;
        });
       });
       });
      return map;
    });
    }
  }


    // ---- 4) Build WbTemplates dynamically ----
  // ===== 4) YOUR existing helpers (unchanged) =====
     // If you need to debug, console.log(WbTemplates) after the await.
  function statementExists(property, value) {
    const WbTemplates = await buildWbTemplatesFromQuery(SEARCH_QUERY);
     items.toArray().some(function (item) {
      return !!$(item).data().propertyId &&
        $(item).data().statementgroupview.options.value.getKey() === property &&
        $(item).data().statementgroupview.options.value.getItemContainer().toArray().some(function (claimItem) {
          return claimItem.getClaim().getMainSnak().getValue().getSerialization() === value;
        });
    });
  }


    // Expose globally so your existing helpers can access it
  function getPropertyEntity(property) {
    window.WbTemplates = WbTemplates;
     return {
 
      id: String(property.id),
    // =========================
       title: 'Property:' + property.id,
    // YOUR EXISTING FUNCTIONS
      datatype: property.datatype,
    // (left untouched, just moved below the builder so they can use WbTemplates)
      label: property.label
     // =========================
    };
 
  }
    function statementExists(property, value) {
       items.toArray().some(item => !!$(item).data().propertyId && $(item).data().statementgroupview.options.value.getKey() === property &&
        $(item).data().statementgroupview.options.value.getItemContainer().toArray().some(claimItem =>
          claimItem.getClaim().getMainSnak().getValue().getSerialization() === value));
    }


    function getPropertyEntity(property) {
  function prefillStatement(claim, onlyProperty) {
      return {
    if (onlyProperty) {
        "id": `${property.id}`,
      pendingStatement(claim.property);
        "title": `Property:${property.id}`,
       return;
        "datatype": property.datatype,
        "label": property.label,
       };
     }
     }
    var statementListView = $('.wikibase-statementgrouplistview').first().data().wikibaseStatementgrouplistview;
    statementListView.enterNewItem();


     function prefillStatement(claim, onlyProperty) {
     var items = statementListView.listview.items();
      if (onlyProperty) {
    var item = items.last().data();
        pendingStatement(claim.property);
    var sv = item.statementgroupview.statementlistview._listview.items().first().data().statementview;
        return;
    var snak = sv.$mainSnak.data().snakview;
      }
      let statementListView = $(".wikibase-statementgrouplistview").first().data().wikibaseStatementgrouplistview;
      statementListView.enterNewItem();
 
      let items = statementListView.listview.items();
      let item = items.last().data();
      let sv = item.statementgroupview.statementlistview._listview.items().first().data().statementview;
      let snak = sv.$mainSnak.data().snakview;


      let es = getPropertyEntity(claim.property);
    var es = getPropertyEntity(claim.property);


      let selector = snak._getPropertySelector();
    var selector = snak._getPropertySelector();
      selector.element.val(es.label);
    selector.element.val(es.label);
      selector.element.attr("title", es.label);
    selector.element.attr('title', es.label);
      selector._trigger('change');
    selector._trigger('change');
      selector._select(es);
    selector._select(es);


      snak._variation._valueView.element.one(
    snak._variation._valueView.element.one(
        snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
      snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
        function () {
      function () {
          let valSelector = snak._variation._valueView._expert.$input.data().entityselector;
        var valSelector = snak._variation._valueView._expert.$input.data().entityselector;
          let valval = getPropertyEntity(claim.value.mainVal);
        var valval = getPropertyEntity(claim.value.mainVal);


          valSelector.element.val(valval.label);
        valSelector.element.val(valval.label);
          valSelector._trigger("change");
        valSelector._trigger('change');
          valSelector._select(valval);
        valSelector._select(valval);


          if (!!claim.qualifiers) {
        if (!!claim.qualifiers) {
            let qlistview = sv._qualifiers;
          var qlistview = sv._qualifiers;
            qlistview.enterNewItem();
          qlistview.enterNewItem();


            var qsnaklistview = qlistview.value()[qlistview.value().length - 1];
          var qsnaklistview = qlistview.value()[qlistview.value().length - 1];
            qsnaklistview.enterNewItem();
          qsnaklistview.enterNewItem();
            let qslv = $(sv._qualifiers.items().first()).data().snaklistview;
          var qslv = $(sv._qualifiers.items().first()).data().snaklistview;
            let qsnak = qslv._listview.items().first().data().snakview;
          var qsnak = qslv._listview.items().first().data().snakview;


            let qsel = qsnak._getPropertySelector();
          var qsel = qsnak._getPropertySelector();
            qsel.element.val(es.label);
          qsel.element.val(es.label);
            qsel._trigger('change');
          qsel._trigger('change');
            qsel._select(es);
          qsel._select(es);


            qsnak._variation._valueView.element.one(
          qsnak._variation._valueView.element.one(
              qsnak._variation._valueView.widgetEventPrefix + 'afterstartediting',
            qsnak._variation._valueView.widgetEventPrefix + 'afterstartediting',
              function () {
            function () {
                let qvalSelector = qsnak._variation._valueView._expert.$input.data().entityselector;
              var qvalSelector = qsnak._variation._valueView._expert.$input.data().entityselector;
                let qvalval = getPropertyEntity(claim.value.mainVal);
              var qvalval = getPropertyEntity(claim.value.mainVal);


                qvalSelector.element.val(qvalval.label);
              qvalSelector.element.val(qvalval.label);
                qvalSelector._trigger("change");
              qvalSelector._trigger('change');


                valSelector._trigger("change");
              valSelector._trigger('change');
                qvalSelector._select(qvalval);
              qvalSelector._select(qvalval);
              }
            }
            );
          );


            qsnak._variation._valueView.element.one(
          qsnak._variation._valueView.element.one(
              qsnak._variation._valueView.widgetEventPrefix + 'change',
            qsnak._variation._valueView.widgetEventPrefix + 'change',
              function () {
            function () {
                let toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
              var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
                toolbar._controller.stopEditing(false);
              toolbar._controller.stopEditing(false);
              });
            });
          }
         }
         }
       );
       }
    );


      snak._variation._valueView.element.one(
    snak._variation._valueView.element.one(
        snak._variation._valueView.widgetEventPrefix + 'change',
      snak._variation._valueView.widgetEventPrefix + 'change',
        function () {
      function () {
          let toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
        var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
          toolbar._controller.stopEditing(false);
        toolbar._controller.stopEditing(false);
        });
      });
    }
  }


    function pendingStatement(claim) {
  function pendingStatement(claim) {
      let prop = claim.property;
    var prop = claim.property;
      return new Promise((resolve, reject) => {
    return new Promise(function (resolve, reject) {
        let statementListView = $(".wikibase-statementgrouplistview")
      var statementListView = $('.wikibase-statementgrouplistview').first().data().wikibaseStatementgrouplistview;
          .first()
      statementListView.enterNewItem();
          .data().wikibaseStatementgrouplistview;
      var items = statementListView.listview.items();
        statementListView.enterNewItem();
      var item = items.last().data();
        let items = statementListView.listview.items();
      var sv = item.statementgroupview.statementlistview._listview.items();
        let item = items.last().data();
      var snak = $(sv).data().statementview.$mainSnak.data().snakview;
        let sv = item.statementgroupview.statementlistview._listview.items();
        let snak = $(sv).data().statementview.$mainSnak.data().snakview;
 
        let selector = snak._getPropertySelector();


        // Set up event listeners and chain them to resolve when finished.
      var selector = snak._getPropertySelector();
        selector.element.on('entityselectorselected', (event, entityId) => {
          $(snak._variation).on("afterdraw", function () {
            snak._variation._valueView.element.one(
              snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
              function () {
                if (claim.datatype === "wikibase-item") {
                  let valSelector = snak._variation._valueView._expert.$input.data().entityselector;
                  let jqe = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;


                  valSelector.element.val("temp");
      selector.element.on('entityselectorselected', function () {
                  valSelector._trigger("change");
        $(snak._variation).on('afterdraw', function () {
                  valSelector._select({ id: "Q80", datatype: "wikibase-item" });
          snak._variation._valueView.element.one(
                  jqe.handlers[0].call();
            snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
                  jqe.handlers[1].call();
            function () {
              if (claim.datatype === 'wikibase-item') {
                var valSelector = snak._variation._valueView._expert.$input.data().entityselector;
                var jqe = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;


                  valSelector.element.val("");
                valSelector.element.val('temp');
                  valSelector._trigger("change");
                valSelector._trigger('change');
                  jqe.handlers[0].call();
                valSelector._select({ id: 'Q80', datatype: 'wikibase-item' });
                  jqe.handlers[1].call();
                jqe.handlers[0].call();
                }
                jqe.handlers[1].call();
                else {
                  let valSelector = snak._variation._valueView._expert.$input.data().inputautoexpand;
                  let jqe = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;


                  if (claim.datatype === "globe-coordinate") {
                valSelector.element.val('');
                    valSelector.$input.val("0,0");
                valSelector._trigger('change');
                    jqe.handlers[0].call();
                jqe.handlers[0].call();
                    jqe.handlers[1].call();
                jqe.handlers[1].call();
                  }
              } else {
                  else {
                var valSelector2 = snak._variation._valueView._expert.$input.data().inputautoexpand;
                    valSelector.$input.val("__"); // shows the placeholder behavior
                var jqe2 = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;
                    jqe.handlers[0].call();
                    jqe.handlers[1].call();
                  }


                   valSelector.$input.val("");
                if (claim.datatype === 'globe-coordinate') {
                   jqe.handlers[0].call();
                  valSelector2.$input.val('0,0');
                   jqe.handlers[1].call();
                  jqe2.handlers[0].call();
                  jqe2.handlers[1].call();
                } else {
                   valSelector2.$input.val('__');
                   jqe2.handlers[0].call();
                   jqe2.handlers[1].call();
                 }
                 }
 
                valSelector2.$input.val('');
                 // Once processing is complete, resolve the Promise.
                 jqe2.handlers[0].call();
                 resolve();
                 jqe2.handlers[1].call();
               }
               }
            );
              resolve();
           });
            }
           );
         });
         });
      });
      selector.element.val(prop.label);
      selector._trigger('change');
      selector._select(prop);
    });
  }
  function addDetails(templateName) {
    var claims = WbTemplates[templateName].userDefinedClaims;
    var chain = Promise.resolve();
    claims.forEach(function (claim) {
      chain = chain.then(function () { return pendingStatement(claim); });
    });
    return chain;
  }


        selector.element.val(prop.label);
  function applyTemplate(templateName) {
        selector._trigger('change');
    WbTemplates[templateName].prefilledClaims.forEach(function (claim) {
        selector._select(prop);
      prefillStatement(claim, false);
      });
    });
    }
  }


    function addDetails(templateName) {
  // ===== 5) UI (render AFTER templates are loaded) =====
       var claims = WbTemplates[templateName].userDefinedClaims;
  function initTemplatingUI() {
       var chain = Promise.resolve();
    var html = ''
      + '<div class="wikibase-templating-section">'
      + '  <div class="wikibase-templating-section-title"><span class="magic-wand-icon"></span> RDA Entity template</div>'
       + '  <div class="wikibase-templating-help"><span class="settings-text">Select an RDA entity class to apply predefined template entries and additional entity details.</span></div>'
       + '  <div class="wikibase-templating-form oo-ui-labelElement oo-ui-fieldLayout">'
      + '    <div class=""><label for="wbtemplate">Entity class</label></div>'
      + '    <div><select id="wbtemplate" class="cdx-select"></select></div>'
      + '  </div>'
      + '  <div class="mw-htmlform-submit-buttons">'
      + '    <button id="applyTemplateBtn" class="cdx-button">Apply template</button>'
      + '    <button id="addDetailsBtn" class="cdx-button">Add details</button>'
      + '  </div>'
      + '</div>';


      claims.forEach(function (claim) {
    $('.wikibase-entitytermsview').append(html);
        chain = chain.then(function () {
          return pendingStatement(claim);
        });
      });


       return chain;
    var key;
    for (key in WbTemplates) {
       $('#wbtemplate').append($('<option>', {
        value: key,
        text: WbTemplates[key].label
      }));
     }
     }


     function applyTemplate(templateName) {
     $('#applyTemplateBtn').on('click', function () {
       WbTemplates[templateName].prefilledClaims.forEach(claim => {
       applyTemplate($('#wbtemplate').find(':selected').val());
        prefillStatement(claim, false);
    });
      });
    $('#addDetailsBtn').on('click', function () {
    }
      addDetails($('#wbtemplate').find(':selected').val());
    });
  }


    // ---- 5) UI: render only after WbTemplates is ready ----
  // ===== 6) Build templates, then render UI =====
     function initTemplatingUI() {
  buildWbTemplatesFromQuery(SEARCH_QUERY)
       // If nothing came back, don't render the UI
     .then(function (map) {
       WbTemplates = map;
      window.WbTemplates = WbTemplates; // if you need it elsewhere
       if (!WbTemplates || !Object.keys(WbTemplates).length) {
       if (!WbTemplates || !Object.keys(WbTemplates).length) {
         console.warn('No templates found for query:', SEARCH_QUERY);
         console.warn('No templates found for query:', SEARCH_QUERY);
         return;
         return;
       }
       }
      initTemplatingUI();
    })
    .fail(function (err) {
      // If this fails for anonymous users, it might be auth/profile config
      // related; try while logged in.
      console.error('Template build failed:', err);
    });


      $(".wikibase-entitytermsview").append(`
<div class="wikibase-templating-section">
  <div class="wikibase-templating-section-title"><span class="magic-wand-icon"></span> RDA Entity template</div>
  <div class="wikibase-templating-help"><span class="settings-text">Select an RDA entity class to apply predefined template entries and additional entity details.</span></div>
  <div class="wikibase-templating-form oo-ui-labelElement oo-ui-fieldLayout">
    <div class=""><label for="wbtemplate">Entity class</label></div>
    <div>
      <select id="wbtemplate" class="cdx-select"></select>
    </div>
  </div>
  <div class="mw-htmlform-submit-buttons">
    <button id="applyTemplateBtn" class="cdx-button">Apply template</button>
    <button id="addDetailsBtn" class="cdx-button">Add details</button>
  </div>
</div>
      `);
      // Populate selector
      Object.entries(WbTemplates).forEach(([key, tpl]) => {
        $('#wbtemplate').append($('<option>', { value: key, text: tpl.label }));
      });
      // Wire buttons
      $("#applyTemplateBtn").on("click", function () {
        applyTemplate($("#wbtemplate").find(":selected").val());
      });
      $("#addDetailsBtn").on("click", function () {
        addDetails($("#wbtemplate").find(":selected").val());
      });
    }
    // finally render
    initTemplatingUI();
  })().catch(console.error);
});
});

Revision as of 18:31, 17 September 2025

/* global mw, $ */
mw.loader.using(['mediawiki.api']).done(function () {

  // ===== 0) CONFIG =====
  var SEARCH_QUERY = 'haswbstatement:P1=Q196450'; // exactly what works in Special:Search
  var WbTemplates = {};
  var api = new mw.Api();
  var userLang = mw.config.get('wgUserLanguage') || 'en';

  // ===== 1) Small helpers (ES5-safe) =====
  function chunk(arr, n) {
    var out = [], i;
    for (i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n));
    return out;
  }
  function getBestLabel(labels) {
    if (!labels) return undefined;
    if (labels[userLang] && labels[userLang].value) return labels[userLang].value;
    for (var k in labels) {
      if (labels[k] && labels[k].value) return labels[k].value;
    }
    return undefined;
  }
  function isPlaceholderSnak(snak) {
    if (!snak || snak.snaktype !== 'value' || !snak.datavalue) return false;
    var dt = snak.datatype;
    var v = snak.datavalue.value;
    if (dt === 'string' || dt === 'external-id' || dt === 'url') return v === '_';
    if (dt === 'monolingualtext') return v && v.text === '_';
    return false;
  }
  function cleanTemplateLabel(raw) {
    if (!raw) return '';
    return raw.replace(/^RDA\s*/i, '').replace(/\s*Template$/i, '').trim();
  }
  function slugify(s) {
    if (!s) return '';
    // simple slug (ASCII only)
    return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
  }

  // ===== 2) API: search → entities → property meta =====
  function searchTemplates(query) {
    return api.get({
      action: 'query',
      list: 'search',
      srsearch: query,
      srbackend: 'CirrusSearch',
      srqiprofile: 'wikibase',
      srlimit: 50,
      formatversion: 2
    }).then(function (res) {
      var hits = (res.query && res.query.search) || [];
      var ids = [];
      for (var i = 0; i < hits.length; i++) {
        var m = /Q\d+$/.exec(hits[i].title);
        if (m) ids.push(m[0]);
      }
      return ids;
    });
  }

  function fetchEntities(ids) {
    var out = {};
    var batches = chunk(ids, 50);
    var d = $.Deferred();
    (function next(i) {
      if (i >= batches.length) { d.resolve(out); return; }
      api.get({
        action: 'wbgetentities',
        ids: batches[i].join('|'),
        props: 'labels|claims',
        languages: userLang
      }).then(function (res) {
        $.extend(out, res.entities || {});
        next(i + 1);
      }).fail(d.reject);
    })(0);
    return d.promise();
  }

  function fetchPropertyMeta(entityMap) {
    var pids = {};
    for (var qid in entityMap) {
      var claims = entityMap[qid].claims || {};
      for (var pid in claims) pids[pid] = true;
    }
    var ids = Object.keys(pids);
    var out = {};
    var batches = chunk(ids, 50);
    var d = $.Deferred();
    (function next(i) {
      if (i >= batches.length) { d.resolve(out); return; }
      api.get({
        action: 'wbgetentities',
        ids: batches[i].join('|'),
        props: 'labels|datatype',
        languages: userLang
      }).then(function (res) {
        $.extend(out, res.entities || {});
        next(i + 1);
      }).fail(d.reject);
    })(0);
    return d.promise().then(function () {
      // normalize
      var meta = {};
      for (var pid in out) {
        var ent = out[pid] || {};
        meta[pid] = {
          id: pid,
          label: getBestLabel(ent.labels) || pid,
          datatype: ent.datatype
        };
      }
      return meta;
    });
  }

  // ===== 3) Transform to your WbTemplates shape =====
  function transformToTemplate(entity, propMeta) {
    var rawLabel = getBestLabel(entity.labels) || entity.id;
    var label = cleanTemplateLabel(rawLabel);

    var prefilledClaims = [];
    var userDefinedClaims = [];

    var claims = entity.claims || {};
    for (var pid in claims) {
      var statements = claims[pid];
      if (!statements || !statements.length) continue;

      var prop = propMeta[pid] || { id: pid, label: pid, datatype: 'string' };

      // find first non-placeholder
      var firstNonPlaceholder = null;
      for (var i = 0; i < statements.length; i++) {
        var ms = statements[i] && statements[i].mainsnak;
        if (ms && ms.snaktype === 'value' && !isPlaceholderSnak(ms)) {
          firstNonPlaceholder = ms;
          break;
        }
      }

      if (firstNonPlaceholder && prop.datatype === 'wikibase-item') {
        var v = firstNonPlaceholder.datavalue && firstNonPlaceholder.datavalue.value;
        prefilledClaims.push({
          property: { id: prop.id, label: prop.label, datatype: prop.datatype },
          value: { mainVal: { id: v && v.id, datatype: 'wikibase-item' } }
        });
      } else {
        userDefinedClaims.push({
          property: { id: prop.id, label: prop.label },
          datatype: prop.datatype
        });
      }
    }

    return { key: slugify(label), label: label, prefilledClaims: prefilledClaims, userDefinedClaims: userDefinedClaims };
  }

  function buildWbTemplatesFromQuery(query) {
    return searchTemplates(query).then(function (qids) {
      if (!qids.length) return {};
      return fetchEntities(qids).then(function (entities) {
        return fetchPropertyMeta(entities).then(function (propMeta) {
          var map = {};
          for (var eid in entities) {
            var tpl = transformToTemplate(entities[eid], propMeta);
            map[tpl.key] = {
              label: tpl.label,
              prefilledClaims: tpl.prefilledClaims,
              userDefinedClaims: tpl.userDefinedClaims
            };
          }
          return map;
        });
      });
    });
  }

  // ===== 4) YOUR existing helpers (unchanged) =====
  function statementExists(property, value) {
    items.toArray().some(function (item) {
      return !!$(item).data().propertyId &&
        $(item).data().statementgroupview.options.value.getKey() === property &&
        $(item).data().statementgroupview.options.value.getItemContainer().toArray().some(function (claimItem) {
          return claimItem.getClaim().getMainSnak().getValue().getSerialization() === value;
        });
    });
  }

  function getPropertyEntity(property) {
    return {
      id: String(property.id),
      title: 'Property:' + property.id,
      datatype: property.datatype,
      label: property.label
    };
  }

  function prefillStatement(claim, onlyProperty) {
    if (onlyProperty) {
      pendingStatement(claim.property);
      return;
    }
    var statementListView = $('.wikibase-statementgrouplistview').first().data().wikibaseStatementgrouplistview;
    statementListView.enterNewItem();

    var items = statementListView.listview.items();
    var item = items.last().data();
    var sv = item.statementgroupview.statementlistview._listview.items().first().data().statementview;
    var snak = sv.$mainSnak.data().snakview;

    var es = getPropertyEntity(claim.property);

    var selector = snak._getPropertySelector();
    selector.element.val(es.label);
    selector.element.attr('title', es.label);
    selector._trigger('change');
    selector._select(es);

    snak._variation._valueView.element.one(
      snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
      function () {
        var valSelector = snak._variation._valueView._expert.$input.data().entityselector;
        var valval = getPropertyEntity(claim.value.mainVal);

        valSelector.element.val(valval.label);
        valSelector._trigger('change');
        valSelector._select(valval);

        if (!!claim.qualifiers) {
          var qlistview = sv._qualifiers;
          qlistview.enterNewItem();

          var qsnaklistview = qlistview.value()[qlistview.value().length - 1];
          qsnaklistview.enterNewItem();
          var qslv = $(sv._qualifiers.items().first()).data().snaklistview;
          var qsnak = qslv._listview.items().first().data().snakview;

          var qsel = qsnak._getPropertySelector();
          qsel.element.val(es.label);
          qsel._trigger('change');
          qsel._select(es);

          qsnak._variation._valueView.element.one(
            qsnak._variation._valueView.widgetEventPrefix + 'afterstartediting',
            function () {
              var qvalSelector = qsnak._variation._valueView._expert.$input.data().entityselector;
              var qvalval = getPropertyEntity(claim.value.mainVal);

              qvalSelector.element.val(qvalval.label);
              qvalSelector._trigger('change');

              valSelector._trigger('change');
              qvalSelector._select(qvalval);
            }
          );

          qsnak._variation._valueView.element.one(
            qsnak._variation._valueView.widgetEventPrefix + 'change',
            function () {
              var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
              toolbar._controller.stopEditing(false);
            });
        }
      }
    );

    snak._variation._valueView.element.one(
      snak._variation._valueView.widgetEventPrefix + 'change',
      function () {
        var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
        toolbar._controller.stopEditing(false);
      });
  }

  function pendingStatement(claim) {
    var prop = claim.property;
    return new Promise(function (resolve, reject) {
      var statementListView = $('.wikibase-statementgrouplistview').first().data().wikibaseStatementgrouplistview;
      statementListView.enterNewItem();
      var items = statementListView.listview.items();
      var item = items.last().data();
      var sv = item.statementgroupview.statementlistview._listview.items();
      var snak = $(sv).data().statementview.$mainSnak.data().snakview;

      var selector = snak._getPropertySelector();

      selector.element.on('entityselectorselected', function () {
        $(snak._variation).on('afterdraw', function () {
          snak._variation._valueView.element.one(
            snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
            function () {
              if (claim.datatype === 'wikibase-item') {
                var valSelector = snak._variation._valueView._expert.$input.data().entityselector;
                var jqe = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;

                valSelector.element.val('temp');
                valSelector._trigger('change');
                valSelector._select({ id: 'Q80', datatype: 'wikibase-item' });
                jqe.handlers[0].call();
                jqe.handlers[1].call();

                valSelector.element.val('');
                valSelector._trigger('change');
                jqe.handlers[0].call();
                jqe.handlers[1].call();
              } else {
                var valSelector2 = snak._variation._valueView._expert.$input.data().inputautoexpand;
                var jqe2 = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;

                if (claim.datatype === 'globe-coordinate') {
                  valSelector2.$input.val('0,0');
                  jqe2.handlers[0].call();
                  jqe2.handlers[1].call();
                } else {
                  valSelector2.$input.val('__');
                  jqe2.handlers[0].call();
                  jqe2.handlers[1].call();
                }
                valSelector2.$input.val('');
                jqe2.handlers[0].call();
                jqe2.handlers[1].call();
              }
              resolve();
            }
          );
        });
      });

      selector.element.val(prop.label);
      selector._trigger('change');
      selector._select(prop);
    });
  }

  function addDetails(templateName) {
    var claims = WbTemplates[templateName].userDefinedClaims;
    var chain = Promise.resolve();
    claims.forEach(function (claim) {
      chain = chain.then(function () { return pendingStatement(claim); });
    });
    return chain;
  }

  function applyTemplate(templateName) {
    WbTemplates[templateName].prefilledClaims.forEach(function (claim) {
      prefillStatement(claim, false);
    });
  }

  // ===== 5) UI (render AFTER templates are loaded) =====
  function initTemplatingUI() {
    var html = ''
      + '<div class="wikibase-templating-section">'
      + '  <div class="wikibase-templating-section-title"><span class="magic-wand-icon"></span> RDA Entity template</div>'
      + '  <div class="wikibase-templating-help"><span class="settings-text">Select an RDA entity class to apply predefined template entries and additional entity details.</span></div>'
      + '  <div class="wikibase-templating-form oo-ui-labelElement oo-ui-fieldLayout">'
      + '    <div class=""><label for="wbtemplate">Entity class</label></div>'
      + '    <div><select id="wbtemplate" class="cdx-select"></select></div>'
      + '  </div>'
      + '  <div class="mw-htmlform-submit-buttons">'
      + '    <button id="applyTemplateBtn" class="cdx-button">Apply template</button>'
      + '    <button id="addDetailsBtn" class="cdx-button">Add details</button>'
      + '  </div>'
      + '</div>';

    $('.wikibase-entitytermsview').append(html);

    var key;
    for (key in WbTemplates) {
      $('#wbtemplate').append($('<option>', {
        value: key,
        text: WbTemplates[key].label
      }));
    }

    $('#applyTemplateBtn').on('click', function () {
      applyTemplate($('#wbtemplate').find(':selected').val());
    });
    $('#addDetailsBtn').on('click', function () {
      addDetails($('#wbtemplate').find(':selected').val());
    });
  }

  // ===== 6) Build templates, then render UI =====
  buildWbTemplatesFromQuery(SEARCH_QUERY)
    .then(function (map) {
      WbTemplates = map;
      window.WbTemplates = WbTemplates; // if you need it elsewhere
      if (!WbTemplates || !Object.keys(WbTemplates).length) {
        console.warn('No templates found for query:', SEARCH_QUERY);
        return;
      }
      initTemplatingUI();
    })
    .fail(function (err) {
      // If this fails for anonymous users, it might be auth/profile config
      // related; try while logged in.
      console.error('Template build failed:', err);
    });

});