• File: ipcard.js
  • Full Path: /home/daemondevin/public_html/portableapps/assets/admin/themestrap/ipcard.js
  • Date Modified: 10/26/2026 5:36 AM
  • File size: 4.32 KB
  • MIME-type: application/javascript
  • Charset: utf-8
// IP address hover cards. Each address is a Themestrap HoverCard that asks the blog for details the first time
// it is shown. The blog fetches them from the lookup service and remembers the answer, so the browser never
// contacts the service itself.
(function ($) {
  'use strict';
  var root = document.querySelector('[data-ip-endpoint]');
  if (!root || !$.fn.themestrapPluginHoverCard) return;
  var endpoint = root.getAttribute('data-ip-endpoint');
  var cache = {};

  function label(k) {
    return k.replace(/[_\-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, function (c) { return c.toUpperCase(); });
  }

  // Two letter country code to a flag emoji
  function flag(cc) {
    if (!/^[A-Za-z]{2}$/.test(cc)) return '';
    return String.fromCodePoint.apply(null, cc.toUpperCase().split('').map(function (c) { return 127397 + c.charCodeAt(0); }));
  }

  function pick(d, names) {
    var keys = Object.keys(d);
    for (var i = 0; i < names.length; i++) {
      for (var j = 0; j < keys.length; j++) if (keys[j].toLowerCase().replace(/[ _\-]/g, '') === names[i]) return keys[j];
    }
    // Then names inside a group, such as "connection isp"
    for (var a = 0; a < names.length; a++) {
      if (names[a].length < 3) continue;
      for (var b = 0; b < keys.length; b++) if (keys[b].toLowerCase().replace(/[ _\-]/g, '').slice(-names[a].length) === names[a]) return keys[b];
    }
    return null;
  }

  function render($card, ip, r) {
    $card.empty();
    var $head = $('<div class="hc-name a-ip-title">').appendTo($card);
    if (!r.ok) {
      $head.text(ip);
      $('<p class="hc-bio a-ip-msg">').text(r.error || 'No details available.').appendTo($card);
      return;
    }
    if (r.local) {
      $head.text(ip);
      $('<p class="hc-bio a-ip-msg">').text('A private or local address. There is nothing to look up.').appendTo($card);
      return;
    }
    var d = r.data, used = {};
    var cityK = pick(d, ['city']), regK = pick(d, ['region', 'regionname', 'state']), cK = pick(d, ['country', 'countryname']), ccK = pick(d, ['countrycode', 'countrycode2', 'cc', 'iso2']);
    var place = [cityK, regK, cK].filter(Boolean).map(function (k) { used[k] = 1; return d[k]; });
    if (ccK) used[ccK] = 1;
    var f = ccK ? flag(d[ccK]) : (cK && flag(d[cK]) ? flag(d[cK]) : '');
    $head.text((f ? f + ' ' : '') + (place.join(', ') || ip));
    $('<div class="hc-handle a-mono">').text(ip).appendTo($card);
    ['ip', 'query', 'success', 'status', 'ipaddress'].forEach(function (n) { var k = pick(d, [n]); if (k) used[k] = 1; });
    var order = [['isp', 'org', 'organization', 'organisation', 'asname'], ['asn', 'as'], ['timezone', 'timezoneid', 'timezonename'], ['latitude', 'lat'], ['longitude', 'lon', 'lng'], ['postal', 'zip', 'postalcode']];
    var rows = [];
    order.forEach(function (names) { var k = pick(d, names); if (k && !used[k]) { used[k] = 1; rows.push(k); } });
    Object.keys(d).forEach(function (k) { if (!used[k]) rows.push(k); });
    if (rows.length) {
      $('<hr class="hc-divider">').appendTo($card);
      var $dl = $('<dl class="a-ip-rows">').appendTo($card);
      rows.forEach(function (k) { $('<dt>').text(label(k)).appendTo($dl); $('<dd>').text(d[k]).appendTo($dl); });
    }
  }

  function load(inst) {
    var $wrap = inst.$el, ip = $wrap.attr('data-ip'), $card = inst.$content;
    if ($wrap.data('ipLoaded')) { inst.update(); return; }
    if (cache[ip]) { $wrap.data('ipLoaded', 1); render($card, ip, cache[ip]); inst.update(); return; }
    $card.empty().append($('<div class="hc-name a-ip-title">').text(ip), $('<p class="hc-bio a-ip-msg">').text('Looking up…'));
    inst.update();
    var u = new URL(endpoint, location.href);
    u.searchParams.set('ip', ip);
    fetch(u.toString(), { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
      .then(function (r) { return r.json(); })
      .catch(function () { return { ok: false, error: 'The lookup failed. Try again in a moment.' }; })
      .then(function (r) {
        if (r.ok) { cache[ip] = r; $wrap.data('ipLoaded', 1); }
        render($card, ip, r);
        inst.update();
      });
  }

  $('[data-plugin-hovercard][data-ip]').each(function () {
    $(this).themestrapPluginHoverCard({ side: 'bottom', align: 'start', arrow: true, portaling: true, delay: [250, 200], ariaLabel: 'IP address details', onShow: function () { load(this); } });
  });
})(jQuery);