Jump to content

MediaWiki:Gadget-templating.js: Difference between revisions

From National Library of Greece
No edit summary
Tag: Reverted
No edit summary
Tag: Manual revert
Line 35: Line 35:
     return undefined;
     return undefined;
   }
   }
 
  function qidToNumeric(id) { return parseInt(String(id).replace(/^[A-Za-z]/, ''), 10); }
function entityTypeFromId(id) { return /^[Pp]/.test(id) ? 'property' : 'item'; }
// Build a datavalue from your template value object
function buildDataValue(mainVal, datatype) {
  // allow datatype override from mainVal
  var dt = (mainVal && mainVal.datatype) || datatype;
  if (dt === 'wikibase-item' || dt === 'wikibase-property') {
    var id = mainVal.id;
    return {
      type: 'wikibase-entityid',
      value: {
        id: id,
        'entity-type': entityTypeFromId(id),
        'numeric-id': qidToNumeric(id)
      }
    };
  }
  if (dt === 'string' || dt === 'external-id' || dt === 'url') {
    return { type: 'string', value: mainVal.text || mainVal.value || mainVal };
  }
  if (dt === 'monolingualtext') {
    return { type: 'monolingualtext', value: { text: mainVal.text, language: mainVal.language || 'en' } };
  }
  if (dt === 'time') {
    // Provide a calendarmodel; adjust if your instance uses a local IRI
    var t = mainVal.time;
    return { type: 'time', value: {
      time: t,
      precision: mainVal.precision || 11,
      timezone: 0, before: 0, after: 0,
      calendarmodel: 'http://www.wikidata.org/entity/Q1985727'
    }};
  }
  if (dt === 'quantity') {
    return { type: 'quantity', value: {
      amount: String(mainVal.amount),
      unit: mainVal.unit || '1'
    }};
  }
  if (dt === 'globecoordinate' || dt === 'globe-coordinate') {
    return { type: 'globecoordinate', value: {
      latitude: mainVal.latitude, longitude: mainVal.longitude,
      precision: mainVal.precision || 1e-6,
      globe: mainVal.globe || 'http://www.wikidata.org/entity/Q2'
    }};
  }
  // Fallback: string
  return { type: 'string', value: String(mainVal && (mainVal.text || mainVal.value || '')) };
}
function buildQualifierSnak(q) {
  if (!q || !q.property || !q.value || !q.value.mainVal) return null;
  var pid = q.property.id;
  var dt = q.property.datatype || (q.datatype || 'string');
  return {
    snaktype: 'value',
    property: pid,
    datavalue: buildDataValue(q.value.mainVal, dt),
    datatype: dt
  };
}
function buildStatementFromClaim(claim) {
  // only build if there is a real main value
  if (!claim || !claim.property || !claim.value || !claim.value.mainVal) return null;
  var pid = claim.property.id;
  var dt = claim.property.datatype || (claim.value.mainVal.datatype || 'string');
  var mainsnak = {
    snaktype: 'value',
    property: pid,
    datavalue: buildDataValue(claim.value.mainVal, dt),
    datatype: dt
  };
  // group qualifiers by property id (Wikibase expects a map pid -> [snaks])
  var qmap = {};
  if (claim.qualifiers && claim.qualifiers.length) {
    for (var i = 0; i < claim.qualifiers.length; i++) {
      var qsnak = buildQualifierSnak(claim.qualifiers[i]);
      if (!qsnak) continue; // skip blanks
      var qpid = qsnak.property;
      if (!qmap[qpid]) qmap[qpid] = [];
      qmap[qpid].push(qsnak);
    }
  }
  var st = { type: 'statement', rank: 'normal', mainsnak: mainsnak };
  if (Object.keys(qmap).length) {
    st.qualifiers = qmap;
    // optional: st['qualifiers-order'] = Object.keys(qmap);
  }
  return st;
}
function getCurrentEntityId() {
  // Try common configs; fallback parse from title ("Item:Q123")
  var id = mw.config.get('wbEntityId') || mw.config.get('wgWikibaseItemId');
  if (id) return id;
  var t = mw.config.get('wgTitle') || '';
  var m = /Q\d+/.exec(t);
  return m ? m[0] : null;
}
function applyTemplateFast(templateName) {
  var tpl = window.WbTemplates && window.WbTemplates[templateName];
  if (!tpl) return Promise.reject(new Error('Template not found: ' + templateName));
  // Build statements array
  var rawClaims = (tpl.prefilledClaims || []);
  var statements = [];
  for (var i = 0; i < rawClaims.length; i++) {
    var st = buildStatementFromClaim(rawClaims[i]);
    if (st) statements.push(st);
  }
  if (!statements.length) {
    return Promise.resolve(); // nothing to do
  }
  var entityId = getCurrentEntityId();
  if (!entityId) return Promise.reject(new Error('Cannot determine entity ID on this page'));
  var api = new mw.Api();
  var payload = { claims: statements };
  return api.postWithToken('csrf', {
    action: 'wbeditentity',
    id: entityId,
    data: JSON.stringify(payload),
    summary: 'Apply template: ' + (tpl.label || templateName),
    // tags: 'YourTag' // optional if you use ChangeTags
    format: 'json'
  }).then(function (res) {
    // Fastest way to reflect new statements in UI:
    location.reload();
  });
}
  function waitForValueWidget(snak, datatype, timeoutMs) {
  var start = Date.now(), timeout = timeoutMs || 10000;
  return new Promise(function (resolve, reject) {
    (function tick() {
      var vv = snak && snak._variation && snak._variation._valueView;
      var ex = vv && vv._expert;
      var $input = ex && ex.$input;
      var plugin = null, jqe = null;
      try {
        if ($input && $input.length) {
          var data = $input.data();
          if (datatype === 'wikibase-item') {
            plugin = data.entityselector || data['mw.widgets.EntitySelector'] || data.ooWidget || null;
          } else {
            plugin = data.inputautoexpand || null;
          }
          jqe = data.jqueryEventSpecialEachchange || null;
        }
      } catch (e) {}
      if ($input && $input.length) return resolve({ $input: $input, plugin: plugin, jqe: jqe });
      if (Date.now() - start > timeout) return reject(new Error('value widget not ready'));
      setTimeout(tick, 50);
    })();
  });
}
   function isPlaceholderSnak(snak) {
   function isPlaceholderSnak(snak) {
     if (!snak || snak.snaktype !== 'value' || !snak.datavalue) return false;
     if (!snak || snak.snaktype !== 'value' || !snak.datavalue) return false;
Line 356: Line 189:
             };
             };
           }
           }
         
            // return { "person": { "label": "Person", "prefilledClaims": [{ "property": {id: "P1", label: "instance of", datatype: "wikibase-item"}, "value": { "mainVal": {id: "Q78", datatype: "wikibase-item"} } },{ "property": {id: "P2", label: "subclass of", datatype: "wikibase-item"}, "value": { "mainVal": {id: "Q79", datatype: "wikibase-item"} } }], "userDefinedClaims": [ { "property": {id: "P162", label: "has authorized access point for person [a/datatype] (RDA:P50411)"}, 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" }, { "property": {id: "P754", label: "has note on corporate body [a/datatype] (RDA:P50393)"}, datatype: "string" }, ], }, };
 
         
            return { "person": { "label": "Person", "prefilledClaims": [{
  property: { id: "P1", label: "instance of", datatype: "wikibase-item" },
  value: { mainVal: { id: "Q78", datatype: "wikibase-item" } },
  qualifiers: [
    // prefilled qualifier
    {
      property: { id: "P580", label: "start time", datatype: "time" },
      value: { mainVal: { time: "+2001-01-01T00:00:00Z", precision: 11, datatype: "time" } }
    },
    // user-defined qualifier (no value → leave blank for the user)
    {
      property: { id: "P1545", label: "series ordinal", datatype: "string" }
    }
  ]
},{ "property": {id: "P2", label: "subclass of", datatype: "wikibase-item"}, "value": { "mainVal": {id: "Q79", datatype: "wikibase-item"} } }], "userDefinedClaims": [ { "property": {id: "P162", label: "has authorized access point for person [a/datatype] (RDA:P50411)"}, 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" }, { "property": {id: "P754", label: "has note on corporate body [a/datatype] (RDA:P50393)"}, datatype: "string" }, ], }, };


           return map;
           return map;
Line 423: Line 239:
   });
   });
}
}
function addOneQualifierToStatement(sv, q) {
  return new Promise(function (resolve) {
    try {
      var qlistview = sv._qualifiers;              // qualifiers controller
      qlistview.enterNewItem();                    // create qualifiers list (if missing) / new row
      // obtain the last qualifiers listview (snaklistview)
      var $qlItems = sv._qualifiers.items();
      var qslv = $qlItems && $qlItems.length ? $($qlItems[$qlItems.length - 1]).data().snaklistview : null;
      if (!qslv || !qslv.enterNewItem) { resolve(); return; }
      qslv.enterNewItem();                          // create the qualifier snak
      var qsnak = qslv._listview.items().last().data().snakview;
      // select qualifier property
      var qprop = getPropertyEntity(q.property);
      var qsel = qsnak._getPropertySelector();
      qsel.element.val(qprop.label);
      qsel._trigger('change');
      qsel._select(qprop);
      // when its value editor is ready, set or leave blank
      qsnak._variation._valueView.element.one(
        qsnak._variation._valueView.widgetEventPrefix + 'afterstartediting',
        function () {
          var dt = q.property.datatype || (q.datatype || 'string');
          waitForValueWidget(qsnak, dt).then(function (vw) {
            var hasValue = q.value && q.value.mainVal;
            if (dt === 'wikibase-item') {
              // prefilled: select entity; else: poke then clear
              if (hasValue && q.value.mainVal.id) {
                if (vw.plugin && typeof vw.plugin._select === 'function') {
                  vw.plugin._select({ id: q.value.mainVal.id, datatype: 'wikibase-item' });
                } else {
                  vw.$input.val(q.value.mainVal.id).trigger('input').trigger('change');
                }
              } else {
                vw.$input.val('Q1').trigger('input').trigger('change');
                vw.$input.val('').trigger('input').trigger('change');
              }
            } else if (dt === 'time' && hasValue && q.value.mainVal.time) {
              vw.$input.val(q.value.mainVal.time).trigger('input').trigger('change');
            } else if (dt === 'globe-coordinate') {
              var v = hasValue && q.value.mainVal.latitude != null ? (q.value.mainVal.latitude + ',' + q.value.mainVal.longitude) : '0,0';
              vw.$input.val(v).trigger('input').trigger('change');
              if (!hasValue) { vw.$input.val('').trigger('input').trigger('change'); }
            } else {
              // string / monolingual / url / external-id (handle placeholder “_”)
              var txt = hasValue && (q.value.mainVal.text || q.value.mainVal) !== '_' ? (q.value.mainVal.text || q.value.mainVal) : '__';
              vw.$input.val(txt).trigger('input').trigger('change');
              if (!hasValue || txt === '__') { vw.$input.val('').trigger('input').trigger('change'); }
            }
            // done with this qualifier
            resolve();
          }).catch(function () { resolve(); });
        }
      );
    } catch (e) {
      console.warn('addOneQualifierToStatement failed', e);
      resolve();
    }
  });
}
function addQualifiersSequentially(sv, qualifiers) {
  qualifiers = qualifiers || [];
  var chain = Promise.resolve();
  for (var i = 0; i < qualifiers.length; i++) {
    (function (q) {
      chain = chain.then(function () { return addOneQualifierToStatement(sv, q); });
    })(qualifiers[i]);
  }
  return chain;
}


