Jump to content

MediaWiki:Gadget-templating.js: Difference between revisions

From National Library of Greece
No edit summary
No edit summary
 
(12 intermediate revisions by the same user not shown)
Line 10: Line 10:
   commitDelay: 120        // short pause to let the widget commit before next step
   commitDelay: 120        // short pause to let the widget commit before next step


};
// Special property remapping
var WB_SPECIAL = {
  PRIMARY_P: 'P1',      // the “real” property to write
  PRIMARY_FROM: 'P10816', // the template surrogate to read from
  PENDING_ITEM: 'Q59' // the placeholder for items
};
};
// Only entities with P1 = this Q are valid templates
var TEMPLATE_CLASS_Q = 'Q196450';
// QID -> template key mapping for auto-select later
var WbTemplatesById = {};
// read ?template=Q123 from the URL
function getUrlTemplateId() {
  try {
    var p = new URLSearchParams(window.location.search);
    var t = p.get('template');
    if (t && /^Q\d+$/i.test(t)) return t.toUpperCase();
  } catch (e) {}
  return null;
}
function getTargetClassId(entity) {
  var s = (entity.claims && entity.claims[WB_SPECIAL.PRIMARY_FROM] || [])[0];
  return s && s.mainsnak && s.mainsnak.datavalue && s.mainsnak.datavalue.value && s.mainsnak.datavalue.value.id || null;
}
// gate: accept entities that have P1 = TEMPLATE_CLASS_Q
function isTemplateEntity(entity) {
  try {
    var arr = (entity.claims && entity.claims.P1) || [];
    for (var i = 0; i < arr.length; i++) {
      var ms = arr[i].mainsnak;
      if (
        ms && ms.snaktype === 'value' &&
        ms.datavalue && ms.datavalue.type === 'wikibase-entityid' &&
        ms.datavalue.value && ms.datavalue.value.id === TEMPLATE_CLASS_Q
      ) {
        return true;
      }
    }
  } catch (_) {}
  return false;
}
function removeAutoTemplateParams() {
  try {
    if (!history.replaceState) return;
    var url = new URL(window.location.href);
    url.searchParams.delete('template'); // required
    url.searchParams.delete('details');  // optional, if you used it
    history.replaceState(null, '', url.toString());
  } catch (e) {
    // very old browsers fallback: do nothing (avoid nuking other params)
  }
}


function getQualifierSnaks(sv) {
function getQualifierSnaks(sv) {
Line 203: Line 258:
     case 'wikibase-item':
     case 'wikibase-item':
     case 'wikibase-property':
     case 'wikibase-property':
       return dv && dv.id ? { id: dv.id, datatype: dt } : null;
       return dv && dv.id ? { id: dv.id, datatype: dt, _pending: (dv.id === WB_SPECIAL.PENDING_ITEM) } : null;


     case 'string':
     case 'string':
Line 209: Line 264:
     case 'url':
     case 'url':
     case 'edtf': // <- treat as plain text
     case 'edtf': // <- treat as plain text
       if (typeof dv === 'string' && dv === '_') return null;
       if (typeof dv === 'string') {
       return (typeof dv === 'string') ? dv : (dv && dv.value) || null;
        if (dv === '_') return null;
        if (dv === '-999999999') return { text: '', _pending: true }; // ← pending EDTF
        return dv;
       } else if (dv && typeof dv.value === 'string') {
        if (dv.value === '_') return null;
        if (dv.value === '-999999999') return { text: '', _pending: true }; // ← pending EDTF
        return dv.value;
      }
      return null;


     case 'monolingualtext':
     case 'monolingualtext':
Line 315: Line 378:
   }
   }
   // Wait until the statement list shows one more row for this property
   // Wait until the statement list shows one more row for this property
  return waitForStatementRendered(pid, prevCount, (window.WB_TIMING && WB_TIMING.renderTimeout) || 1200);
