Jump to content

MediaWiki:Gadget-templating.js

From National Library of Greece
Revision as of 18:23, 17 September 2025 by Admin (talk | contribs)

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

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* 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);
});