Find-Your-Country-Code

Detect country/phone code fields and quickly search/fill international dialing codes on any website.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Find-Your-Country-Code
// @name:zh-CN   快速选择你的手机号国家区号
// @namespace    https://github.com/Xxx91n/Find-Your-Country-Code
// @version      1.3.4
// @description  Detect country/phone code fields and quickly search/fill international dialing codes on any website.
// @description:zh-CN  智能识别国家/电话区号字段,提供可搜索的快速选择面板并自动填充区号。
// @author       Xxx91n
// @license      MIT
// @homepageURL  https://greasyfork.org/zh-CN/scripts/573755-find-your-country-code
// @supportURL   https://github.com/Xxx91n/Find-Your-Country-Code/issues
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// @run-at       document-idle
// ==/UserScript==

(function () {
'use strict';

// ════════════════════════════════════════════════════════
// CONFIG
// ════════════════════════════════════════════════════════
const OWN_ROOT_ID   = 'cch-root';
const WRAPPER_CLASS = 'cch-wrapper';

const SELECT_KW = [
  'country','countrycode','country_code','country-code',
  'dialcode','dial_code','dial-code','dialingcode','dialing_code',
  'callingcode','calling_code','calling-code',
  'phonecode','phone_code','phone-code',
  'areacode','area_code','area-code',
  'intlcode','intl_code','intl-code',
  'prefix','phoneprefix','phone_prefix',
  'mobile_code','mobilecode','countryphone','idd','npa',
];

const SELECT_EXCLUDE_KW = [
  'locale','language','lang','translate','translation','i18n',
  '语言','语种','本地化','翻译',
  'province','state','city','region','county','district','prefecture','locality',
  '省份','城市','区县','县区','州','地区','行政区',
];

const INPUT_KW = [
  'mobile_code','mobilecode','phone_code','phonecode',
  'dialcode','dial_code','callingcode','calling_code',
  'countrycode','country_code','intlcode','intl_code',
  'tel_code','telcode',
];

const LABEL_PHRASES = [
  'country code','dial code','calling code','phone code',
  '国家区号','区号','国际区号','电话区号','呼叫代码',
];

// ════════════════════════════════════════════════════════
// COUNTRY DATA
// ════════════════════════════════════════════════════════
const COUNTRIES = [
['+93','AF','🇦🇫','阿富汗','Afghanistan'],
['+355','AL','🇦🇱','阿尔巴尼亚','Albania'],
['+213','DZ','🇩🇿','阿尔及利亚','Algeria'],
['+1684','AS','🇦🇸','美属萨摩亚','American Samoa'],
['+376','AD','🇦🇩','安道尔','Andorra'],
['+244','AO','🇦🇴','安哥拉','Angola'],
['+1264','AI','🇦🇮','安圭拉','Anguilla'],
['+1268','AG','🇦🇬','安提瓜和巴布达','Antigua and Barbuda'],
['+54','AR','🇦🇷','阿根廷','Argentina'],
['+374','AM','🇦🇲','亚美尼亚','Armenia'],
['+297','AW','🇦🇼','阿鲁巴','Aruba'],
['+61','AU','🇦🇺','澳大利亚','Australia'],
['+43','AT','🇦🇹','奥地利','Austria'],
['+994','AZ','🇦🇿','阿塞拜疆','Azerbaijan'],
['+1242','BS','🇧🇸','巴哈马','Bahamas'],
['+973','BH','🇧🇭','巴林','Bahrain'],
['+880','BD','🇧🇩','孟加拉国','Bangladesh'],
['+1246','BB','🇧🇧','巴巴多斯','Barbados'],
['+375','BY','🇧🇾','白俄罗斯','Belarus'],
['+32','BE','🇧🇪','比利时','Belgium'],
['+501','BZ','🇧🇿','伯利兹','Belize'],
['+229','BJ','🇧🇯','贝宁','Benin'],
['+1441','BM','🇧🇲','百慕大','Bermuda'],
['+975','BT','🇧🇹','不丹','Bhutan'],
['+591','BO','🇧🇴','玻利维亚','Bolivia'],
['+387','BA','🇧🇦','波斯尼亚和黑塞哥维那','Bosnia and Herzegovina'],
['+267','BW','🇧🇼','博茨瓦纳','Botswana'],
['+55','BR','🇧🇷','巴西','Brazil'],
['+673','BN','🇧🇳','文莱','Brunei'],
['+359','BG','🇧🇬','保加利亚','Bulgaria'],
['+226','BF','🇧🇫','布基纳法索','Burkina Faso'],
['+257','BI','🇧🇮','布隆迪','Burundi'],
['+855','KH','🇰🇭','柬埔寨','Cambodia'],
['+237','CM','🇨🇲','喀麦隆','Cameroon'],
['+1','CA','🇨🇦','加拿大','Canada'],
['+238','CV','🇨🇻','佛得角','Cape Verde'],
['+1345','KY','🇰🇾','开曼群岛','Cayman Islands'],
['+236','CF','🇨🇫','中非共和国','Central African Republic'],
['+235','TD','🇹🇩','乍得','Chad'],
['+56','CL','🇨🇱','智利','Chile'],
['+86','CN','🇨🇳','中国','China'],
['+57','CO','🇨🇴','哥伦比亚','Colombia'],
['+269','KM','🇰🇲','科摩罗','Comoros'],
['+242','CG','🇨🇬','刚果共和国','Republic of the Congo'],
['+243','CD','🇨🇩','刚果民主共和国','DR Congo'],
['+682','CK','🇨🇰','库克群岛','Cook Islands'],
['+506','CR','🇨🇷','哥斯达黎加','Costa Rica'],
['+225','CI','🇨🇮','科特迪瓦',"Cote d'Ivoire"],
['+385','HR','🇭🇷','克罗地亚','Croatia'],
['+53','CU','🇨🇺','古巴','Cuba'],
['+357','CY','🇨🇾','塞浦路斯','Cyprus'],
['+420','CZ','🇨🇿','捷克','Czech Republic'],
['+45','DK','🇩🇰','丹麦','Denmark'],
['+253','DJ','🇩🇯','吉布提','Djibouti'],
['+1767','DM','🇩🇲','多米尼克','Dominica'],
['+1849','DO','🇩🇴','多米尼加共和国','Dominican Republic'],
['+593','EC','🇪🇨','厄瓜多尔','Ecuador'],
['+20','EG','🇪🇬','埃及','Egypt'],
['+503','SV','🇸🇻','萨尔瓦多','El Salvador'],
['+240','GQ','🇬🇶','赤道几内亚','Equatorial Guinea'],
['+291','ER','🇪🇷','厄立特里亚','Eritrea'],
['+372','EE','🇪🇪','爱沙尼亚','Estonia'],
['+268','SZ','🇸🇿','斯威士兰','Eswatini'],
['+251','ET','🇪🇹','埃塞俄比亚','Ethiopia'],
['+500','FK','🇫🇰','福克兰群岛','Falkland Islands'],
['+298','FO','🇫🇴','法罗群岛','Faroe Islands'],
['+679','FJ','🇫🇯','斐济','Fiji'],
['+358','FI','🇫🇮','芬兰','Finland'],
['+33','FR','🇫🇷','法国','France'],
['+594','GF','🇬🇫','法属圭亚那','French Guiana'],
['+689','PF','🇵🇫','法属波利尼西亚','French Polynesia'],
['+241','GA','🇬🇦','加蓬','Gabon'],
['+220','GM','🇬🇲','冈比亚','Gambia'],
['+995','GE','🇬🇪','格鲁吉亚','Georgia'],
['+49','DE','🇩🇪','德国','Germany'],
['+233','GH','🇬🇭','加纳','Ghana'],
['+350','GI','🇬🇮','直布罗陀','Gibraltar'],
['+30','GR','🇬🇷','希腊','Greece'],
['+299','GL','🇬🇱','格陵兰','Greenland'],
['+1473','GD','🇬🇩','格林纳达','Grenada'],
['+590','GP','🇬🇵','瓜德罗普','Guadeloupe'],
['+1671','GU','🇬🇺','关岛','Guam'],
['+502','GT','🇬🇹','危地马拉','Guatemala'],
['+224','GN','🇬🇳','几内亚','Guinea'],
['+245','GW','🇬🇼','几内亚比绍','Guinea-Bissau'],
['+592','GY','🇬🇾','圭亚那','Guyana'],
['+509','HT','🇭🇹','海地','Haiti'],
['+504','HN','🇭🇳','洪都拉斯','Honduras'],
['+852','HK','🇭🇰','香港','Hong Kong'],
['+36','HU','🇭🇺','匈牙利','Hungary'],
['+354','IS','🇮🇸','冰岛','Iceland'],
['+91','IN','🇮🇳','印度','India'],
['+62','ID','🇮🇩','印度尼西亚','Indonesia'],
['+98','IR','🇮🇷','伊朗','Iran'],
['+964','IQ','🇮🇶','伊拉克','Iraq'],
['+353','IE','🇮🇪','爱尔兰','Ireland'],
['+972','IL','🇮🇱','以色列','Israel'],
['+39','IT','🇮🇹','意大利','Italy'],
['+1876','JM','🇯🇲','牙买加','Jamaica'],
['+81','JP','🇯🇵','日本','Japan'],
['+962','JO','🇯🇴','约旦','Jordan'],
['+7','KZ','🇰🇿','哈萨克斯坦','Kazakhstan'],
['+254','KE','🇰🇪','肯尼亚','Kenya'],
['+686','KI','🇰🇮','基里巴斯','Kiribati'],
['+850','KP','🇰🇵','朝鲜','North Korea'],
['+82','KR','🇰🇷','韩国','South Korea'],
['+965','KW','🇰🇼','科威特','Kuwait'],
['+996','KG','🇰🇬','吉尔吉斯斯坦','Kyrgyzstan'],
['+856','LA','🇱🇦','老挝','Laos'],
['+371','LV','🇱🇻','拉脱维亚','Latvia'],
['+961','LB','🇱🇧','黎巴嫩','Lebanon'],
['+266','LS','🇱🇸','莱索托','Lesotho'],
['+231','LR','🇱🇷','利比里亚','Liberia'],
['+218','LY','🇱🇾','利比亚','Libya'],
['+423','LI','🇱🇮','列支敦士登','Liechtenstein'],
['+370','LT','🇱🇹','立陶宛','Lithuania'],
['+352','LU','🇱🇺','卢森堡','Luxembourg'],
['+853','MO','🇲🇴','澳门','Macau'],
['+261','MG','🇲🇬','马达加斯加','Madagascar'],
['+265','MW','🇲🇼','马拉维','Malawi'],
['+60','MY','🇲🇾','马来西亚','Malaysia'],
['+960','MV','🇲🇻','马尔代夫','Maldives'],
['+223','ML','🇲🇱','马里','Mali'],
['+356','MT','🇲🇹','马耳他','Malta'],
['+692','MH','🇲🇭','马绍尔群岛','Marshall Islands'],
['+596','MQ','🇲🇶','马提尼克','Martinique'],
['+222','MR','🇲🇷','毛里塔尼亚','Mauritania'],
['+230','MU','🇲🇺','毛里求斯','Mauritius'],
['+52','MX','🇲🇽','墨西哥','Mexico'],
['+691','FM','🇫🇲','密克罗尼西亚','Micronesia'],
['+373','MD','🇲🇩','摩尔多瓦','Moldova'],
['+377','MC','🇲🇨','摩纳哥','Monaco'],
['+976','MN','🇲🇳','蒙古','Mongolia'],
['+382','ME','🇲🇪','黑山','Montenegro'],
['+1664','MS','🇲🇸','蒙特塞拉特','Montserrat'],
['+212','MA','🇲🇦','摩洛哥','Morocco'],
['+258','MZ','🇲🇿','莫桑比克','Mozambique'],
['+95','MM','🇲🇲','缅甸','Myanmar'],
['+264','NA','🇳🇦','纳米比亚','Namibia'],
['+674','NR','🇳🇷','瑙鲁','Nauru'],
['+977','NP','🇳🇵','尼泊尔','Nepal'],
['+31','NL','🇳🇱','荷兰','Netherlands'],
['+687','NC','🇳🇨','新喀里多尼亚','New Caledonia'],
['+64','NZ','🇳🇿','新西兰','New Zealand'],
['+505','NI','🇳🇮','尼加拉瓜','Nicaragua'],
['+227','NE','🇳🇪','尼日尔','Niger'],
['+234','NG','🇳🇬','尼日利亚','Nigeria'],
['+683','NU','🇳🇺','纽埃','Niue'],
['+1670','MP','🇲🇵','北马里亚纳群岛','Northern Mariana Islands'],
['+47','NO','🇳🇴','挪威','Norway'],
['+968','OM','🇴🇲','阿曼','Oman'],
['+92','PK','🇵🇰','巴基斯坦','Pakistan'],
['+680','PW','🇵🇼','帕劳','Palau'],
['+970','PS','🇵🇸','巴勒斯坦','Palestine'],
['+507','PA','🇵🇦','巴拿马','Panama'],
['+675','PG','🇵🇬','巴布亚新几内亚','Papua New Guinea'],
['+595','PY','🇵🇾','巴拉圭','Paraguay'],
['+51','PE','🇵🇪','秘鲁','Peru'],
['+63','PH','🇵🇭','菲律宾','Philippines'],
['+48','PL','🇵🇱','波兰','Poland'],
['+351','PT','🇵🇹','葡萄牙','Portugal'],
['+1787','PR','🇵🇷','波多黎各','Puerto Rico'],
['+974','QA','🇶🇦','卡塔尔','Qatar'],
['+262','RE','🇷🇪','留尼汪','Reunion'],
['+40','RO','🇷🇴','罗马尼亚','Romania'],
['+7','RU','🇷🇺','俄罗斯','Russia'],
['+250','RW','🇷🇼','卢旺达','Rwanda'],
['+1869','KN','🇰🇳','圣基茨和尼维斯','Saint Kitts and Nevis'],
['+1758','LC','🇱🇨','圣卢西亚','Saint Lucia'],
['+1784','VC','🇻🇨','圣文森特和格林纳丁斯','Saint Vincent and the Grenadines'],
['+685','WS','🇼🇸','萨摩亚','Samoa'],
['+378','SM','🇸🇲','圣马力诺','San Marino'],
['+239','ST','🇸🇹','圣多美和普林西比','Sao Tome and Principe'],
['+966','SA','🇸🇦','沙特阿拉伯','Saudi Arabia'],
['+221','SN','🇸🇳','塞内加尔','Senegal'],
['+381','RS','🇷🇸','塞尔维亚','Serbia'],
['+248','SC','🇸🇨','塞舌尔','Seychelles'],
['+232','SL','🇸🇱','塞拉利昂','Sierra Leone'],
['+65','SG','🇸🇬','新加坡','Singapore'],
['+1721','SX','🇸🇽','圣马丁岛','Sint Maarten'],
['+421','SK','🇸🇰','斯洛伐克','Slovakia'],
['+386','SI','🇸🇮','斯洛文尼亚','Slovenia'],
['+677','SB','🇸🇧','所罗门群岛','Solomon Islands'],
['+252','SO','🇸🇴','索马里','Somalia'],
['+27','ZA','🇿🇦','南非','South Africa'],
['+211','SS','🇸🇸','南苏丹','South Sudan'],
['+34','ES','🇪🇸','西班牙','Spain'],
['+94','LK','🇱🇰','斯里兰卡','Sri Lanka'],
['+249','SD','🇸🇩','苏丹','Sudan'],
['+597','SR','🇸🇷','苏里南','Suriname'],
['+46','SE','🇸🇪','瑞典','Sweden'],
['+41','CH','🇨🇭','瑞士','Switzerland'],
['+963','SY','🇸🇾','叙利亚','Syria'],
['+886','TW','🇹🇼','台湾','Taiwan'],
['+992','TJ','🇹🇯','塔吉克斯坦','Tajikistan'],
['+255','TZ','🇹🇿','坦桑尼亚','Tanzania'],
['+66','TH','🇹🇭','泰国','Thailand'],
['+670','TL','🇹🇱','东帝汶','Timor-Leste'],
['+228','TG','🇹🇬','多哥','Togo'],
['+676','TO','🇹🇴','汤加','Tonga'],
['+1868','TT','🇹🇹','特立尼达和多巴哥','Trinidad and Tobago'],
['+216','TN','🇹🇳','突尼斯','Tunisia'],
['+90','TR','🇹🇷','土耳其','Turkey'],
['+993','TM','🇹🇲','土库曼斯坦','Turkmenistan'],
['+1649','TC','🇹🇨','特克斯和凯科斯群岛','Turks and Caicos Islands'],
['+688','TV','🇹🇻','图瓦卢','Tuvalu'],
['+256','UG','🇺🇬','乌干达','Uganda'],
['+380','UA','🇺🇦','乌克兰','Ukraine'],
['+971','AE','🇦🇪','阿联酋','United Arab Emirates'],
['+44','GB','🇬🇧','英国','United Kingdom'],
['+1','US','🇺🇸','美国','United States'],
['+598','UY','🇺🇾','乌拉圭','Uruguay'],
['+998','UZ','🇺🇿','乌兹别克斯坦','Uzbekistan'],
['+678','VU','🇻🇺','瓦努阿图','Vanuatu'],
['+58','VE','🇻🇪','委内瑞拉','Venezuela'],
['+84','VN','🇻🇳','越南','Vietnam'],
['+967','YE','🇾🇪','也门','Yemen'],
['+260','ZM','🇿🇲','赞比亚','Zambia'],
['+263','ZW','🇿🇼','津巴布韦','Zimbabwe'],
['+389','MK','🇲🇰','北马其顿','North Macedonia'],
['+1340','VI','🇻🇮','美属维尔京群岛','U.S. Virgin Islands'],
['+1284','VG','🇻🇬','英属维尔京群岛','British Virgin Islands'],
['+246','IO','🇮🇴','英属印度洋领地','British Indian Ocean Territory'],
].map(([code, iso, flag, zh, en]) => ({ code, iso, flag, country: zh, countryEn: en }));

const ISO2_MAP = Object.fromEntries(COUNTRIES.map(c => [c.iso.toLowerCase(), c]));

// ════════════════════════════════════════════════════════
// I18N
// ════════════════════════════════════════════════════════
const LANG = (navigator.language || 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en';
const MSG = {
  zh: { search:'搜索国家或区号…', favs:'收藏', all:'全部', none:'无结果',
        ok:'已填入', copied:'已复制', needTarget:'请先点击目标字段',
        addFav:'添加收藏', rmFav:'取消收藏' },
  en: { search:'Search country or code…', favs:'Favorites', all:'All', none:'No results',
        ok:'Filled', copied:'Copied', needTarget:'Click target field first',
        addFav:'Add to favorites', rmFav:'Remove from favorites' },
};
const t = k => (MSG[LANG] || MSG.en)[k] || k;

// ════════════════════════════════════════════════════════
// STORAGE
// ════════════════════════════════════════════════════════
const Store = {
  _k: 'cch_v33',
  _c: null,
  _bc: null,
  _gmListener: null,
  _subs: new Set(),
  _notifyQueued: false,
  _sid: Math.random().toString(36).slice(2),
  init() {
    if (!this._bc && typeof BroadcastChannel !== 'undefined') {
      try {
        this._bc = new BroadcastChannel('cch-favs-sync-v1');
        this._bc.addEventListener('message', e => {
          const msg = e && e.data;
          if (!msg || msg.sid === this._sid || msg.type !== 'favs-sync') return;
          if (!Array.isArray(msg.favs)) return;
          const d = this._load();
          d.favs = msg.favs;
          this._save(d, true);
          this._notify();
        });
      } catch {}
    }
    if (!this._gmListener && typeof GM_addValueChangeListener === 'function') {
      try {
        this._gmListener = GM_addValueChangeListener(this._k, (_k, _o, n, remote) => {
          if (!remote) return;
          try {
            const parsed = JSON.parse(n || '{}');
            if (!Array.isArray(parsed.favs)) parsed.favs = [];
            this._c = parsed;
            this._notify();
          } catch {}
        });
      } catch {}
    }
  },
  _notify() {
    if (this._notifyQueued) return;
    this._notifyQueued = true;
    setTimeout(() => {
      this._notifyQueued = false;
      this._subs.forEach(fn => {
        try { fn(); } catch {}
      });
    }, 0);
  },
  subscribe(fn) {
    if (typeof fn !== 'function') return () => {};
    this._subs.add(fn);
    return () => this._subs.delete(fn);
  },
  _broadcastFavs(favs) {
    if (!this._bc) return;
    try {
      this._bc.postMessage({ type: 'favs-sync', sid: this._sid, favs });
    } catch {}
  },
  _load() {
    if (this._c) return this._c;
    try { this._c = JSON.parse(GM_getValue(this._k, '{}')); } catch { this._c = {}; }
    if (!Array.isArray(this._c.favs)) this._c.favs = [];
    return this._c;
  },
  _save(d, silent) {
    this._c = d;
    GM_setValue(this._k, JSON.stringify(d));
    if (!silent) this._broadcastFavs(d.favs);
  },
  isFav(code, iso) { return this._load().favs.some(f => f.code === code && f.iso === iso); },
  addFav(c) {
    const d = this._load();
    if (!this.isFav(c.code, c.iso)) {
      d.favs.push(c);
      this._save(d);
      this._notify();
    }
  },
  rmFav(code, iso) {
    const d = this._load();
    d.favs = d.favs.filter(f => !(f.code === code && f.iso === iso));
    this._save(d);
    this._notify();
  },
  getFavs() { return this._load().favs; },
};

// ════════════════════════════════════════════════════════
// DETECTION
// ════════════════════════════════════════════════════════
const Detect = {
  _done: new WeakSet(),

  _own(el) {
    return !!el.closest('#' + OWN_ROOT_ID) ||
           !!el.closest('.' + WRAPPER_CLASS) ||
           el.id === 'cch-search';
  },

  _kw(str, list) {
    if (!str) return false;
    const s = str.toLowerCase().replace(/[-_\s]/g, '');
    return list.some(k => s.includes(k.replace(/[-_\s]/g, '')));
  },

  _label(el) {
    if (el.id) {
      const l = document.querySelector('label[for="' + el.id + '"]');
      if (l) return l.textContent;
    }
    const lp = el.closest('label');
    if (lp) return lp.textContent;
    const lid = el.getAttribute('aria-labelledby');
    if (lid) { const l = document.getElementById(lid); if (l) return l.textContent; }
    return '';
  },

  _isIti(el) {
    if (el.tagName !== 'INPUT') return false;
    if (el.closest('.iti') || el.closest('.intl-tel-input')) return true;
    if (el.dataset && el.dataset.intlTelInputId) return true;
    if (typeof window.jQuery !== 'undefined') {
      try {
        const pluginData = window.jQuery(el).data('plugin_intlTelInput') ||
                           window.jQuery(el).data('intlTelInput');
        if (pluginData) return true;
      } catch {}
    }
    return false;
  },

  _isSelect(el) {
    if (el.tagName !== 'SELECT') return false;
    const opts = Array.from(el.options).filter(o => (o.value || '').trim());
    if (opts.length < 2) return false;

    const attrStr = [el.name, el.id, el.className,
      el.getAttribute('data-name'), el.getAttribute('aria-label'), el.title]
      .filter(Boolean).join(' ');

    const lbl = this._label(el).toLowerCase();
    const parentHint = el.parentElement
      ? `${el.parentElement.className || ''} ${(el.parentElement.getAttribute('aria-label') || '')}`
      : '';
    const detectHint = `${attrStr} ${lbl} ${parentHint}`.toLowerCase();
    if (this._kw(detectHint, SELECT_EXCLUDE_KW)) return false;

    const hasLabelPhrase = LABEL_PHRASES.some(p => lbl.includes(p));

    const hasAttrKw = this._kw(attrStr, SELECT_KW) ||
      this._kw(this._label(el), SELECT_KW) ||
      (el.parentElement && this._kw(
        el.parentElement.className + ' ' + (el.parentElement.getAttribute('aria-label') || ''),
        SELECT_KW
      ));

    const hitCode = opts.filter(o => {
      const v = (o.value || '').trim();
      const txt = (o.text || '').trim();
      return /^\+\d{1,4}$/.test(v) || /^00\d{1,4}$/.test(v) || /^\d{1,4}$/.test(v) || /\(\+\d{1,4}\)/.test(txt);
    });
    const hitIso = opts.filter(o => {
      const v = (o.value || '').trim().toLowerCase();
      return /^[a-z]{2}$/.test(v) && !!ISO2_MAP[v];
    });
    const hitPlusLike = opts.filter(o => {
      const v = (o.value || '').trim();
      const txt = (o.text || '').trim();
      return /^\+\d{1,4}$/.test(v) || /^00\d{1,4}$/.test(v) || /\(\+\d{1,4}\)/.test(txt);
    });

    if (hasAttrKw || hasLabelPhrase) {
      return hitCode.length >= 2 || hitIso.length >= 2;
    }

    if (hitPlusLike.length >= 2 && hitPlusLike.length / opts.length >= 0.4) return true;

    const allText = opts.map(o => (o.text || '').toLowerCase()).join(' ');
    if ((hitCode.length >= 2 || hitIso.length >= 2) && /(china|japan|united states|usa|america|germany|france|india|canada|australia|united kingdom|uk)/.test(allText)) {
      return true;
    }

    return false;
  },

  _isInput(el) {
    if (el.tagName !== 'INPUT') return false;
    if (this._isIti(el)) return false;
    const type = (el.type || 'text').toLowerCase();
    if (!['text','tel',''].includes(type)) return false;

    const attrStr = [el.name, el.id, el.className,
      el.getAttribute('placeholder'), el.getAttribute('aria-label'),
      el.getAttribute('data-name'), el.title].filter(Boolean).join(' ');
    if (this._kw(attrStr, INPUT_KW)) return true;

    // FIX v3.3.3: label 含"呼叫代码"等短语即命中
    const lbl = this._label(el).toLowerCase();
    if (LABEL_PHRASES.some(p => lbl.includes(p))) return true;

    return false;
  },

  scan(root) {
    root = root || document.body;
    root.querySelectorAll('select').forEach(el => this._process(el));
    ['.iti input', '.intl-tel-input input'].forEach(sel => {
      root.querySelectorAll(sel).forEach(el => this._process(el));
    });
    root.querySelectorAll('input[type="tel"],input[type="text"],input:not([type])').forEach(el => this._process(el));
  },

  _process(el) {
    if (this._done.has(el)) return;
    if (this._own(el)) return;
    if (el.disabled || el.readOnly) return;

    let kind = null;
    if (this._isIti(el))         kind = 'iti';
    else if (this._isSelect(el)) kind = 'select';
    else if (this._isInput(el))  kind = 'input';

    if (kind) {
      this._done.add(el);
      UI.attach(el, kind);
    }
  },
};

// ════════════════════════════════════════════════════════
// FILL
// ════════════════════════════════════════════════════════
const Fill = {
  _dispatch(el) {
    try {
      const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
      if (setter && el.tagName === 'INPUT') setter.set.call(el, el.value);
    } catch {}
    ['input','change','blur'].forEach(t => el.dispatchEvent(new Event(t, { bubbles: true })));
  },

  fillIti(el, country) {
    const iso = country.iso.toLowerCase();

    try {
      const globalIti = window.intlTelInput || window.intlTelInputGlobals;
      if (globalIti && typeof globalIti.getInstance === 'function') {
        const inst = globalIti.getInstance(el);
        if (inst && typeof inst.setCountry === 'function') { inst.setCountry(iso); return true; }
      }
    } catch {}

    try {
      if (el.iti && typeof el.iti.setCountry === 'function') { el.iti.setCountry(iso); return true; }
    } catch {}

    try {
      const id = el.dataset.intlTelInputId;
      if (id) {
        const globalIti = window.intlTelInput || window.intlTelInputGlobals;
        const inst = globalIti && globalIti.instances && globalIti.instances[id];
        if (inst && typeof inst.setCountry === 'function') { inst.setCountry(iso); return true; }
      }
    } catch {}

    try {
      const $ = window.jQuery || window.$;
      if ($) {
        if (typeof $(el).intlTelInput === 'function') {
          try { $(el).intlTelInput('setCountry', iso); return true; } catch {}
          try { $(el).intlTelInput('setCountry', iso.toUpperCase()); return true; } catch {}
        }
        const inst = $(el).data('plugin_intlTelInput') || $(el).data('intlTelInput');
        if (inst && typeof inst.setCountry === 'function') { inst.setCountry(iso); return true; }
      }
    } catch {}

    try {
      const wrapper = el.closest('.iti') || el.closest('.intl-tel-input');
      if (wrapper) {
        const btn = wrapper.querySelector('.iti__selected-country, .iti__flag-container, .selected-flag');
        if (btn) btn.click();
        const clickItem = () => {
          const item = wrapper.querySelector(
            `[data-country-code="${iso}"], [data-dial-code="${country.code.replace('+','')}"]`
          ) || document.querySelector(
            `.iti__country[data-country-code="${iso}"], .country[data-country-code="${iso}"]`
          );
          if (item) { item.click(); return true; }
          return false;
        };
        if (clickItem()) return true;
        setTimeout(() => { if (!clickItem()) { el.value = country.code; this._dispatch(el); } }, 120);
        return true;
      }
    } catch {}

    el.value = country.code;
    this._dispatch(el);
    return true;
  },

  fillSelect(el, country) {
    const opts   = Array.from(el.options);
    const digits = country.code.replace(/\D/g, '');
    const iso    = country.iso.toLowerCase();

    let m = opts.find(o => {
      const v = o.value.trim();
      return v === country.code || v === digits || v === '00' + digits || v.toLowerCase() === iso;
    });
    if (!m) m = opts.find(o =>
      (o.getAttribute('data-country-code') || '').toLowerCase() === iso ||
      (o.getAttribute('data-iso') || '').toLowerCase() === iso
    );
    if (!m) m = opts.find(o =>
      o.text.includes(country.code) ||
      o.text.toLowerCase().includes(country.countryEn.toLowerCase()) ||
      o.text.includes(country.country)
    );
    if (m) { el.value = m.value; this._dispatch(el); return true; }
    return false;
  },

  fillInput(el, country) {
    const ph  = (el.placeholder || '').trim();
    let fmt = 'plus';
    if (/^00\d/.test(ph))  fmt = 'double0';
    else if (/^\d/.test(ph)) fmt = 'digits';
    const digits    = country.code.replace(/\D/g, '');
    const formatted = fmt === 'double0' ? '00' + digits : fmt === 'digits' ? digits : country.code;
    const rest = (el.value || '').replace(/^(\+|00)?\d{1,4}\s*/, '').trim();
    el.value = formatted + (rest ? ' ' + rest : '');
    this._dispatch(el);
    return true;
  },

  run(el, kind, country) {
    let ok = false;
    if (kind === 'iti')         ok = this.fillIti(el, country);
    else if (kind === 'select') ok = this.fillSelect(el, country);
    else                        ok = this.fillInput(el, country);
    if (ok) UI.toast(t('ok') + ': ' + country.flag + ' ' + country.code);
    else {
      try { navigator.clipboard.writeText(country.code); } catch {}
      UI.toast(t('copied') + ': ' + country.code);
    }
  },
};

// ════════════════════════════════════════════════════════
// UI
// ════════════════════════════════════════════════════════
const UI = {
  _root: null, _popup: null, _target: null, _kind: null,
  _toastTimer: null, _closeHandler: null, _anchor: null,
  _viewportHandler: null, _rafPending: false,

  css() {
    if (document.getElementById('cch-style')) return;
    const s = document.createElement('style');
    s.id = 'cch-style';
    s.textContent = `
.${WRAPPER_CLASS}{position:relative;display:inline-block;width:100%}
.cch-btn{position:absolute;top:-12px;right:-12px;transform:none;
width:24px;height:24px;border-radius:50%;background:rgba(255,255,255,.96);
border:1px solid rgba(15,23,42,.16);cursor:pointer;display:flex;
align-items:center;justify-content:center;font-size:13px;z-index:10000;
box-shadow:0 8px 18px rgba(2,8,23,.18);transition:transform .12s ease,box-shadow .12s ease;
user-select:none;line-height:1;padding:0}
.cch-btn:hover{transform:scale(1.06);box-shadow:0 10px 20px rgba(2,8,23,.22)}
#${OWN_ROOT_ID}{z-index:2147483647;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
#cch-pop{--cch-surface:rgba(255,255,255,.78);--cch-surface-strong:rgba(255,255,255,.92);
--cch-border:rgba(15,23,42,.12);--cch-text:#0f172a;--cch-subtext:#475569;--cch-accent:#0f766e;
background:var(--cch-surface);border:1px solid var(--cch-border);border-radius:16px;
box-shadow:0 18px 48px rgba(2,8,23,.16);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
width:320px;max-height:min(78vh,460px);display:flex;flex-direction:column;overflow:hidden;
animation:cchIn .12s ease;z-index:2147483647}
@keyframes cchIn{from{opacity:0;transform:translateY(4px) scale(.985)}to{opacity:1;transform:translateY(0) scale(1)}}
#cch-sw{padding:12px 12px 10px;border-bottom:1px solid rgba(15,23,42,.08);background:var(--cch-surface-strong)}
#cch-si{width:100%;box-sizing:border-box;padding:9px 12px;border:1px solid rgba(15,23,42,.12);
background:rgba(255,255,255,.88);color:var(--cch-text);border-radius:10px;font-size:13px;outline:none}
#cch-si:focus{border-color:rgba(15,118,110,.45);box-shadow:0 0 0 3px rgba(15,118,110,.12)}
.cch-body{display:flex;flex-direction:column;gap:8px;padding:8px 8px 10px;overflow:hidden;flex:1;min-height:0}
.cch-sec{border:1px solid rgba(15,23,42,.08);background:rgba(255,255,255,.66);border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
.cch-sec-favs{flex:0 0 auto}
.cch-sec-all{flex:1 1 auto;min-height:120px}
.cch-sec-hd{padding:7px 10px;font-size:11px;font-weight:700;letter-spacing:.02em;color:var(--cch-subtext);
text-transform:uppercase;background:rgba(255,255,255,.52);border-bottom:1px solid rgba(15,23,42,.06)}
.cch-list{display:flex;flex-direction:column}
.cch-sec-favs .cch-list{max-height:132px;overflow-y:auto}
.cch-sec-all .cch-list{flex:1 1 auto;min-height:0;overflow-y:auto}
.cch-row{display:flex;align-items:center;padding:8px 10px;cursor:pointer;
gap:8px;border-bottom:1px solid rgba(15,23,42,.06);transition:background .12s ease,transform .08s ease}
.cch-row:last-child{border-bottom:none}
.cch-row:hover{background:rgba(15,118,110,.08)}
.cch-fl{font-size:17px;flex-shrink:0;width:24px;text-align:center}
.cch-cd{font-weight:600;font-size:13px;color:var(--cch-accent);min-width:44px}
.cch-nm{font-size:12px;color:var(--cch-subtext);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cch-fav{background:none;border:none;cursor:pointer;font-size:15px;color:#b8c1cc;
padding:2px 4px;border-radius:4px;flex-shrink:0;transition:color .1s}
.cch-fav.on,.cch-fav:hover{color:#f59e0b}
.cch-empty{padding:14px 10px;text-align:center;color:#8a95a3;font-size:12px}
#cch-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);
background:#01696f;color:#fff;padding:8px 20px;border-radius:20px;
font-size:13px;z-index:2147483647;pointer-events:none;opacity:0;
transition:opacity .2s;white-space:nowrap}
#cch-toast.on{opacity:1}`;
    document.head.appendChild(s);
  },

  toast(msg) {
    let el = document.getElementById('cch-toast');
    if (!el) { el = document.createElement('div'); el.id = 'cch-toast'; document.body.appendChild(el); }
    el.textContent = msg;
    el.classList.add('on');
    clearTimeout(this._toastTimer);
    this._toastTimer = setTimeout(() => el.classList.remove('on'), 2000);
  },

  attach(el, kind) {
    if (el.closest('.' + WRAPPER_CLASS)) return;

    const wrap = document.createElement('div');
    wrap.className = WRAPPER_CLASS;
    const cs = getComputedStyle(el);
    wrap.style.display = cs.display === 'inline' ? 'inline-block' : cs.display;

    // 仅宽度可测时设显式宽,否则继承父容器
    if (el.offsetWidth > 0) {
      wrap.style.width = el.offsetWidth + 'px';
    }

    // 不论宽度是否为 0,都立即插入
    el.parentNode.insertBefore(wrap, el);
    wrap.appendChild(el);

    const btn = document.createElement('button');
    btn.className = 'cch-btn';
    btn.type = 'button';
    btn.title = 'Country Code Helper';
    btn.setAttribute('aria-label', 'Country Code Helper');
    btn.textContent = '🌐';
    btn.addEventListener('click', e => {
      e.stopPropagation();
      e.preventDefault();
      this.open(el, kind, btn);
    });
    wrap.appendChild(btn);
  },

  open(target, kind, anchor) {
    if (this._popup && this._anchor === anchor) {
      this._closePopup();
      return;
    }

    this._target = target;
    this._kind   = kind;
    if (!this._root) {
      this._root = document.createElement('div');
      this._root.id = OWN_ROOT_ID;
      document.body.appendChild(this._root);
    }
    this._closePopup();
    this._anchor = anchor;

    const pop = document.createElement('div');
    pop.id = 'cch-pop';
    this._popup = pop;

    const sw = document.createElement('div'); sw.id = 'cch-sw';
    const si = document.createElement('input');
    si.type = 'text'; si.id = 'cch-si';
    si.placeholder = t('search');
    si.setAttribute('autocomplete', 'off');
    sw.appendChild(si); pop.appendChild(sw);

    const body = document.createElement('div');
    body.className = 'cch-body';

    const favSec = document.createElement('section');
    favSec.className = 'cch-sec cch-sec-favs';
    const favHd = document.createElement('div');
    favHd.className = 'cch-sec-hd';
    favHd.textContent = t('favs');
    const favList = document.createElement('div');
    favList.className = 'cch-list';
    favList.setAttribute('data-sec', 'favs');
    favSec.appendChild(favHd);
    favSec.appendChild(favList);

    const allSec = document.createElement('section');
    allSec.className = 'cch-sec cch-sec-all';
    const allHd = document.createElement('div');
    allHd.className = 'cch-sec-hd';
    allHd.textContent = t('all');
    const allList = document.createElement('div');
    allList.className = 'cch-list';
    allList.setAttribute('data-sec', 'all');
    allSec.appendChild(allHd);
    allSec.appendChild(allList);

    body.appendChild(favSec);
    body.appendChild(allSec);
    pop.appendChild(body);

    document.body.appendChild(pop);
    this._pos(pop, anchor);
    this._bindViewportTracking();
    this._bindPopupEvents(pop);
    this._render('');

    const close = e => {
      if (!pop.contains(e.target) && e.target !== anchor) {
        this._closePopup();
      }
    };
    this._closeHandler = close;
    setTimeout(() => document.addEventListener('mousedown', close), 0);
    requestAnimationFrame(() => {
      if (this._popup === pop) si.focus();
    });
  },

  _closePopup() {
    if (this._popup) {
      this._popup.remove();
      this._popup = null;
    }
    if (this._closeHandler) {
      document.removeEventListener('mousedown', this._closeHandler);
      this._closeHandler = null;
    }
    if (this._viewportHandler) {
      window.removeEventListener('scroll', this._viewportHandler, true);
      window.removeEventListener('resize', this._viewportHandler);
      this._viewportHandler = null;
    }
    this._rafPending = false;
    this._anchor = null;
  },

  _bindViewportTracking() {
    if (this._viewportHandler) return;
    this._viewportHandler = () => {
      if (this._rafPending) return;
      this._rafPending = true;
      requestAnimationFrame(() => {
        this._rafPending = false;
        if (!this._popup || !this._anchor) return;
        this._pos(this._popup, this._anchor);
      });
    };
    window.addEventListener('scroll', this._viewportHandler, true);
    window.addEventListener('resize', this._viewportHandler);
  },

  _bindPopupEvents(pop) {
    const si = pop.querySelector('#cch-si');
    if (si) {
      si.addEventListener('input', () => {
        if (this._popup !== pop) return;
        this._render(si.value);
      });
    }
    pop.addEventListener('click', e => {
      if (this._popup !== pop) return;
      const favBtn = e.target.closest('.cch-fav');
      if (favBtn) {
        e.stopPropagation();
        const iso = (favBtn.dataset.iso || '').toLowerCase();
        const entry = ISO2_MAP[iso];
        if (!entry) return;
        if (Store.isFav(entry.code, entry.iso)) Store.rmFav(entry.code, entry.iso);
        else Store.addFav(entry);
        return;
      }
      const row = e.target.closest('.cch-row');
      if (!row) return;
      const iso = (row.dataset.iso || '').toLowerCase();
      const c = ISO2_MAP[iso];
      if (!c) return;
      Fill.run(this._target, this._kind, c);
      this._closePopup();
    });
  },

  _pos(pop, anchor) {
    const r = anchor.getBoundingClientRect();
    const pw = pop.offsetWidth || 320;
    const ph = pop.offsetHeight || 440;
    const m = 8;
    let l = r.left;
    let tp = r.bottom + 8;
    if (l + pw > innerWidth - m) l = Math.max(m, innerWidth - pw - m);
    if (tp + ph > innerHeight - m) tp = Math.max(m, r.top - ph - 8);
    pop.style.cssText += `;left:${l}px;top:${tp}px;position:fixed`;
  },

  _match(c, query) {
    return c.country.includes(query) ||
      c.countryEn.toLowerCase().includes(query) ||
      c.code.includes(query) ||
      c.iso.toLowerCase().includes(query);
  },

  _renderRows(list, data) {
    list.innerHTML = '';
    if (!data.length) {
      list.innerHTML = `<div class="cch-empty">${t('none')}</div>`;
      return;
    }
    const frag = document.createDocumentFragment();
    data.forEach(c => {
      const row = document.createElement('div'); row.className = 'cch-row';
      row.dataset.iso = c.iso;
      const fav = Store.isFav(c.code, c.iso);
      row.innerHTML = `
<span class="cch-fl">${c.flag}</span>
<span class="cch-cd">${c.code}</span>
<span class="cch-nm">${c.country} ${c.countryEn}</span>
<button type="button" class="cch-fav${fav ? ' on' : ''}" data-code="${c.code}" data-iso="${c.iso}" title="${fav ? t('rmFav') : t('addFav')}">${fav ? '★' : '☆'}</button>`;
      frag.appendChild(row);
    });
    list.appendChild(frag);
  },

  _render(q) {
    if (!this._popup) return;
    const favList = this._popup.querySelector('.cch-list[data-sec="favs"]');
    const allList = this._popup.querySelector('.cch-list[data-sec="all"]');
    if (!favList || !allList) return;
    const query = q.toLowerCase().trim();
    let favData = Store.getFavs();
    let allData = COUNTRIES;
    if (query) {
      favData = favData.filter(c => this._match(c, query));
      allData = allData.filter(c => this._match(c, query));
    }
    this._renderRows(favList, favData);
    this._renderRows(allList, allData);
  },
};

// ════════════════════════════════════════════════════════
// OBSERVER & INIT
// ════════════════════════════════════════════════════════
function observe() {
  let tid = null;
  new MutationObserver(() => {
    clearTimeout(tid);
    tid = setTimeout(() => Detect.scan(document.body), 350);
  }).observe(document.body, { childList: true, subtree: true });
}

function init() {
  Store.init();
  UI.css();
  Store.subscribe(() => {
    if (!UI._popup) return;
    const q = UI._popup.querySelector('#cch-si')?.value || '';
    UI._render(q);
  });
  Detect.scan(document.body);
  let n = 0;
  const poll = setInterval(() => {
    Detect.scan(document.body);
    if (++n >= 8) clearInterval(poll);
  }, 500);
  observe();
}

document.readyState === 'loading'
  ? document.addEventListener('DOMContentLoaded', init)
  : init();

})();