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:
console.log('Wikibase Templating Gadget loaded');
/* eslint-env browser, es6 */
$.getJSON('https://wikibase2.nlg.okfn.gr/wiki/Special:EntityData/Q196451.json', function (data) {
/* global mw, $ */
    console.log(data); // do something with the JSON
});
WbTemplates = {
    "person": {
    "label": "Person",
        "prefilledClaims": [{
            "property": {id: "P1", label: "instance of", datatype: "wikibase-item"},
            "value": {
                "mainVal": {id: "Q78", datatype: "wikibase-item"}
            }


        }],
mw.loader.using(['mediawiki.api']).then(function () {
        "userDefinedClaims": [
  (async function () {
        {
    const api = new mw.Api();
        "property": {id: "P162", label: "has authorized access point for person [a/datatype] (RDA:P50411)"},
     const userLang = mw.config.get('wgUserLanguage') || 'en';
        datatype: "string"
        },
        {
        "property": {id: "P1087", label: "has surname [a/datatype] (RDA:P50291)"},
        datatype: "string"
        },
        {
        "property": {id: "P579", label: "has given name [a/datatype] (RDA:P50292)"},
        datatype: "string"
        },
        {
        "property": {id: "P1105", label: "has term of rank or honour or office [a/datatype] (RDA:P50110)"},
        datatype: "string"
        },
        {
        "property": {id: "P1893", label: "has place of birth [a/object] (RDA:P50119)"},
        datatype: "wikibase-item"
        },
        {
        "property": {id: "P2251", label: "has profession or occupation [a/object] (RDA:P50104)"},
        datatype: "wikibase-item"
        },
        {
        "property": {id: "P2250", label: "has language of person [a/object] (RDA:P50102)"},
        datatype: "wikibase-item"
        },
        {
        "property": {id: "P1123", label: "has variant access point for person [a/datatype] (RDA:P50412)"},
        datatype: "string"
        },
        {
        "property": {id: "P2007", label: "has related corporate body of person [a/object] (RDA:P50318)"},
        datatype: "wikibase-item"
        },
        {
        "property": {id: "P756", label: "has note on person [a/datatype] (RDA:P50395)"},
        datatype: "string"
        },
        ],
     },
    "corporate": {
    "label": "Corporate Body",
        "prefilledClaims": [
        {
            "property": {id: "P1", label: "instance of", datatype: "wikibase-item"},
            "value": {
                "mainVal": {id: "Q72", datatype: "wikibase-item"}
            }
        }
        ],
              "userDefinedClaims": [
            {
        "property": {id: "P10814", label: "has longitude and latitude (P60345)"},
        datatype: "globe-coordinate"
        },
        {
        "property": {id: "P160", label: "has authorized access point for corporate body [a/datatype] (RDA:P50407)"},
        datatype: "string"
        },
        {
        "property": {id: "P1121", label: "has variant access point for corporate body [a/datatype] (RDA:P50408)"},
        datatype: "string"
        },


        {
    // ---- 1) CONFIG: how we find your template items ----
        "property": {id: "P754", label: "has note on corporate body [a/datatype] (RDA:P50393)"},
    // Use exactly what works in the search box:
        datatype: "string"
    const SEARCH_QUERY = 'haswbstatement:P1=Q196450';
        },
     // If needed, you can hardcode Q-IDs instead and skip searchTemplates().
        ],
     },
};


function statementExists(property, value) {
    // ---- 2) HELPERS used by the builder ----
    items.toArray().some(item => !!$(item).data().propertyId && $(item).data().statementgroupview.options.value.getKey() === property &&
    const chunk = (arr, n) => { const out=[]; for (let i=0;i<arr.length;i+=n) out.push(arr.slice(i,i+n)); return out; };
    const getBestLabel = (labels) =>
      (labels?.[userLang]?.value) || (labels && Object.values(labels)[0]?.value) || undefined;
 
    // decide if "_" means "placeholder" for string-like types
    const isPlaceholderSnak = (snak) => {
      if (!snak || snak.snaktype !== 'value' || !snak.datavalue) return false;
      const dt = snak.datatype;
      const v  = snak.datavalue.value;
      if (dt === 'string' || dt === 'external-id' || dt === 'url') return v === '_';
      if (dt === 'monolingualtext') return v?.text === '_';
      return false;
    };
 
    const cleanTemplateLabel = (raw) => (raw || '')
      .replace(/^RDA\s*/i, '')
      .replace(/\s*Template$/i, '')
      .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 ----
    async function searchTemplates(query) {
      const res = await api.get({
        action: 'query',
        list: 'search',
        srsearch: query,
        // make API behave like Special:Search
        srbackend: 'CirrusSearch',
        srqiprofile: 'wikibase',
        srlimit: 50,
        formatversion: 2
      });
      return (res.query?.search || [])
        .map(h => (h.title.match(/Q\d+$/) || [])[0])
        .filter(Boolean);
    }
 
    async function fetchEntities(ids) {
      const map = {};
      for (const group of chunk(ids, 50)) {
        const res = await api.get({
          action: 'wbgetentities',
          ids: group.join('|'),
          props: 'labels|claims',
          languages: userLang
        });
        Object.assign(map, res.entities || {});
      }
      return map;
    }
 
    async function fetchPropertyMeta(entityMap) {
      const pids = new Set();
      Object.values(entityMap).forEach(e => Object.keys(e.claims || {}).forEach(pid => pids.add(pid)));
      const map = {};
      for (const group of chunk([...pids], 50)) {
        const res = await api.get({
          action: 'wbgetentities',
          ids: group.join('|'),
          props: 'labels|datatype',
          languages: userLang
        });
        Object.assign(map, res.entities || {});
      }
      // normalize
      const meta = {};
      for (const [pid, ent] of Object.entries(map)) {
        meta[pid] = {
          id: pid,
          label: getBestLabel(ent.labels) || pid,
          datatype: ent.datatype // e.g., 'wikibase-item', 'string', 'globe-coordinate'
        };
      }
      return meta;
    }
 
    function transformToTemplate(entity, propMeta) {
      const rawLabel = getBestLabel(entity.labels) || entity.id;
      const label    = cleanTemplateLabel(rawLabel);
 
      const prefilledClaims = [];
      const userDefinedClaims = [];
 
      for (const [pid, statements] of Object.entries(entity.claims || {})) {
        if (!Array.isArray(statements) || !statements.length) continue;
        const prop = propMeta[pid] || { id: pid, label: pid, datatype: 'string' };
 
        // first non-placeholder statement (if any)
        const firstNonPlaceholder = statements
          .map(st => st.mainsnak)
          .find(ms => ms && ms.snaktype === 'value' && !isPlaceholderSnak(ms));
 
        if (firstNonPlaceholder) {
          // NOTE: to keep compatibility with your prefillStatement (which assumes wikibase-item),
          // we only prefill non-placeholder values for wikibase-item. Others go to userDefined.
          if (prop.datatype === 'wikibase-item') {
            const v = firstNonPlaceholder.datavalue?.value; // {id: 'Qxx'}
            prefilledClaims.push({
              property: { id: prop.id, label: prop.label, datatype: prop.datatype },
              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 };
    }
 
    async function buildWbTemplatesFromQuery(query) {
      const qids = await searchTemplates(query);
      if (!qids.length) return {};
      const entities = await fetchEntities(qids);
      const propMeta = await fetchPropertyMeta(entities);
 
      const map = {};
      Object.values(entities).forEach(e => {
        const tpl = transformToTemplate(e, propMeta);
        map[tpl.key] = { label: tpl.label, prefilledClaims: tpl.prefilledClaims, userDefinedClaims: tpl.userDefinedClaims };
      });
      return map;
    }
 
    // ---- 4) Build WbTemplates dynamically ----
    // If you need to debug, console.log(WbTemplates) after the await.
    const WbTemplates = await buildWbTemplatesFromQuery(SEARCH_QUERY);
 
    // Expose globally so your existing helpers can access it
    window.WbTemplates = WbTemplates;
 
    // =========================
    // YOUR EXISTING FUNCTIONS
    // (left untouched, just moved below the builder so they can use WbTemplates)
    // =========================
 
    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 =>
         $(item).data().statementgroupview.options.value.getItemContainer().toArray().some(claimItem =>
            claimItem.getClaim().getMainSnak().getValue().getSerialization() === value));
          claimItem.getClaim().getMainSnak().getValue().getSerialization() === value));
}
    }


function getPropertyEntity(property) {
    function getPropertyEntity(property) {
    return {
      return {
         "id": `${property.id}`,
         "id": `${property.id}`,
         "title": `Property:${property.id}`,
         "title": `Property:${property.id}`,
         "datatype": property.datatype,
         "datatype": property.datatype,
         "label": property.label,
         "label": property.label,
    };
      };
}
    }


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


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


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


    let selector = snak._getPropertySelector();
      let 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;
          let valSelector = snak._variation._valueView._expert.$input.data().entityselector;
            let valval = getPropertyEntity(claim.value.mainVal);
          let 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;
            let 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;
            let qslv = $(sv._qualifiers.items().first()).data().snaklistview;
            let qsnak = qslv._listview.items().first().data().snakview;
            let qsnak = qslv._listview.items().first().data().snakview;
 
            let qsel = qsnak._getPropertySelector();
            let 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;
                let qvalSelector = qsnak._variation._valueView._expert.$input.data().entityselector;
                    let qvalval = getPropertyEntity(claim.value.mainVal);
                let 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;
                let 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;
          let 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;
      let prop = claim.property;
  return new Promise((resolve, reject) => {
      return new Promise((resolve, reject) => {
    let statementListView = $(".wikibase-statementgrouplistview")
        let statementListView = $(".wikibase-statementgrouplistview")
      .first()
          .first()
      .data().wikibaseStatementgrouplistview;
          .data().wikibaseStatementgrouplistview;
    statementListView.enterNewItem();
        statementListView.enterNewItem();
    let items = statementListView.listview.items();
        let items = statementListView.listview.items();
    let item = items.last().data();
        let item = items.last().data();
    let sv = item.statementgroupview.statementlistview._listview.items();
        let sv = item.statementgroupview.statementlistview._listview.items();
    let snak = $(sv).data().statementview.$mainSnak.data().snakview;
        let snak = $(sv).data().statementview.$mainSnak.data().snakview;
   
    let selector = snak._getPropertySelector();
   
    // Set up event listeners and chain them to resolve when finished.
    selector.element.on('entityselectorselected', (event, entityId) => {
    console.log('entityselectorselected');
      $(snak._variation).on("afterdraw", function() {
    console.log('afterdraw');
        snak._variation._valueView.element.one(
          snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
          function() {
    console.log('afterstartediting');
            // Simulate filling in values
           
           
            if(claim.datatype === "wikibase-item"){
            let valSelector = snak._variation._valueView._expert.$input.data().entityselector;
            let jqe = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;


        let selector = snak._getPropertySelector();


            valSelector.element.val("temp");
        // Set up event listeners and chain them to resolve when finished.
            valSelector._trigger("change");
        selector.element.on('entityselectorselected', (event, entityId) => {
            valSelector._select({id: "Q80", datatype: "wikibase-item"});
          $(snak._variation).on("afterdraw", function () {
            jqe.handlers[0].call();
            snak._variation._valueView.element.one(
            jqe.handlers[1].call();
              snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
           
              function () {
            valSelector.element.val("");
                if (claim.datatype === "wikibase-item") {
            valSelector._trigger("change");
                  let valSelector = snak._variation._valueView._expert.$input.data().entityselector;
            //valSelector._select({});
                  let jqe = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;
           
                       
            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.$input.val("0,0");
            jqe.handlers[0].call();
            jqe.handlers[1].call();
            }
            else {
            valSelector.$input.val("__");
            jqe.handlers[0].call();
            jqe.handlers[1].call();
            }
     
         
            valSelector.$input.val("");
            jqe.handlers[0].call();
            jqe.handlers[1].call();
           
            }
           
           
           
            // Once processing is complete, resolve the Promise.
            resolve();
          }
        );
      });
    });
   
    selector.element.val(prop.label);
    selector._trigger('change');
    selector._select(prop);
  });
}


function addDetails(templateName) {
                  valSelector.element.val("temp");
  var claims = WbTemplates[templateName].userDefinedClaims;
                  valSelector._trigger("change");
  var chain = Promise.resolve();
                  valSelector._select({ id: "Q80", datatype: "wikibase-item" });
 
                  jqe.handlers[0].call();
  claims.forEach(function(claim) {
                  jqe.handlers[1].call();
    chain = chain.then(function() {
      return pendingStatement(claim);
    });
  });
 
  return chain;
}


function applyTemplate(templateName) {
                  valSelector.element.val("");
    WbTemplates[templateName].prefilledClaims.forEach(claim => {
                  valSelector._trigger("change");
                  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.$input.val("0,0");
                    jqe.handlers[0].call();
                    jqe.handlers[1].call();
                  }
                  else {
                    valSelector.$input.val("__"); // shows the placeholder behavior
                    jqe.handlers[0].call();
                    jqe.handlers[1].call();
                  }
 
                  valSelector.$input.val("");
                  jqe.handlers[0].call();
                  jqe.handlers[1].call();
                }
 
                // Once processing is complete, resolve the Promise.
                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(claim => {
         prefillStatement(claim, false);
         prefillStatement(claim, false);
     });
      });
}
     }
 
    // ---- 5) UI: render only after WbTemplates is ready ----
    function initTemplatingUI() {
      // If nothing came back, don't render the UI
      if (!WbTemplates || !Object.keys(WbTemplates).length) {
        console.warn('No templates found for query:', SEARCH_QUERY);
        return;
      }


$(".wikibase-entitytermsview").append(`
      $(".wikibase-entitytermsview").append(`
<div class="wikibase-templating-section">
<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-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 predifined template entries and additional entity details.</span></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="wikibase-templating-form oo-ui-labelElement oo-ui-fieldLayout">
<div class=""><label for="wbtemplate">Entity class</label></div>
    <div class=""><label for="wbtemplate">Entity class</label></div>
<div>
    <div>
<select id="wbtemplate" class="cdx-select"></select>
      <select id="wbtemplate" class="cdx-select"></select>
</div>
    </div>
  </div>
</div>
  <div class="mw-htmlform-submit-buttons">
<div class="mw-htmlform-submit-buttons">
    <button id="applyTemplateBtn" class="cdx-button">Apply template</button>
<button id="applyTemplateBtn" class="cdx-button">Apply template</button>
    <button id="addDetailsBtn" class="cdx-button">Add details</button>
<button id="addDetailsBtn" class="cdx-button">Add details</button>
  </div>
</div>
</div>
</div>
`);
      `);


Object.entries(WbTemplates).forEach(entry => {
      // Populate selector
    $('#wbtemplate').append($('<option>', {
      Object.entries(WbTemplates).forEach(([key, tpl]) => {
         value: entry[0],
        $('#wbtemplate').append($('<option>', { value: key, text: tpl.label }));
         text: entry[1].label
      });
    }));
 
      // Wire buttons
      $("#applyTemplateBtn").on("click", function () {
         applyTemplate($("#wbtemplate").find(":selected").val());
      });
      $("#addDetailsBtn").on("click", function () {
         addDetails($("#wbtemplate").find(":selected").val());
      });
    }


});
    // finally render
    initTemplatingUI();


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

Revision as of 18:23, 17 September 2025

/* eslint-env browser, es6 */
/* global mw, $ */

mw.loader.using(['mediawiki.api']).then(function () {
  (async function () {
    const api = new mw.Api();
    const userLang = mw.config.get('wgUserLanguage') || 'en';

    // ---- 1) CONFIG: how we find your template items ----
    // Use exactly what works in the search box:
    const SEARCH_QUERY = 'haswbstatement:P1=Q196450';
    // If needed, you can hardcode Q-IDs instead and skip searchTemplates().

    // ---- 2) HELPERS used by the builder ----
    const chunk = (arr, n) => { const out=[]; for (let i=0;i<arr.length;i+=n) out.push(arr.slice(i,i+n)); return out; };
    const getBestLabel = (labels) =>
      (labels?.[userLang]?.value) || (labels && Object.values(labels)[0]?.value) || undefined;

    // decide if "_" means "placeholder" for string-like types
    const isPlaceholderSnak = (snak) => {
      if (!snak || snak.snaktype !== 'value' || !snak.datavalue) return false;
      const dt = snak.datatype;
      const v  = snak.datavalue.value;
      if (dt === 'string' || dt === 'external-id' || dt === 'url') return v === '_';
      if (dt === 'monolingualtext') return v?.text === '_';
      return false;
    };

    const cleanTemplateLabel = (raw) => (raw || '')
      .replace(/^RDA\s*/i, '')
      .replace(/\s*Template$/i, '')
      .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 ----
    async function searchTemplates(query) {
      const res = await api.get({
        action: 'query',
        list: 'search',
        srsearch: query,
        // make API behave like Special:Search
        srbackend: 'CirrusSearch',
        srqiprofile: 'wikibase',
        srlimit: 50,
        formatversion: 2
      });
      return (res.query?.search || [])
        .map(h => (h.title.match(/Q\d+$/) || [])[0])
        .filter(Boolean);
    }

    async function fetchEntities(ids) {
      const map = {};
      for (const group of chunk(ids, 50)) {
        const res = await api.get({
          action: 'wbgetentities',
          ids: group.join('|'),
          props: 'labels|claims',
          languages: userLang
        });
        Object.assign(map, res.entities || {});
      }
      return map;
    }

    async function fetchPropertyMeta(entityMap) {
      const pids = new Set();
      Object.values(entityMap).forEach(e => Object.keys(e.claims || {}).forEach(pid => pids.add(pid)));
      const map = {};
      for (const group of chunk([...pids], 50)) {
        const res = await api.get({
          action: 'wbgetentities',
          ids: group.join('|'),
          props: 'labels|datatype',
          languages: userLang
        });
        Object.assign(map, res.entities || {});
      }
      // normalize
      const meta = {};
      for (const [pid, ent] of Object.entries(map)) {
        meta[pid] = {
          id: pid,
          label: getBestLabel(ent.labels) || pid,
          datatype: ent.datatype // e.g., 'wikibase-item', 'string', 'globe-coordinate'
        };
      }
      return meta;
    }

    function transformToTemplate(entity, propMeta) {
      const rawLabel = getBestLabel(entity.labels) || entity.id;
      const label    = cleanTemplateLabel(rawLabel);

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

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

        // first non-placeholder statement (if any)
        const firstNonPlaceholder = statements
          .map(st => st.mainsnak)
          .find(ms => ms && ms.snaktype === 'value' && !isPlaceholderSnak(ms));

        if (firstNonPlaceholder) {
          // NOTE: to keep compatibility with your prefillStatement (which assumes wikibase-item),
          // we only prefill non-placeholder values for wikibase-item. Others go to userDefined.
          if (prop.datatype === 'wikibase-item') {
            const v = firstNonPlaceholder.datavalue?.value; // {id: 'Qxx'}
            prefilledClaims.push({
              property: { id: prop.id, label: prop.label, datatype: prop.datatype },
              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 };
    }

    async function buildWbTemplatesFromQuery(query) {
      const qids = await searchTemplates(query);
      if (!qids.length) return {};
      const entities = await fetchEntities(qids);
      const propMeta = await fetchPropertyMeta(entities);

      const map = {};
      Object.values(entities).forEach(e => {
        const tpl = transformToTemplate(e, propMeta);
        map[tpl.key] = { label: tpl.label, prefilledClaims: tpl.prefilledClaims, userDefinedClaims: tpl.userDefinedClaims };
      });
      return map;
    }

    // ---- 4) Build WbTemplates dynamically ----
    // If you need to debug, console.log(WbTemplates) after the await.
    const WbTemplates = await buildWbTemplatesFromQuery(SEARCH_QUERY);

    // Expose globally so your existing helpers can access it
    window.WbTemplates = WbTemplates;

    // =========================
    // YOUR EXISTING FUNCTIONS
    // (left untouched, just moved below the builder so they can use WbTemplates)
    // =========================

    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) {
      return {
        "id": `${property.id}`,
        "title": `Property:${property.id}`,
        "datatype": property.datatype,
        "label": property.label,
      };
    }

    function prefillStatement(claim, onlyProperty) {
      if (onlyProperty) {
        pendingStatement(claim.property);
        return;
      }
      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);

      let 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 () {
          let valSelector = snak._variation._valueView._expert.$input.data().entityselector;
          let valval = getPropertyEntity(claim.value.mainVal);

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

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

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

            let 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 () {
                let qvalSelector = qsnak._variation._valueView._expert.$input.data().entityselector;
                let 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 () {
                let toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
                toolbar._controller.stopEditing(false);
              });
          }
        }
      );

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

    function pendingStatement(claim) {
      let prop = claim.property;
      return new Promise((resolve, reject) => {
        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();
        let snak = $(sv).data().statementview.$mainSnak.data().snakview;

        let selector = snak._getPropertySelector();

        // Set up event listeners and chain them to resolve when finished.
        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");
                  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 {
                  let valSelector = snak._variation._valueView._expert.$input.data().inputautoexpand;
                  let jqe = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;

                  if (claim.datatype === "globe-coordinate") {
                    valSelector.$input.val("0,0");
                    jqe.handlers[0].call();
                    jqe.handlers[1].call();
                  }
                  else {
                    valSelector.$input.val("__"); // shows the placeholder behavior
                    jqe.handlers[0].call();
                    jqe.handlers[1].call();
                  }

                  valSelector.$input.val("");
                  jqe.handlers[0].call();
                  jqe.handlers[1].call();
                }

                // Once processing is complete, resolve the Promise.
                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(claim => {
        prefillStatement(claim, false);
      });
    }

    // ---- 5) UI: render only after WbTemplates is ready ----
    function initTemplatingUI() {
      // If nothing came back, don't render the UI
      if (!WbTemplates || !Object.keys(WbTemplates).length) {
        console.warn('No templates found for query:', SEARCH_QUERY);
        return;
      }

      $(".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);
});