// ==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 // @downloadURL https://update.greasyfork.icu/scripts/573755/%E5%BF%AB%E9%80%9F%E9%80%89%E6%8B%A9%E4%BD%A0%E7%9A%84%E6%89%8B%E6%9C%BA%E5%8F%B7%E5%9B%BD%E5%AE%B6%E5%8C%BA%E5%8F%B7.user.js // @updateURL https://update.greasyfork.icu/scripts/573755/%E5%BF%AB%E9%80%9F%E9%80%89%E6%8B%A9%E4%BD%A0%E7%9A%84%E6%89%8B%E6%9C%BA%E5%8F%B7%E5%9B%BD%E5%AE%B6%E5%8C%BA%E5%8F%B7.meta.js // ==/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 = `
${t('none')}
`; 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 = ` ${c.flag} ${c.code} ${c.country} ${c.countryEn} `; 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(); })();