return waitForStatementRendered(pid, prevCount, WB_TIMING.renderTimeout || 1200);
}
}
function sleep(ms){ return new Promise(function(res){ setTimeout(res, ms); }); }
function sleep(ms){ return new Promise(function(res){ setTimeout(res, ms); }); }
Line 397: Line 460:


   if (dt === 'wikibase-item' && dv && dv.id) {
   if (dt === 'wikibase-item' && dv && dv.id) {
     q.value = { mainVal: { id: dv.id, datatype: 'wikibase-item' } };
     if (dv.id === WB_SPECIAL.PENDING_ITEM) {
      // Sentinel -> add the qualifier slot but no value
      q._scaffold = true;
    } else {
      q.value = { mainVal: { id: dv.id, datatype: 'wikibase-item' } };
    }
   } else if ((dt === 'string' || dt === 'external-id' || dt === 'url' || dt === 'edtf') &&
   } else if ((dt === 'string' || dt === 'external-id' || dt === 'url' || dt === 'edtf') &&
             typeof dv === 'string' && !isUnderscore(dv)) {
             typeof dv === 'string' && !isUnderscore(dv)) {
     q.value = { mainVal: dv };
     q.value = { mainVal: dv };
  }else if (dt === 'edtf') {
    var ed = valOf(dv);
    if (ed === '-999999999') {
      q._scaffold = true; // pending EDTF qualifier
    } else if (ed && !isUnderscore(ed)) {
      q.value = { mainVal: ed };
    }
   } else if (dt === 'monolingualtext' && dv && dv.text && !isUnderscore(dv.text)) {
   } else if (dt === 'monolingualtext' && dv && dv.text && !isUnderscore(dv.text)) {
     q.value = { mainVal: { text: dv.text, language: dv.language || userLang } };
     q.value = { mainVal: { text: dv.text, language: dv.language || userLang } };
Line 509: Line 584:
     for (var pid in claims) {
     for (var pid in claims) {
       pids[pid] = true; // mainsnak pid
       pids[pid] = true; // mainsnak pid
      // If the template uses P10816 as surrogate, we also need meta for P1
  if (pid === WB_SPECIAL.PRIMARY_FROM) {
    pids[WB_SPECIAL.PRIMARY_P] = true;
  }
       var stmts = claims[pid] || [];
       var stmts = claims[pid] || [];
       for (var i = 0; i < stmts.length; i++) {
       for (var i = 0; i < stmts.length; i++) {
Line 551: Line 630:
function transformToTemplate(entity, propMeta) {
function transformToTemplate(entity, propMeta) {
   var rawLabel = getBestLabel(entity.labels) || entity.id;
   var rawLabel = getBestLabel(entity.labels) || entity.id;
   var label = cleanTemplateLabel(rawLabel);
   var label = rawLabel; // cleanTemplateLabel(rawLabel);
  var targetClassId = getTargetClassId(entity);
 


   var prefilledClaims = [];
   var prefilledClaims = [];
Line 558: Line 639:
   var claims = entity.claims || {};
   var claims = entity.claims || {};
   for (var pid in claims) {
   for (var pid in claims) {
var statements = claims[pid];
  if (pid === WB_SPECIAL.PRIMARY_P) { continue; }
if (!statements || !statements.length) continue;
 
  // If we see P10816, we will write it *as* P1
  var targetPid = (pid === WB_SPECIAL.PRIMARY_FROM) ? WB_SPECIAL.PRIMARY_P : pid;
  var statements = claims[pid];
  if (!statements || !statements.length) continue;
var prop = propMeta[pid] || { id: pid, label: pid, datatype: 'string' };
  var prop = propMeta[targetPid] || { id: targetPid, label: targetPid, datatype: 'string' };
 
// Track if we found at least one concrete (non-placeholder) value across statements
// Track if we found at least one concrete (non-placeholder) value across statements
Line 574: Line 660:
  var mainVal = buildMainValFromMainsnak(msk, dt);
  var mainVal = buildMainValFromMainsnak(msk, dt);
  if (mainVal !== null) {
      if (mainVal !== null) {
    hadConcrete = true;
        var qualifiersRaw = extractQualifiersFromStatement(stmt, propMeta);
    var qualifiers = extractQualifiersFromStatement(stmt, propMeta).filter(qualifierHasValue);
        // Is this statement “pending” because of the sentinel?
    prefilledClaims.push({
var hasPending =
      property: { id: prop.id, label: prop.label, datatype: dt },
  (dt === 'wikibase-item' && mainVal._pending === true) ||
      value:    { mainVal: mainVal },
  (dt === 'edtf' && mainVal && mainVal._pending === true) ||
      qualifiers: qualifiers
  qualifiersRaw.some(function (q) { return q && q._scaffold === true; });
    });
  }
        if (hasPending) {
          // Leave row open; scaffold qualifier properties without values
          var mixedQs = qualifiersRaw.map(function(q){
            if (q && q._scaffold === true) {
              return { property: q.property, datatype: q.datatype, _scaffold: true };
            }
            return qualifierHasValue(q)
              ? q
              : { property: q.property, datatype: q.datatype, _scaffold: true };
          });
          userDefinedClaims.push({
            property: { id: prop.id, label: prop.label },
            datatype: prop.datatype,
            qualifiers: mixedQs
          });
          hadConcrete = true; // prevent extra fallback scaffold later
          continue; // do NOT prefill this statement
        }
        hadConcrete = true;
        var qualifiers = qualifiersRaw.filter(qualifierHasValue);
        prefilledClaims.push({
          property: { id: prop.id, label: prop.label, datatype: dt },
          value:    { mainVal: mainVal },
          qualifiers: qualifiers
        });
      }
}
}
Line 597: Line 709:


   }
   }
var template = { key: slugify(label), label: label, prefilledClaims: prefilledClaims, userDefinedClaims: userDefinedClaims };
var template = { key: slugify(label), label: label, prefilledClaims: prefilledClaims, userDefinedClaims: userDefinedClaims,  targetClassId: targetClassId,  // ← stays
};
console.log(template);
console.log(template);
   return template;
   return template;
Line 603: Line 716:




  function buildWbTemplatesFromQuery(query) {
function buildWbTemplatesFromQuery(query) {
    return searchTemplates(query).then(function (qids) {
  return searchTemplates(query).then(function (qids) {
      if (!qids.length) return {};
    if (!qids.length) return {};
      return fetchEntities(qids).then(function (entities) {
 
         return fetchPropertyMeta(entities).then(function (propMeta) {
    return fetchEntities(qids).then(function (entities) {
          var map = {};
      // Keep only entities that are actually templates (P1 = TEMPLATE_CLASS_Q)
          for (var eid in entities) {
      var gated = {};
            var tpl = transformToTemplate(entities[eid], propMeta);
      for (var id in entities) {
            map[tpl.key] = {
         if (isTemplateEntity(entities[id])) gated[id] = entities[id];
              label: tpl.label,
      }
              prefilledClaims: tpl.prefilledClaims,
      if (!Object.keys(gated).length) return {};
              userDefinedClaims: tpl.userDefinedClaims
 
             };
      return fetchPropertyMeta(gated).then(function (propMeta) {
        var map = {};
        WbTemplatesById = {}; // reset mapping for this run
 
        for (var eid in gated) {
          var tpl = transformToTemplate(gated[eid], propMeta);
          map[tpl.key] = {
            label: tpl.label,
            prefilledClaims: tpl.prefilledClaims,
            userDefinedClaims: tpl.userDefinedClaims,
             targetClassId: tpl.targetClassId
          };
          // remember which template key corresponds to which QID
          WbTemplatesById[gated[eid].id] = tpl.key;
        }
 
          // collect target class Q-ids
        var classIds = [];
        for (var k in map) {
          if (map[k].targetClassId) classIds.push(map[k].targetClassId);
        }
        // de-dup
        classIds = Array.from(new Set(classIds));
 
        if (!classIds.length) {
          return map;
        }
 
        // fetch class labels and attach to each template
        return fetchEntities(classIds).then(function(ents){
          for (var k in map) {
            var cid = map[k].targetClassId;
            if (cid && ents[cid]) {
              map[k].targetClass = { id: cid, label: getBestLabel(ents[cid].labels) || cid };
            } else if (cid) {
              map[k].targetClass = { id: cid, label: cid };
            } else {
              map[k].targetClass = null;
            }
           }
           }
            // 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;
           return map;
         });
         });
       });
       });
     });
     });
   }
   });
}


   // ===== 4) YOUR existing helpers (unchanged) =====
   // ===== 4) YOUR existing helpers (unchanged) =====
Line 821: Line 971:
    if (ctx.dt === 'wikibase-item') {
    if (ctx.dt === 'wikibase-item') {
      emitStatementChange(sv);
      emitStatementChange(sv);
      return waitForSavableStatement(sv, WB_TIMING.valueViewTimeout).then(function () { return ctx; });
      return waitForSavableStatement(sv, snak, WB_TIMING.valueViewTimeout).then(function () { return ctx; });
    }
    }
    return ctx; // non-item: proceed immediately
    return ctx; // non-item: proceed immediately
Line 950: Line 1,100:


function addOneQualifierToStatement(sv, q) {
function addOneQualifierToStatement(sv, q) {
  // skip scaffold on prefilled path
  // If no concrete value AND not marked for scaffolding, skip
  if (!qualifierHasValue(q)) return Promise.resolve();
  if (!qualifierHasValue(q) && !q._scaffold) return Promise.resolve();


   return new Promise(function (resolve) {
   return new Promise(function (resolve) {
Line 973: Line 1,123:
         qsel._trigger('change');
         qsel._trigger('change');
         qsel._select(qprop);
         qsel._select(qprop);
 
        // If this is a scaffold-only qualifier, leave its value blank
        if (q._scaffold === true) { resolve(); return; }
       
         var dt = q.property.datatype || q.datatype || 'string';
         var dt = q.property.datatype || q.datatype || 'string';
         waitForValueWidget(qsnak, dt, WB_TIMING.widgetTimeout).then(function (vw) {
         waitForValueWidget(qsnak, dt, WB_TIMING.widgetTimeout).then(function (vw) {
Line 1,105: Line 1,257:
     $('.wikibase-entitytermsview').append(html);
     $('.wikibase-entitytermsview').append(html);


     var key;
  // group templates by target class label
     for (key in WbTemplates) {
     var groups = {}; // groupLabel -> [{key, label}]
       $('#wbtemplate').append($('<option>', {
     for (var k in WbTemplates) {
        value: key,
       var grp = (WbTemplates[k].targetClass && WbTemplates[k].targetClass.label) || 'Other';
        text: WbTemplates[key].label
      if (!groups[grp]) groups[grp] = [];
      }));
      groups[grp].push({ key: k, label: WbTemplates[k].label });
     }
     }
    var $sel = $('#wbtemplate').empty();
    Object.keys(groups).sort().forEach(function(glab){
      var $og = $('<optgroup>').attr('label', glab);
      groups[glab].forEach(function(opt){
        $og.append($('<option>', { value: opt.key, text: opt.label }));
      });
      $sel.append($og);
    });




Line 1,124: Line 1,285:
   }
   }


  // ===== 6) Build templates, then render UI =====
// ===== 6) Build templates, then render UI =====
  buildWbTemplatesFromQuery(SEARCH_QUERY)
var _requestedQid = getUrlTemplateId();
    .then(function (map) {
 
      WbTemplates = map;
buildWbTemplatesFromQuery(SEARCH_QUERY)
      window.WbTemplates = WbTemplates; // if you need it elsewhere
  .then(function (map) {
      if (!WbTemplates || !Object.keys(WbTemplates).length) {
    WbTemplates = map;
        console.warn('No templates found for query:', SEARCH_QUERY);
    window.WbTemplates = WbTemplates;
        return;
    if (!WbTemplates || !Object.keys(WbTemplates).length) {
      }
      console.warn('No templates found for query:', SEARCH_QUERY);
      initTemplatingUI();
      return;
     })
    }
     .fail(function (err) {
 
      // If this fails for anonymous users, it might be auth/profile config
    initTemplatingUI();
       // related; try while logged in.
 
      console.error('Template build failed:', err);
     // Auto-select ONLY if it’s in the loaded (gated) templates
    });
     if (_requestedQid && WbTemplatesById[_requestedQid]) {
      var key = WbTemplatesById[_requestedQid];
      $('#wbtemplate').val(key);
      var $btn = $('#applyTemplateBtn').prop('disabled', true);
      applyTemplate(key).then(function () {
        $btn.prop('disabled', false);
        // optional: also add details
        addDetails(key, { skipExisting: true }).then(function(){
   
    removeAutoTemplateParams();
        });
       });
    }
  })
  .fail(function (err) {
    console.error('Template build failed:', err);
  });


});
});

Latest revision as of 18:11, 25 September 2025

/* global mw, $ */
mw.loader.using(['mediawiki.api']).done(function () {
// ---- timing knobs (ms) ----
var WB_TIMING = {
  pollInterval: 50,        // how often we poll for readiness
  valueViewTimeout: 1200,  // wait for value view to exist
  widgetTimeout: 1800,     // wait for input widget/$input
  renderTimeout: 1200,     // wait for statement list to reflect the save (+1)
  settleDelay: 0,           // optional pause between claims (0 = off)
  commitDelay: 120        // short pause to let the widget commit before next step

};
// Special property remapping
var WB_SPECIAL = {
  PRIMARY_P: 'P1',       // the “real” property to write
  PRIMARY_FROM: 'P10816', // the template surrogate to read from
  PENDING_ITEM: 'Q59' // the placeholder for items
};


// Only entities with P1 = this Q are valid templates
var TEMPLATE_CLASS_Q = 'Q196450';

// QID -> template key mapping for auto-select later
var WbTemplatesById = {};

// read ?template=Q123 from the URL
function getUrlTemplateId() {
  try {
    var p = new URLSearchParams(window.location.search);
    var t = p.get('template');
    if (t && /^Q\d+$/i.test(t)) return t.toUpperCase();
  } catch (e) {}
  return null;
}
function getTargetClassId(entity) {
  var s = (entity.claims && entity.claims[WB_SPECIAL.PRIMARY_FROM] || [])[0];
  return s && s.mainsnak && s.mainsnak.datavalue && s.mainsnak.datavalue.value && s.mainsnak.datavalue.value.id || null;
}
// gate: accept entities that have P1 = TEMPLATE_CLASS_Q
function isTemplateEntity(entity) {
  try {
    var arr = (entity.claims && entity.claims.P1) || [];
    for (var i = 0; i < arr.length; i++) {
      var ms = arr[i].mainsnak;
      if (
        ms && ms.snaktype === 'value' &&
        ms.datavalue && ms.datavalue.type === 'wikibase-entityid' &&
        ms.datavalue.value && ms.datavalue.value.id === TEMPLATE_CLASS_Q
      ) {
        return true;
      }
    }
  } catch (_) {}
  return false;
}
function removeAutoTemplateParams() {
  try {
    if (!history.replaceState) return;
    var url = new URL(window.location.href);
    url.searchParams.delete('template'); // required
    url.searchParams.delete('details');  // optional, if you used it
    history.replaceState(null, '', url.toString());
  } catch (e) {
    // very old browsers fallback: do nothing (avoid nuking other params)
  }
}

function getQualifierSnaks(sv) {
  var out = [];
  try {
    var items = sv._qualifiers && sv._qualifiers.items ? sv._qualifiers.items() : [];
    if (!items || !items.length) return out;
    var qslv = $(items[items.length - 1]).data().snaklistview;
    var list = qslv && qslv._listview ? qslv._listview.items() : [];
    list.each(function () {
      var qsv = $(this).data().snakview;
      if (qsv) out.push(qsv);
    });
  } catch (e) {}
  return out;
}

function ensureQualifiersCommitted(sv, perTimeout) {
  var qs = getQualifierSnaks(sv);
  var p = Promise.resolve();
  for (var i = 0; i < qs.length; i++) {
    (function (qsnak) {
      p = p.then(function () {
        // try to observe a non-null datavalue; if it times out, continue anyway
        return waitForViewValue(qsnak, perTimeout || 500).catch(function(){});
      });
    })(qs[i]);
  }
  return p;
}

function blurAllQualifierInputs(sv) {
  try {
    var items = sv._qualifiers && sv._qualifiers.items ? sv._qualifiers.items() : [];
    if (!items || !items.length) return;
    var qslv = $(items[items.length - 1]).data().snaklistview;
    var list = qslv && qslv._listview ? qslv._listview.items() : [];
    list.each(function () {
      try {
        var qsnak = $(this).data().snakview;
        var vv = qsnak && qsnak._variation && qsnak._variation._valueView;
        var ex = vv && vv._expert;
        var $inp = ex && ex.$input;
        if ($inp && $inp.length) { $inp.trigger('input').trigger('change').blur(); }
        emitValueViewChange(qsnak);
      } catch (e) {}
    });
  } catch (e) {}
}
function qualifierHasValue(q) {
  if (!q || !q.property) return false;
  var dt = q.property.datatype || q.datatype || 'string';
  var mv = q.value && q.value.mainVal;

  if (!mv) return false;
  if (dt === 'wikibase-item') return !!mv.id;
  if (dt === 'monolingualtext') return !!(mv.text && mv.text !== '_');
	if (dt === 'string' || dt === 'external-id' || dt === 'url' || dt === 'edtf')
  return String(mv).length > 0 && mv !== '_';
  if (dt === 'globe-coordinate' || dt === 'globecoordinate') return typeof mv.latitude === 'number' && typeof mv.longitude === 'number';
  if (dt === 'time') return !!mv.time;
  if (dt === 'quantity') return !!(mv.amount || mv.value);
  return false;
}

function toInputText(dt, mv) {
  if (!mv) return '';
  if (typeof mv === 'string') return mv;
  if (dt === 'string' || dt === 'external-id' || dt === 'url' || dt === 'edtf') return (mv.text || mv.value || '') + '';
  if (dt === 'monolingualtext') return (mv.text || '') + '';
  if (dt === 'globe-coordinate' || dt === 'globecoordinate') {
    return (mv.latitude != null && mv.longitude != null) ? (mv.latitude + ',' + mv.longitude) : '';
  }
  if (dt === 'time') return (mv.time || '') + '';
  if (dt === 'quantity') return (mv.amount || mv.value || '') + '';
  return (mv.text || mv.value || '') + '';
}

/* Commit simple datatypes so valueView.value() becomes non-null */
function hardCommitSimpleValue(sv, snak, dt, mainVal) {
  return waitForValueWidget(snak, dt, 800).then(function(vw){
    var vv = snak && snak._variation && snak._variation._valueView;
    var ex = vv && vv._expert;
    var $inp = vw && vw.$input;
    var txt  = toInputText(dt, mainVal);

    try {
      if ($inp && $inp.length) {
        // type target
        $inp.val(txt).trigger('input').trigger('change');
        // legacy eachchange hooks
        if (vw.jqe && vw.jqe.handlers) {
          for (var i = 0; i < vw.jqe.handlers.length; i++) { try { vw.jqe.handlers[i].call(); } catch(e) {} }
        }
        // some builds need an explicit apply on the expert widget
        if (ex && typeof ex._apply === 'function') ex._apply();
        else if (ex && typeof ex.applyValue === 'function') ex.applyValue();
        $inp.blur();
      }
    } catch(e1) {}

    // notify both views
    emitValueViewChange(snak);
    try { var se = sv.widgetEventPrefix ? (sv.widgetEventPrefix + 'change') : 'change'; sv.element && sv.element.trigger(se); } catch(e2){}

    // if still null, toggle placeholder then back (forces conversion)
    return waitForViewValue(snak, 600).catch(function(){
      try {
        if ($inp && $inp.length) {
          $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(e3) {} }
          }
          if (ex && typeof ex._apply === 'function') ex._apply();
          else if (ex && typeof ex.applyValue === 'function') ex.applyValue();

          $inp.val(txt).trigger('input').trigger('change').blur();
        }
      } catch(e4) {}
      emitValueViewChange(snak);
      return waitForViewValue(snak, 600);
    });
  });
}
// set vv.value(...) AND (if widget exists) type into the input + fire eachchange
function setAndCommitValue(snak, dt, mainVal) {
  // 1) try datamodel set
  var dvSet = false;
  try { dvSet = setValueOnValueView(snak, dt, mainVal); } catch (e) { dvSet = false; }

  // 2) if non-item, also drive the input widget when present
  if (dt === 'wikibase-item') {
    // items are handled elsewhere (pickEntity), nothing to do here
    emitValueViewChange(snak);
    return Promise.resolve(dvSet);
  }

  return waitForValueWidget(snak, dt, 600).then(function (vw) {
    try {
      var txt = toInputText(dt, mainVal);
      if (vw.$input && vw.$input.length) {
        vw.$input.val(txt).trigger('input').trigger('change').blur();
      }
      // legacy commit hooks
      if (vw.jqe && vw.jqe.handlers) {
        for (var i = 0; i < vw.jqe.handlers.length; i++) {
          try { vw.jqe.handlers[i].call(); } catch (e2) {}
        }
      }
    } catch (e3) {}
    emitValueViewChange(snak);
    return true;
  }, function () {
    // no widget surfaced; at least emit change for vv
    emitValueViewChange(snak);
    return dvSet;
  });
}
function qidToNumeric(id) {
  return parseInt(String(id).replace(/^[A-Za-z]/, ''), 10);
}
function emitValueViewChange(snak) {
  try {
    var vv = snak && snak._variation && snak._variation._valueView;
    if (!vv || !vv.element) return;
    var ev = vv.widgetEventPrefix ? (vv.widgetEventPrefix + 'change') : 'change';
    vv.element.trigger(ev);
  } catch (e) {}
}

// Wait until the valueView has a non-null value() (i.e., savable)
function waitForViewValue(snak, timeoutMs) {
  var start = Date.now();
  var timeout = (typeof timeoutMs === 'number') ? timeoutMs : WB_TIMING.valueViewTimeout;
  return new Promise(function (resolve, reject) {
    (function tick() {
      try {
        var vv = snak && snak._variation && snak._variation._valueView;
        var val = vv && typeof vv.value === 'function' ? vv.value() : null;
        if (val !== null) return resolve(val);
      } catch (e) {}
      if (Date.now() - start > timeout) return reject(new Error('view value still null'));
      setTimeout(tick, WB_TIMING.pollInterval);
    })();
  });
}

function buildMainValFromMainsnak(ms, dt) {
  if (!ms || ms.snaktype !== 'value' || !ms.datavalue) return null;
  var dv = ms.datavalue.value;
  switch (dt) {
    case 'wikibase-item':
    case 'wikibase-property':
      return dv && dv.id ? { id: dv.id, datatype: dt, _pending: (dv.id === WB_SPECIAL.PENDING_ITEM) } : null;

    case 'string':
    case 'external-id':
    case 'url':
    case 'edtf': // <- treat as plain text
      if (typeof dv === 'string') {
        if (dv === '_') return null;
        if (dv === '-999999999') return { text: '', _pending: true }; // ← pending EDTF
        return dv;
      } else if (dv && typeof dv.value === 'string') {
        if (dv.value === '_') return null;
        if (dv.value === '-999999999') return { text: '', _pending: true }; // ← pending EDTF
        return dv.value;
      }
      return null;

    case 'monolingualtext':
      if (!dv || !dv.text || dv.text === '_') return null;
      return { text: dv.text, language: dv.language || (typeof userLang === 'string' ? userLang : 'en') };

    case 'time':
      return dv && dv.time ? { time: dv.time, precision: dv.precision } : null;

    case 'globe-coordinate':
    case 'globecoordinate':
      return (dv && typeof dv.latitude === 'number' && typeof dv.longitude === 'number')
        ? { latitude: dv.latitude, longitude: dv.longitude, precision: dv.precision, globe: dv.globe }
        : null;

    case 'quantity':
      return dv && dv.amount
        ? { amount: dv.amount, unit: dv.unit || null, upperBound: dv.upperBound, lowerBound: dv.lowerBound }
        : null;

    default:
      return dv && (dv.id || dv.text || dv.time || dv.latitude != null || dv.amount)
        ? dv
        : null;
  }
}



function buildDataValueFor(dt, mainVal) {
  if (!mainVal) return null;
  if (dt === 'wikibase-item' || dt === 'wikibase-property') {
    var id = mainVal.id;
    return {
      type: 'wikibase-entityid',
      value: {
        id: id,
        'entity-type': (/^P/i.test(id) ? 'property' : 'item'),
        'numeric-id': qidToNumeric(id)
      }
    };
  }
if (dt === 'string' || dt === 'external-id' || dt === 'url' || dt === 'edtf') {
  return { type: 'string', value: mainVal.text || mainVal.value || String(mainVal) };
}

  if (dt === 'monolingualtext') {
    return { type: 'monolingualtext', value: { text: mainVal.text || '', language: mainVal.language || 'en' } };
  }
  if (dt === 'time') {
    return { type: 'time', value: {
      time: mainVal.time,
      precision: mainVal.precision || 11,
      timezone: 0, before: 0, after: 0,
      calendarmodel: 'http://www.wikidata.org/entity/Q1985727'
    }};
  }
  if (dt === 'globe-coordinate' || dt === 'globecoordinate') {
    return { type: 'globecoordinate', value: {
      latitude: mainVal.latitude, longitude: mainVal.longitude,
      precision: mainVal.precision || 1e-6,
      globe: mainVal.globe || 'http://www.wikidata.org/entity/Q2'
    }};
  }
  return null;
}

/** Set the value directly on the valueView and emit a change */
function setValueOnValueView(snak, dt, mainVal) {
  try {
    var vv = snak._variation && snak._variation._valueView;
    var dv = buildDataValueFor(dt, mainVal);
    if (!vv || !dv) return false;
    if (typeof vv.value === 'function') {
      vv.value(dv); // set datamodel value
      // notify listeners (statementview watches these)
      var ev = vv.widgetEventPrefix ? (vv.widgetEventPrefix + 'change') : 'change';
      vv.element && vv.element.trigger(ev);
      return true;
    }
  } catch (e) {}
  return false;
}

function doSave(toolbar, pid, prevCount) {
  // Try the controller API first
  try {
    if (toolbar && toolbar._controller && typeof toolbar._controller.stopEditing === 'function') {

      toolbar._controller.stopEditing(false);
    } else {
      // Fallback: click the save/publish button in the toolbar DOM
      var $root = toolbar && toolbar.$element ? toolbar.$element : $(document);
      var $btn = $root.find('.wikibase-toolbarbutton-save, .wikibase-toolbarbutton-publish');
      if ($btn.length) { $btn.click(); }
    }
  } catch (e) {
    console.warn('doSave: toolbar save failed, trying button fallback', e);
    try {
      var $root2 = toolbar && toolbar.$element ? toolbar.$element : $(document);
      var $btn2 = $root2.find('.wikibase-toolbarbutton-save, .wikibase-toolbarbutton-publish');
      if ($btn2.length) { $btn2.click(); }
    } catch (e2) {}
  }
  // Wait until the statement list shows one more row for this property
 return waitForStatementRendered(pid, prevCount, WB_TIMING.renderTimeout || 1200);
}
function sleep(ms){ return new Promise(function(res){ setTimeout(res, ms); }); }

  // ===== 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;
  }
  
// Robustly select a wikibase entity in an EntitySelector (plugin optional)
function pickEntity(vw, qid, commitDelay) {
  return new Promise(function (resolve) {
    var committed = false;
    try {
      // Preferred: official OOUI widget API (if exposed)
      if (vw.plugin && typeof vw.plugin.setValue === 'function') {
        vw.plugin.setValue(qid);
        committed = true;
      }
      // Common fallback on many builds
      else if (vw.plugin && typeof vw.plugin._select === 'function') {
        vw.plugin._select({ id: qid, label: qid });
        committed = true;
      }
    } catch (e) {}

    // Last resort: simulate typing + Enter + blur on the raw input
    if (!committed) {
      try {
        vw.$input
          .val(qid)
          .trigger('input')
          .trigger('change')
          .trigger({ type: 'keydown', which: 13, keyCode: 13 })
          .trigger({ type: 'keyup',   which: 13, keyCode: 13 })
          .blur();
        committed = true;
      } catch (e2) {}
    }

    // Let the widget finish internal commits
    sleep(typeof commitDelay === 'number' ? commitDelay : 120).then(resolve);
  });
}

function buildQualifierFromSnak(qpid, snak, propMeta) {
  if (!snak || snak.snaktype !== 'value') {
    return {
      property: { id: qpid, label: (propMeta[qpid] && propMeta[qpid].label) || qpid, datatype: (propMeta[qpid] && propMeta[qpid].datatype) || (snak && snak.datatype) || 'string' },
      datatype: (propMeta[qpid] && propMeta[qpid].datatype) || (snak && snak.datatype) || 'string'
    };
  }

  var meta = propMeta[qpid] || { id: qpid, label: qpid, datatype: snak.datatype || 'string' };
  var dt = meta.datatype || snak.datatype || 'string';
  var dv = snak.datavalue && snak.datavalue.value;
  var q = { property: { id: meta.id, label: meta.label, datatype: dt }, datatype: dt };

  function isUnderscore(v) { return v === '_'; }

  if (dt === 'wikibase-item' && dv && dv.id) {
    if (dv.id === WB_SPECIAL.PENDING_ITEM) {
      // Sentinel -> add the qualifier slot but no value
      q._scaffold = true;
    } else {
      q.value = { mainVal: { id: dv.id, datatype: 'wikibase-item' } };
    }
  } else if ((dt === 'string' || dt === 'external-id' || dt === 'url' || dt === 'edtf') &&
             typeof dv === 'string' && !isUnderscore(dv)) {
    q.value = { mainVal: dv };
  }else if (dt === 'edtf') {
    var ed = valOf(dv);
    if (ed === '-999999999') {
      q._scaffold = true; // pending EDTF qualifier
    } else if (ed && !isUnderscore(ed)) {
      q.value = { mainVal: ed };
    }
  } else if (dt === 'monolingualtext' && dv && dv.text && !isUnderscore(dv.text)) {
    q.value = { mainVal: { text: dv.text, language: dv.language || userLang } };
  } else if ((dt === 'globe-coordinate' || dt === 'globecoordinate') && dv && typeof dv.latitude === 'number' && typeof dv.longitude === 'number') {
    q.value = { mainVal: { latitude: dv.latitude, longitude: dv.longitude, precision: dv.precision, globe: dv.globe } };
  } else if (dt === 'time' && dv && dv.time) {
    q.value = { mainVal: { time: dv.time, precision: dv.precision } };
  }
  return q;
}

function extractQualifiersFromStatement(stmt, propMeta) {
  var out = [];
  if (!stmt || !stmt.qualifiers) return out;

  // Respect qualifiers-order if present, otherwise iterate keys
  var order = stmt['qualifiers-order'];
  if (order && order.length) {
    for (var i = 0; i < order.length; i++) {
      var qpid = order[i];
      var arr = stmt.qualifiers[qpid] || [];
      for (var j = 0; j < arr.length; j++) {
        out.push(buildQualifierFromSnak(qpid, arr[j], propMeta));
      }
    }
  } else {
    for (var qlpid in stmt.qualifiers) {
      var arr2 = stmt.qualifiers[qlpid] || [];
      for (var k = 0; k < arr2.length; k++) {
        out.push(buildQualifierFromSnak(qlpid, arr2[k], propMeta));
      }
    }
  }
  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; // mainsnak pid
      // If the template uses P10816 as surrogate, we also need meta for P1
	  if (pid === WB_SPECIAL.PRIMARY_FROM) {
	    pids[WB_SPECIAL.PRIMARY_P] = true;
	  }
      var stmts = claims[pid] || [];
      for (var i = 0; i < stmts.length; i++) {
        var qobj = (stmts[i] && stmts[i].qualifiers) || {};
        for (var qpid in qobj) { pids[qpid] = true; } // qualifier pids
      }
    }
  }

  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 () {
    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 = rawLabel; // cleanTemplateLabel(rawLabel);
  var targetClassId = getTargetClassId(entity);


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

  var claims = entity.claims || {};
  for (var pid in claims) {
  	if (pid === WB_SPECIAL.PRIMARY_P) { continue; }

	  // If we see P10816, we will write it *as* P1
	  var targetPid = (pid === WB_SPECIAL.PRIMARY_FROM) ? WB_SPECIAL.PRIMARY_P : pid;
	  var statements = claims[pid];
	  if (!statements || !statements.length) continue;
	
	  var prop = propMeta[targetPid] || { id: targetPid, label: targetPid, datatype: 'string' };

	
	// Track if we found at least one concrete (non-placeholder) value across statements
	var hadConcrete = false;
	
	for (var i = 0; i < statements.length; i++) {
	  var stmt = statements[i];
	  var msk  = stmt && stmt.mainsnak;
	  if (!msk) continue;
	
	  var dt = prop.datatype || msk.datatype || 'string';
	  var mainVal = buildMainValFromMainsnak(msk, dt);
	
       if (mainVal !== null) {
         var qualifiersRaw = extractQualifiersFromStatement(stmt, propMeta);
         // Is this statement “pending” because of the sentinel?
		var hasPending =
		  (dt === 'wikibase-item' && mainVal._pending === true) ||
		  (dt === 'edtf' && mainVal && mainVal._pending === true) ||
		  qualifiersRaw.some(function (q) { return q && q._scaffold === true; });
 
         if (hasPending) {
           // Leave row open; scaffold qualifier properties without values
           var mixedQs = qualifiersRaw.map(function(q){
             if (q && q._scaffold === true) {
               return { property: q.property, datatype: q.datatype, _scaffold: true };
             }
             return qualifierHasValue(q)
               ? q
               : { property: q.property, datatype: q.datatype, _scaffold: true };
           });
           userDefinedClaims.push({
             property: { id: prop.id, label: prop.label },
             datatype: prop.datatype,
             qualifiers: mixedQs
           });
           hadConcrete = true; // prevent extra fallback scaffold later
           continue; // do NOT prefill this statement
         }
 
         hadConcrete = true;
         var qualifiers = qualifiersRaw.filter(qualifierHasValue);
         prefilledClaims.push({
           property: { id: prop.id, label: prop.label, datatype: dt },
           value:    { mainVal: mainVal },
           qualifiers: qualifiers
         });
       }
	}
	
	// If no concrete values at all, scaffold a single user-defined claim with qualifiers from the first stmt (if any)
	if (!hadConcrete) {
	  var sourceStmt = statements[0];
	  var qScaffold  = extractQualifiersFromStatement(sourceStmt, propMeta);
	  userDefinedClaims.push({
	    property: { id: prop.id, label: prop.label },
	    datatype: prop.datatype,
	    qualifiers: qScaffold
	  });
	}

  }
	var template = { key: slugify(label), label: label, prefilledClaims: prefilledClaims, userDefinedClaims: userDefinedClaims,   targetClassId: targetClassId,   // ← stays
};
	console.log(template);
  return template;
}


function buildWbTemplatesFromQuery(query) {
  return searchTemplates(query).then(function (qids) {
    if (!qids.length) return {};

    return fetchEntities(qids).then(function (entities) {
      // Keep only entities that are actually templates (P1 = TEMPLATE_CLASS_Q)
      var gated = {};
      for (var id in entities) {
        if (isTemplateEntity(entities[id])) gated[id] = entities[id];
      }
      if (!Object.keys(gated).length) return {};

      return fetchPropertyMeta(gated).then(function (propMeta) {
        var map = {};
        WbTemplatesById = {}; // reset mapping for this run

        for (var eid in gated) {
          var tpl = transformToTemplate(gated[eid], propMeta);
          map[tpl.key] = {
            label: tpl.label,
            prefilledClaims: tpl.prefilledClaims,
            userDefinedClaims: tpl.userDefinedClaims,
            targetClassId: tpl.targetClassId
          };
          // remember which template key corresponds to which QID
          WbTemplatesById[gated[eid].id] = tpl.key;
        }

          // collect target class Q-ids
        var classIds = [];
        for (var k in map) {
          if (map[k].targetClassId) classIds.push(map[k].targetClassId);
        }
        // de-dup
        classIds = Array.from(new Set(classIds));

        if (!classIds.length) {
          return map;
        }

        // fetch class labels and attach to each template
        return fetchEntities(classIds).then(function(ents){
          for (var k in map) {
            var cid = map[k].targetClassId;
            if (cid && ents[cid]) {
              map[k].targetClass = { id: cid, label: getBestLabel(ents[cid].labels) || cid };
            } else if (cid) {
              map[k].targetClass = { id: cid, label: cid };
            } else {
              map[k].targetClass = null;
            }
          }
          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 refreshSVContext(item) {
  try {
    var svItems = item.statementgroupview.statementlistview._listview.items();
    var last    = svItems.last().data();
    var sv      = last.statementview;
    var snak    = sv.$mainSnak.data().snakview;
    var toolbar = svItems.last().data().edittoolbar;
    return { sv: sv, snak: snak, toolbar: toolbar };
  } catch (e) {
    return { sv: null, snak: null, toolbar: null };
  }
}

function waitForStatementRendered(pid, prevCount, timeoutMs) {
  var start = Date.now(), timeout = timeoutMs || 1000;
  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, WB_TIMING.pollInterval);
    })();
  });
}
function waitForValueView(snak, timeoutMs) {
  var start = Date.now();
  var timeout = (typeof timeoutMs === 'number') ? timeoutMs : WB_TIMING.valueViewTimeout;
  return new Promise(function (resolve, reject) {
    (function tick() {
      try {
        var vv = snak && snak._variation && snak._variation._valueView;
        if (vv && vv.element && vv.element.length) return resolve(vv);
      } catch (e) {}
      if (Date.now() - start > timeout) return reject(new Error('value view not ready'));
      setTimeout(tick, WB_TIMING.pollInterval);
    })();
  });
}
// Count qualifier snaks on a statementview
function getQualifierCount(sv) {
  try {
    var items = sv._qualifiers && sv._qualifiers.items ? sv._qualifiers.items() : [];
    if (!items || !items.length) return 0;
    var qslv = $(items[items.length - 1]).data().snaklistview;
    return qslv && qslv._listview ? qslv._listview.items().length : 0;
  } catch (e) { return 0; }
}

function waitForQualifierRendered(sv, prevCount, timeoutMs) {
  var start = Date.now();
  var timeout = (typeof timeoutMs === 'number') ? timeoutMs : WB_TIMING.valueViewTimeout;
  return new Promise(function (resolve) {
    (function tick() {
      if (getQualifierCount(sv) > prevCount) return resolve();
      if (Date.now() - start > timeout) return resolve(); // fail-safe
      setTimeout(tick, WB_TIMING.pollInterval);
    })();
  });
}

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, WB_TIMING.pollInterval);
    })();
  });
}
function emitStatementChange(sv) {
  try {
    var ev = sv.widgetEventPrefix ? (sv.widgetEventPrefix + 'change') : 'change';
    sv.element && sv.element.trigger(ev);
  } catch (e) {}
}

// Wait until the row is savable (valueView.value() or statementview.value() becomes non-null).
// Always resolves: true = savable, false = timed out (we'll try to save anyway).
function waitForSavableStatement(sv, snak, timeoutMs) {
  var start = Date.now();
  var timeout = (typeof timeoutMs === 'number') ? timeoutMs : WB_TIMING.valueViewTimeout;

  function valueViewHasValue() {
    try {
      var vv = snak && snak._variation && snak._variation._valueView;
      if (vv && typeof vv.value === 'function') { return vv.value() !== null; }
    } catch (e) {}
    return false;
  }
  function statementViewHasValue() {
    try {
      if (sv && typeof sv.value === 'function') { return sv.value() !== null; }
    } catch (e) {}
    return false;
  }

  return new Promise(function (resolve) {
    (function tick() {
      if (valueViewHasValue() || statementViewHasValue()) return resolve(true);
      if (Date.now() - start > timeout) return resolve(false); // ← never reject
      setTimeout(tick, WB_TIMING.pollInterval);
    })();
  });
}
function prefillStatement(claim, onlyProperty) {
  return new Promise(function (resolve) {
    var pid = claim.property.id;
    var prevCount = getStatementCount(pid);

    var statementListView = $(".wikibase-statementgrouplistview").first().data().wikibaseStatementgrouplistview;
    statementListView.enterNewItem();

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

    // Select main property
    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);

    // Wait for the value view + widget, set main value (no save yet)
	// Wait for the value view, then commit main value (datamodel + widget for simple types)
	var mainVal = claim.value && claim.value.mainVal;
	var dt = claim.property.datatype || (mainVal && mainVal.datatype) || claim.datatype || 'string';
	
	waitForValueView(snak, WB_TIMING.valueViewTimeout)
	  .then(function () {
	    if (dt === 'wikibase-item' && mainVal && mainVal.id) {
	      return waitForValueWidget(snak, dt, WB_TIMING.widgetTimeout)
	        .then(function (vw) { return pickEntity(vw, mainVal.id, WB_TIMING.commitDelay).then(function(){ return { dt: dt, mainVal: mainVal }; }); },
	              function ()   { return { dt: dt, mainVal: mainVal }; });
	    }
	    // 👉 simple types: force the widget to commit so valueView.value() is non-null
	    return hardCommitSimpleValue(sv, snak, dt, mainVal).then(function(){ return { dt: dt, mainVal: mainVal }; });
	  })

		// 2) For items we do a quick savable wait; for non-items skip it (we’ll verify after qualifiers)
	  .then(function (ctx) {
	    if (ctx.dt === 'wikibase-item') {
	      emitStatementChange(sv);
	      return waitForSavableStatement(sv, snak, WB_TIMING.valueViewTimeout).then(function () { return ctx; });
	    }
	    return ctx; // non-item: proceed immediately
	  })

	 // 3) Add ALL qualifiers
	  .then(function (ctx) {
	    return addQualifiersSequentially(sv, claim.qualifiers || []).then(function () { return ctx; });
	  })

	 // 4) Row may have re-rendered: re-acquire sv/snak/toolbar
	.then(function () {
	  // re-acquire (qualifiers may re-render the row)
	  var ctx = refreshSVContext(item);
	  sv      = ctx.sv;
	  snak    = ctx.snak;
	  toolbar = ctx.toolbar;
	
	  var mainVal = claim.value && claim.value.mainVal;
	  var dt = claim.property.datatype || (mainVal && mainVal.datatype) || claim.datatype || 'string';
	
	  // re-commit mainsnak on the *current* row
	  if (dt === 'wikibase-item' && mainVal && mainVal.id) {
	    return waitForValueWidget(snak, dt, WB_TIMING.widgetTimeout)
	      .then(function (vw) { return pickEntity(vw, mainVal.id, WB_TIMING.commitDelay); })
	      .then(function () { emitStatementChange(sv); return waitForSavableStatement(sv, snak, WB_TIMING.valueViewTimeout); });
	  } else {
	    return hardCommitSimpleValue(sv, snak, dt, mainVal)
	      .then(function () { emitStatementChange(sv); return waitForSavableStatement(sv, snak, WB_TIMING.valueViewTimeout); });
	  }
	})
	.then(function () {
	  // already re-acquired sv/snak/toolbar and re-committed mainsnak above…
	
	  // make sure qualifiers aren’t holding focus and their datavalues exist
	  blurAllQualifierInputs(sv);
	  return ensureQualifiersCommitted(sv, 500);
	})
	.then(function () {
	  // make sure qualifiers aren’t holding focus
	  return doSave(toolbar, pid, prevCount);
	})
	.then(resolve)
	  .catch(function () { return doSave(toolbar, pid, prevCount).then(resolve); });
	});
}
function pendingStatement(claim, opts) {
  opts = opts || {};
  var shouldSave = (opts.save === true); // default: leave open (no save)

  var prop = claim.property;
  var pid = prop.id;
  var prevCount = getStatementCount(pid);

  return new Promise(function (resolve) {
    var statementListView = $(".wikibase-statementgrouplistview").first().data().wikibaseStatementgrouplistview;
    statementListView.enterNewItem();

    var items   = statementListView.listview.items();
    var item    = items.last().data();
    var svItems = item.statementgroupview.statementlistview._listview.items();
    var sv      = svItems.last().data().statementview; // <- always last row we just created
    var snak    = sv.$mainSnak.data().snakview;
    var toolbar = svItems.last().data().edittoolbar;

    // Build the full property entity object the selector expects
    var es = getPropertyEntity(prop);
    var selector = snak._getPropertySelector();

    // Resolve when property has actually been applied
    var propertyReady = new Promise(function (res) {
      var done = false; function ok(){ if(!done){ done=true; res(); } }
      selector.element.one('entityselectorselected', ok);
      $(snak._variation).one('afterdraw', ok);
    });

    // Select the property (redraw value view)
    selector.element.val(es.label);
    selector.element.attr('title', es.label);
    selector._trigger('change');
    selector._select(es);

    propertyReady.then(function () {
      var dt = claim.datatype || prop.datatype || 'string';

      // Wait until the value widget exists
      return waitForValueWidget(snak, dt, WB_TIMING.widgetTimeout).then(function (vw) {
        try {
          // Poke the widget so the input is visibly open, then clear to blank
          if (dt === 'wikibase-item') {
            if (vw.plugin && typeof vw.plugin._select === 'function') {
              vw.plugin._select({ id: 'Q80', datatype: 'wikibase-item' });
            } else {
              vw.$input.val('Q80').trigger('input').trigger('change');
            }
            vw.$input.val('').trigger('input').trigger('change');
          } else if (dt === 'globe-coordinate' || dt === 'globecoordinate') {
            vw.$input.val('0,0').trigger('input').trigger('change');
            vw.$input.val('').trigger('input').trigger('change');
          } else {
            vw.$input.val('__').trigger('input').trigger('change');
            vw.$input.val('').trigger('input').trigger('change');
          }
          // keep focus on main value field for user convenience
          try { vw.$input.focus(); } catch (e) {}
        } catch (e) { /* ignore */ }

        // If the template carries qualifier scaffolds, add them (still no save)
        if (claim.qualifiers && claim.qualifiers.length) {
          return addQualifiersSequentially(sv, claim.qualifiers);
        }
      }).then(function () {
        // If caller asked to save (rare for details), do it; otherwise resolve.
        if (shouldSave) {
          return doSave(toolbar, pid, prevCount).then(resolve);
        }
        // Leave the row open in edit mode:
        // nudge the statement so toolbar enables Save button in case user types
        emitStatementChange(sv);
        resolve();
      }).catch(function () {
        // Even if something failed (e.g., widget too slow), just resolve to keep UI responsive
        resolve();
      });
    });
  });
}

function addOneQualifierToStatement(sv, q) {
   // If no concrete value AND not marked for scaffolding, skip
   if (!qualifierHasValue(q) && !q._scaffold) return Promise.resolve();

  return new Promise(function (resolve) {
    try {
      var qlistview = sv._qualifiers;
      var prev = getQualifierCount(sv);
      qlistview.enterNewItem();

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

      waitForQualifierRendered(sv, prev).then(function () {
        var qsnak = qslv._listview.items().last().data().snakview;
        var qprop = getPropertyEntity(q.property);
        var qsel = qsnak._getPropertySelector();

        qsel.element.val(qprop.label);
        qsel._trigger('change');
        qsel._select(qprop);
        // If this is a scaffold-only qualifier, leave its value blank
        if (q._scaffold === true) { resolve(); return; }
        
        var dt = q.property.datatype || q.datatype || 'string';
        waitForValueWidget(qsnak, dt, WB_TIMING.widgetTimeout).then(function (vw) {
          try {
            if (dt === 'wikibase-item' && q.value && q.value.mainVal && q.value.mainVal.id) {
              return pickEntity(vw, q.value.mainVal.id, WB_TIMING.commitDelay).then(function () {
                try {         // If the selector didn't show a label (empty visual), hint the ID in the input.
			        // We do NOT change the selection; we just help the user see something.
			        if (vw.$input && (!vw.$input.val() || !$.trim(vw.$input.val()))) {
			          vw.$input.attr('placeholder', q.value.mainVal.id); // visible hint
			          vw.$input.attr('title', q.value.mainVal.id);       // tooltip on hover
			          vw.$input.val(q.value.mainVal.id);
			        }
			        // Blur to finalize the qualifier UI state
			        vw.$input && vw.$input.blur();
			    } catch(e){}
                emitValueViewChange(qsnak); // commit qualifier valueView
                resolve();
              });
            } else {
			    // >>> NEW: robust commit for simple types (string/url/ext-id/monolingual/time/coord/quantity)
			    var vv = qsnak && qsnak._variation && qsnak._variation._valueView;
			    var ex = vv && vv._expert;
			    var $inp = vw && vw.$input;
			    var base = toInputText( dt, q.value && q.value.mainVal );
			    var nudged = base + (base ? ' ' : '  ');
			
			    function fireEachchange() {
			      try {
			        if (vw.jqe && vw.jqe.handlers) {
			          for (var i = 0; i < vw.jqe.handlers.length; i++) { try { vw.jqe.handlers[i].call(); } catch(_){} }
			        }
			      } catch(_) {}
			    }
			    function applyExpert() {
			      try {
			        if (ex && typeof ex._apply === 'function') ex._apply();
			        else if (ex && typeof ex.applyValue === 'function') ex.applyValue();
			      } catch(_) {}
			    }
			
			    if ($inp && $inp.length) {
			      $inp.val(base).trigger('input').trigger('change'); fireEachchange(); applyExpert();
			      $inp.val(nudged).trigger('input').trigger('change'); fireEachchange(); applyExpert();
			      $inp.val(base).trigger('input').trigger('change'); fireEachchange(); applyExpert();
			      $inp.blur();
			    }
			    emitValueViewChange(qsnak);
			
			    // ensure the qualifier has a non-null datavalue before we proceed
			    return waitForViewValue(qsnak, 600).then(function(){ resolve(); }, function(){ resolve(); });
			  }
          } catch (e) { resolve(); }
        }).catch(function () { resolve(); });
      });
    } catch (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;
}


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

  // group templates by target class label
    var groups = {}; // groupLabel -> [{key, label}]
    for (var k in WbTemplates) {
      var grp = (WbTemplates[k].targetClass && WbTemplates[k].targetClass.label) || 'Other';
      if (!groups[grp]) groups[grp] = [];
      groups[grp].push({ key: k, label: WbTemplates[k].label });
    }

    var $sel = $('#wbtemplate').empty();
    Object.keys(groups).sort().forEach(function(glab){
      var $og = $('<optgroup>').attr('label', glab);
      groups[glab].forEach(function(opt){
        $og.append($('<option>', { value: opt.key, text: opt.label }));
      });
      $sel.append($og);
    });


$('#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 =====
var _requestedQid = getUrlTemplateId();

buildWbTemplatesFromQuery(SEARCH_QUERY)
  .then(function (map) {
    WbTemplates = map;
    window.WbTemplates = WbTemplates;
    if (!WbTemplates || !Object.keys(WbTemplates).length) {
      console.warn('No templates found for query:', SEARCH_QUERY);
      return;
    }

    initTemplatingUI();

    // Auto-select ONLY if it’s in the loaded (gated) templates
    if (_requestedQid && WbTemplatesById[_requestedQid]) {
      var key = WbTemplatesById[_requestedQid];
      $('#wbtemplate').val(key);
      var $btn = $('#applyTemplateBtn').prop('disabled', true);
      applyTemplate(key).then(function () {
        $btn.prop('disabled', false);
        // optional: also add details
        addDetails(key, { skipExisting: true }).then(function(){
    
    		removeAutoTemplateParams();
        });
      });
    }
  })
  .fail(function (err) {
    console.error('Template build failed:', err);
  });

});