/* NEW: wait until the value widget exists for the given datatype */
/* NEW: wait until the value widget exists for the given datatype */
Line 530: Line 267:
}
}
function prefillStatement(claim, onlyProperty) {
function prefillStatement(claim, onlyProperty) {
   return new Promise(function (resolve) {
   return new Promise(function (resolve, reject) {
     var pid = claim.property.id;
     var pid = claim.property.id;
     var prevCount = getStatementCount(pid);
     var prevCount = getStatementCount(pid);
    if (onlyProperty) {
      pendingStatement(claim.property).then(function () {
        return waitForStatementRendered(pid, prevCount).then(resolve);
      }, reject);
      return;
    }


     var statementListView = $(".wikibase-statementgrouplistview").first().data().wikibaseStatementgrouplistview;
     var statementListView = $(".wikibase-statementgrouplistview").first().data().wikibaseStatementgrouplistview;
Line 542: Line 286:
     var snak = sv.$mainSnak.data().snakview;
     var snak = sv.$mainSnak.data().snakview;


    // select main property
     var es = getPropertyEntity(claim.property);
     var es = getPropertyEntity(claim.property);
     var 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);
 
if(snak._variation._valueView){
    // fill main value when ready
     snak._variation._valueView.element.one(
     snak._variation._valueView.element.one(
       snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
       snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
       function () {
       function () {
         var dt = claim.property.datatype || (claim.value && claim.value.mainVal && claim.value.mainVal.datatype) || 'string';
         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 (onlyProperty) {
         if (claim.qualifiers) {
           // just property; optionally still add qualifiers
           var qlistview = sv._qualifiers;
           addQualifiersSequentially(sv, claim.qualifiers).then(function () {
           qlistview.enterNewItem();
            var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
 
             try { toolbar._controller.stopEditing(false); } catch (e) {}
          var qslv = $(sv._qualifiers.items().first()).data().snaklistview;
            waitForStatementRendered(pid, prevCount).then(resolve);
          var qsnak = qslv._listview.items().first().data().snakview;
          });
 
           return;
          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");
              qvalSelector._select(qvalval);
            }
           );
         }
         }
      }
    );


        waitForValueWidget(snak, dt).then(function (vw) {
    snak._variation._valueView.element.one(
          // set main value (wikibase-item typical in your templates)
      snak._variation._valueView.widgetEventPrefix + 'change',
          if (dt === 'wikibase-item' && claim.value && claim.value.mainVal && claim.value.mainVal.id) {
      function () {
            if (vw.plugin && typeof vw.plugin._select === 'function') {
        var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
              vw.plugin._select({ id: claim.value.mainVal.id, datatype: 'wikibase-item' });
        var resolved = false;
            } else {
        function doneOnce() { if (!resolved) { resolved = true; } }
              vw.$input.val(claim.value.mainVal.id).trigger('input').trigger('change');
 
            }
        // Trigger save
          } else {
        try { toolbar._controller.stopEditing(false); } catch (e) {}
            // for non-item, just poke and clear so it saves
            vw.$input.val('__').trigger('input').trigger('change');
            vw.$input.val('').trigger('input').trigger('change');
          }


          // add qualifiers (if any), then save once
        // Wait for visual render (count + 1) before resolving
          addQualifiersSequentially(sv, claim.qualifiers).then(function () {
        waitForStatementRendered(pid, prevCount).then(function () {
            var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
           doneOnce();
            try { toolbar._controller.stopEditing(false); } catch (e) {}
          resolve();
            waitForStatementRendered(pid, prevCount).then(resolve);
          });
        }).catch(function () {
           // even if value widget wait fails, try to finish gracefully
          addQualifiersSequentially(sv, claim.qualifiers).then(function () {
            var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
            try { toolbar._controller.stopEditing(false); } catch (e) {}
            waitForStatementRendered(pid, prevCount).then(resolve);
          });
         });
         });
       }
       }
     );
     );
}
   });
   });
}
}
function pendingStatement(claim) {
function pendingStatement(claim) {
   var prop = claim.property;
   var prop = claim.property;
   var pid = prop.id;
   var pid = prop.id;
   var prevCount = getStatementCount(pid);
   var prevCount = getStatementCount(pid);


   return new Promise(function (resolve) {
   return new Promise(function (resolve, reject) {
     var statementListView = $(".wikibase-statementgrouplistview").first().data().wikibaseStatementgrouplistview;
     var statementListView = $(".wikibase-statementgrouplistview").first().data().wikibaseStatementgrouplistview;
     statementListView.enterNewItem();
     statementListView.enterNewItem();
Line 612: Line 364:
     var snak = $(sv).data().statementview.$mainSnak.data().snakview;
     var snak = $(sv).data().statementview.$mainSnak.data().snakview;


    // select property
     var selector = snak._getPropertySelector();
     var selector = snak._getPropertySelector();
    // 1) when property gets selected, the value widget will be (re)drawn
     selector.element.one('entityselectorselected', function () {
     selector.element.one('entityselectorselected', function () {
       $(snak._variation).one('afterdraw', function () {
       $(snak._variation).one('afterdraw', function () {
        // 2) value view will enter editing -> now wait until the widget exists
         snak._variation._valueView.element.one(
         snak._variation._valueView.element.one(
           snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
           snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
           function () {
           function () {
             var dt = claim.datatype || claim.property.datatype || 'string';
             waitForValueWidget(snak, claim.datatype).then(function (vw) {


            // lightly poke main value to mark as edited in some builds
              // 3) poke the widget to create a blank statement the user can later fill
            waitForValueWidget(snak, dt).then(function (vw) {
               if (claim.datatype === "wikibase-item") {
               if (dt === 'wikibase-item') {
                 var valSelector = vw.widget;
                 vw.$input.val('Q1').trigger('input').trigger('change');
                try {
                vw.$input.val('').trigger('input').trigger('change');
                  // select a temp value then clear it to trigger 'change'
              } else if (dt === 'globe-coordinate') {
                  if (valSelector.element) {
                vw.$input.val('0,0').trigger('input').trigger('change');
                    valSelector.element.val("temp");
                vw.$input.val('').trigger('input').trigger('change');
                  }
                  if (typeof valSelector._select === 'function') {
                    valSelector._select({ id: "Q80", datatype: "wikibase-item" });
                  }
                  if (valSelector.element) {
                    valSelector.element.val("");
                    valSelector.element.trigger('change');
                  }
                  if (vw.jqe && vw.jqe.handlers) {
                    // fire eachchange handlers if present (older widget versions)
                    for (var i = 0; i < vw.jqe.handlers.length; i++) {
                      try { vw.jqe.handlers[i].call(); } catch (e) {}
                    }
                  }
                } catch (e) {
                  console.warn('entityselector poke failed', e);
                }
               } else {
               } else {
                 vw.$input.val('__').trigger('input').trigger('change');
                 var iae = vw.widget; // inputautoexpand
                vw.$input.val('').trigger('input').trigger('change');
                try {
                  var $inp = iae.$input || (snak._variation._valueView._expert.$input);
                  if ($inp && $inp.length) {
                    if (claim.datatype === "globe-coordinate") {
                      $inp.val("0,0");
                    } else {
                      $inp.val("__");
                    }
                    $inp.trigger('input').trigger('change');
                    // clear back to blank
                    $inp.val("").trigger('input').trigger('change');
                  }
                  if (vw.jqe && vw.jqe.handlers) {
                    for (var j = 0; j < vw.jqe.handlers.length; j++) {
                      try { vw.jqe.handlers[j].call(); } catch (e2) {}
                    }
                  }
                } catch (e3) {
                  console.warn('inputautoexpand poke failed', e3);
                }
               }
               }
 
             }).catch(function (err) {
              // add qualifiers then save once
               console.error('waitForValueWidget failed', err);
              addQualifiersSequentially($(sv).data().statementview, claim.qualifiers).then(function () {
                var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
                try { toolbar._controller.stopEditing(false); } catch (e) {}
                waitForStatementRendered(pid, prevCount).then(resolve);
              });
             }).catch(function () {
               addQualifiersSequentially($(sv).data().statementview, claim.qualifiers).then(function () {
                var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
                try { toolbar._controller.stopEditing(false); } catch (e) {}
                waitForStatementRendered(pid, prevCount).then(resolve);
              });
             });
             });
           }
           }
Line 652: Line 430:
     });
     });


    // 4) select the property (triggers entityselectorselected + redraw)
     selector.element.val(prop.label);
     selector.element.val(prop.label);
     selector._trigger('change');
     selector._trigger('change');
     selector._select(prop);
     selector._select(prop);
    // 5) when the value view emits 'change', save; then wait for visual render
    snak._variation._valueView.element.one(
      snak._variation._valueView.widgetEventPrefix + 'change',
      function () {
        var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
        try { toolbar._controller.stopEditing(false); } catch (e) {}
        waitForStatementRendered(pid, prevCount).then(resolve);
      }
    );
   
   });
   });
}
}
Line 736: Line 527:


$('#applyTemplateBtn').off('click').on('click', function () {
$('#applyTemplateBtn').off('click').on('click', function () {
   var $b = $(this).prop('disabled', true);
   var $b=$(this).prop('disabled', true);
   var name = $('#wbtemplate').val();
   applyTemplate($('#wbtemplate').val()).then(function(){ $b.prop('disabled', false); });
  applyTemplateFast(name)
    .catch(function (e) { console.error('applyTemplateFast failed:', e); })
    .then(function () { $b.prop('disabled', false); });
});
});
$('#addDetailsBtn').off('click').on('click', function () {
$('#addDetailsBtn').off('click').on('click', function () {

Revision as of 20:45, 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';
// Serialize all UI writes so nothing overlaps
var WriteQueue = (function () {
  var chain = Promise.resolve();
  function push(taskFn) {
    chain = chain.then(function () {
      return taskFn();
    }).catch(function (e) {
      console.error('Write failed:', e);
      // swallow so the queue continues
    });
    return chain;
  }
  return { push: push };
})();
  // ===== 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',
      srnamespace: 120,
      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
        });
      }
    }

	console.log({ key: slugify(label), label: label, prefilledClaims: prefilledClaims, userDefinedClaims: userDefinedClaims });

    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 { "person": { "label": "Person", "prefilledClaims": [{ "property": {id: "P1", label: "instance of", datatype: "wikibase-item"}, "value": { "mainVal": {id: "Q78", datatype: "wikibase-item"} } },{ "property": {id: "P2", label: "subclass of", datatype: "wikibase-item"}, "value": { "mainVal": {id: "Q79", datatype: "wikibase-item"} } }], "userDefinedClaims": [ { "property": {id: "P162", label: "has authorized access point for person [a/datatype] (RDA:P50411)"}, 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" }, { "property": {id: "P754", label: "has note on corporate body [a/datatype] (RDA:P50393)"}, datatype: "string" }, ], }, };

          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 getStatementCount(pid) {
  var count = 0;
  $('.wikibase-statementgroupview').each(function () {
    var d = $(this).data();
    if (d && d.propertyId === pid) {
      try { count = d.statementgroupview.statementlistview._listview.items().length; } catch (e) {}
    }
  });
  return count;
}

function waitForStatementRendered(pid, prevCount, timeoutMs) {
  var start = Date.now(), timeout = timeoutMs || 8000;
  return new Promise(function (resolve) {
    (function tick() {
      var now = getStatementCount(pid);
      if (now > prevCount) return resolve();
      if (Date.now() - start > timeout) return resolve(); // fail-safe
      setTimeout(tick, 100);
    })();
  });
}

/* NEW: wait until the value widget exists for the given datatype */
function waitForValueWidget(snak, datatype, timeoutMs) {
  var start = Date.now(), timeout = timeoutMs || 10000;
  return new Promise(function (resolve, reject) {
    (function tick() {
      var widget = null, jqe = null;
      try {
        var vv = snak && snak._variation && snak._variation._valueView;
        var ex = vv && vv._expert;
        var $input = ex && ex.$input;
        if ($input && $input.length) {
          var data = $input.data();
          if (datatype === 'wikibase-item') {
            widget = data.entityselector || data['mw.widgets.EntitySelector'] || null;
          } else {
            widget = data.inputautoexpand || null;
          }
          jqe = data.jqueryEventSpecialEachchange || null;
        }
      } catch (e) {}
      if (widget) return resolve({ widget: widget, jqe: jqe });
      if (Date.now() - start > timeout) return reject(new Error('value widget not ready'));
      setTimeout(tick, 50);
    })();
  });
}
function prefillStatement(claim, onlyProperty) {
  return new Promise(function (resolve, reject) {
    var pid = claim.property.id;
    var prevCount = getStatementCount(pid);

    if (onlyProperty) {
      pendingStatement(claim.property).then(function () {
        return waitForStatementRendered(pid, prevCount).then(resolve);
      }, reject);
      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);
	if(snak._variation._valueView){
    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 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");
              qvalSelector._select(qvalval);
            }
          );
        }
      }
    );

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

        // Trigger save
        try { toolbar._controller.stopEditing(false); } catch (e) {}

        // Wait for visual render (count + 1) before resolving
        waitForStatementRendered(pid, prevCount).then(function () {
          doneOnce();
          resolve();
        });
      }
    );
	}
  });
}
 function pendingStatement(claim) {
  var prop = claim.property;
  var pid = prop.id;
  var prevCount = getStatementCount(pid);

  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();

    // 1) when property gets selected, the value widget will be (re)drawn
    selector.element.one('entityselectorselected', function () {
      $(snak._variation).one('afterdraw', function () {
        // 2) value view will enter editing -> now wait until the widget exists
        snak._variation._valueView.element.one(
          snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
          function () {
            waitForValueWidget(snak, claim.datatype).then(function (vw) {

              // 3) poke the widget to create a blank statement the user can later fill
              if (claim.datatype === "wikibase-item") {
                var valSelector = vw.widget;
                try {
                  // select a temp value then clear it to trigger 'change'
                  if (valSelector.element) {
                    valSelector.element.val("temp");
                  }
                  if (typeof valSelector._select === 'function') {
                    valSelector._select({ id: "Q80", datatype: "wikibase-item" });
                  }
                  if (valSelector.element) {
                    valSelector.element.val("");
                    valSelector.element.trigger('change');
                  }
                  if (vw.jqe && vw.jqe.handlers) {
                    // fire eachchange handlers if present (older widget versions)
                    for (var i = 0; i < vw.jqe.handlers.length; i++) {
                      try { vw.jqe.handlers[i].call(); } catch (e) {}
                    }
                  }
                } catch (e) {
                  console.warn('entityselector poke failed', e);
                }
              } else {
                var iae = vw.widget; // inputautoexpand
                try {
                  var $inp = iae.$input || (snak._variation._valueView._expert.$input);
                  if ($inp && $inp.length) {
                    if (claim.datatype === "globe-coordinate") {
                      $inp.val("0,0");
                    } else {
                      $inp.val("__");
                    }
                    $inp.trigger('input').trigger('change');
                    // clear back to blank
                    $inp.val("").trigger('input').trigger('change');
                  }
                  if (vw.jqe && vw.jqe.handlers) {
                    for (var j = 0; j < vw.jqe.handlers.length; j++) {
                      try { vw.jqe.handlers[j].call(); } catch (e2) {}
                    }
                  }
                } catch (e3) {
                  console.warn('inputautoexpand poke failed', e3);
                }
              }
            }).catch(function (err) {
              console.error('waitForValueWidget failed', err);
            });
          }
        );
      });
    });

    // 4) select the property (triggers entityselectorselected + redraw)
    selector.element.val(prop.label);
    selector._trigger('change');
    selector._select(prop);

    // 5) when the value view emits 'change', save; then wait for visual render

    snak._variation._valueView.element.one(
      snak._variation._valueView.widgetEventPrefix + 'change',
      function () {
        var toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
        try { toolbar._controller.stopEditing(false); } catch (e) {}
        waitForStatementRendered(pid, prevCount).then(resolve);
      }
    );
    
  });
}
function getExistingPropertyIds() {
  var map = {};
  $('.wikibase-statementgroupview').each(function () {
    var d = $(this).data();
    if (d && d.propertyId) map[d.propertyId] = true;
  });
  return map;
}

function addDetails(templateName, opts) {
  opts = opts || {};
  var skipExisting = (typeof opts.skipExisting === 'boolean') ? opts.skipExisting : true;

  var tpl = WbTemplates[templateName] || { userDefinedClaims: [] };
  var claims = (tpl.userDefinedClaims || []).slice();
  var existing = getExistingPropertyIds();

  var seq = Promise.resolve();
  claims.forEach(function (claim) {
    if (skipExisting && existing[claim.property.id]) { return; }
    seq = seq.then(function () {
      return WriteQueue.push(function () {
        return pendingStatement(claim).then(function () {
          existing[claim.property.id] = true;
        });
      });
    });
  });
  return seq;
}
function applyTemplate(templateName) {
  var claims = (WbTemplates[templateName].prefilledClaims || []).slice();
  var seq = Promise.resolve();
  claims.forEach(function (claim) {
    seq = seq.then(function () {
      return WriteQueue.push(function () {
        return prefillStatement(claim, false); // returns a Promise
      });
    });
  });
  return seq;
}

  // ===== 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
      }));
    }

function applyTemplate(templateName) {
  var claims = (WbTemplates[templateName].prefilledClaims || []).slice();
  var chain = Promise.resolve();
  claims.forEach(function (claim) {
    chain = chain.then(function () { return prefillStatement(claim, false); });
  });
  return chain;
}

$('#applyTemplateBtn').off('click').on('click', function () {
  var $b=$(this).prop('disabled', true);
  applyTemplate($('#wbtemplate').val()).then(function(){ $b.prop('disabled', false); });
});
$('#addDetailsBtn').off('click').on('click', function () {
  var $b=$(this).prop('disabled', true);
  addDetails($('#wbtemplate').val(), { skipExisting: true }).then(function(){ $b.prop('disabled', false); });
});
  }

  // ===== 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);
    });

});