PageSourceSearch

https://cc.guru/js/ccguru-contact-fields.js?v=20260919-2

js cc.guru collected 2026-09-25 06:06:45 UTC 25,477 bytes, 310 lines download raw bytes

1(function () {
2    'use strict';
3    if (window.ccgContactFieldsReady) return;
4    window.ccgContactFieldsReady = true;
5    const lib = window.libphonenumber;
6    const root = '/data/ccguru-contact/';
7    const ready = new WeakSet(), cache = new Map();
8    let countries = [], sequence = 0;
9    const countryPriority = ['RU','BY','KZ','KG','TJ','UZ'];
10    const rankCountry = value => {const i=countryPriority.indexOf(countryCode(value));return i<0?99:i;};
11    const phoneNames = new Set(['phone','mobile','fax','cphone','cc_phone','access_phone','phone_access']);
12    const cityNames = new Set(['city','ccity']);
13    const norm = value => String(value || '').trim().toLocaleLowerCase('ru').replace(/ё/g, 'е');
14    function load(path) {
15        if (!cache.has(path)) cache.set(path, fetch(root + path + '?v=20260919-1', {credentials:'omit'})
16            .then(r => { if (!r.ok) throw new Error('Dictionary unavailable'); return r.json(); })
17            .catch(e => {cache.delete(path); throw e;}));
18        return cache.get(path);
19    }
20    function key(input) {
21        const name = input.name || '';
22        const m = name.match(/^(.*)\[([^\]]+)\]$/);
23        return m ? {prefix:m[1], field:m[2]} : {prefix:'', field:name || input.id};
24    }
25    function sibling(input, field) {
26        const k = key(input), scope = input.form || input.closest('[role="dialog"]') || document;
27        return Array.from(scope.querySelectorAll('input[name],select[name]')).find(el => {
28            const other = key(el); return other.prefix === k.prefix && other.field === field;
29        });
30    }
31    function option(select, value, label) {const o = new Option(label, value); select.add(o); return o;}
32    function notify(input) {input.dispatchEvent(new Event('input',{bubbles:true})); input.dispatchEvent(new Event('change',{bubbles:true}));}
33    function label(text, control) {
34        control.id = control.id || 'ccg-contact-' + (++sequence);
35        const l = document.createElement('label');l.className='ccg-contact-label';l.htmlFor=control.id;l.textContent=text;return l;
36    }
37    function countryCode(value) {
38        const match = countries.find(c => c.iso === String(value).toUpperCase() || norm(c.name) === norm(value) || norm(c.english) === norm(value));
39        return match ? match.iso : '';
40    }
41    function selectCountry(input) {
42        if (ready.has(input) || input.readOnly || input.disabled) return input;
43        const current = input.value, isSelect = input.tagName === 'SELECT';
44        const isoValues = isSelect ? Array.from(input.options).filter(o=>o.value).every(o=>/^[A-Z]{2}$/.test(o.value)) : /^[A-Z]{2}$/.test(current) || key(input).prefix==='User';
45        const select = isSelect ? input : document.createElement('select');
46        if (!isSelect) {
47            ['id','name','class','required','disabled','aria-describedby'].forEach(a=>{if(input.hasAttribute(a))select.setAttribute(a,input.getAttribute(a));});
48            input.replaceWith(select);
49        }
50        const existing = new Set(Array.from(select.options).map(o=>o.value));
51        if (!existing.has('')) option(select,'','Выберите страну');
52        countries.forEach(c=>{const value=isoValues?c.iso:c.name;if(!existing.has(value))option(select,value,c.name);});
53        if (current && !Array.from(select.options).some(o=>o.value===current)) option(select,current,current);
54        Array.from(select.options).sort((a,b)=>{if(!a.value)return -1;if(!b.value)return 1;return rankCountry(a.value)-rankCountry(b.value)||a.text.localeCompare(b.text,'ru');}).forEach(o=>select.append(o));
55        select.value=current;Array.from(select.options).forEach(o=>o.defaultSelected=o.value===current); select.classList.add('ccg-country-select');select.autocomplete='country';ready.add(select);return select;
56    }
57    function phone(input) {
58        if (!lib || ready.has(input) || input.readOnly || input.disabled) return;
59        ready.add(input);
60        const area = key(input).field === 'phone' ? sibling(input,'phone_code_city') : null;
61        const prefix = area ? sibling(input,'phone_code_country') : null;
62        if (prefix) {splitPhone(input,prefix,area);return;}
63        input.type='tel';input.inputMode='tel';input.autocomplete='tel';input.classList.add('ccg-phone-number');
64        input.removeAttribute('pattern');input.maxLength=Math.max(40, input.maxLength);
65        const wrap=document.createElement('div');wrap.className='ccg-phone';input.before(wrap);
66        const select=document.createElement('select');select.className='ccg-phone-country';select.setAttribute('aria-label','Страна и телефонный код');
67        phoneCountries(select);wrap.append(select,input);
68        const geo=sibling(input,'country');let iso=countryCode(geo && geo.value) || 'RU';
69        const initial=lib.parsePhoneNumberFromString(input.value,iso);
70        if(initial && initial.country)iso=initial.country;
71        select.value=iso;
71Array.from(select.options).forEach(o=>o.defaultSelected=o.value===iso);
72        input.placeholder='+'+lib.getCountryCallingCode(iso)+' …';
73        const hint=document.createElement('small');hint.className='ccg-contact-hint';hint.id='ccg-phone-hint-'+(++sequence);hint.textContent='Код города или оператора и номер абонента';wrap.after(hint);
74        input.setAttribute('aria-describedby', [input.getAttribute('aria-describedby'),hint.id].filter(Boolean).join(' '));
75        let formatting=false;
76        function format() {
77            if(formatting)return;
78            const raw=input.value;if(!raw.trim())return;
79            const caret=input.selectionStart, digitsBefore=raw.slice(0,caret).replace(/\D/g,'').length;
80            const parsed=lib.parsePhoneNumberFromString(raw,select.value);
81            // Preserve legacy extensions and unrecognised multi-number text for explicit user correction.
82            if(parsed && parsed.ext)return;
83            if(/[a-zа-я,;\/]/i.test(raw))return;
84            let digits=raw.replace(/\D/g,'');
85            let international=raw.trim().startsWith('+');
86            if(raw.startsWith('00')){digits=digits.slice(2);international=true;}
87            if(!international && ['RU','KZ'].includes(select.value) && digits.length===11 && digits[0]==='8')digits='7'+digits.slice(1),international=true;
88            if(!international){
89                const code=lib.getCountryCallingCode(select.value);
90                // The parser removes national trunk prefixes without dropping subscriber digits.
91                if(parsed && parsed.isPossible())digits=parsed.number.slice(1);
92                else digits=code+digits;
93            }
94            const formatter=new lib.AsYouType(select.value);
95            let value=formatter.input('+'+digits);
96            const detected=formatter.getCountry();if(detected && select.querySelector('option[value="'+detected+'"]'))select.value=detected;
97            if(['RU','KZ'].includes(select.value))value=value.replace(/^(\+7) (\d{3}) (\d{3}) (\d{2}) (\d{2})$/, '$1 ($2) $3-$4-$5');
98            formatting=true;input.value=value;formatting=false;
99            if(document.activeElement===input){
100                let target=digitsBefore+(international?0:lib.getCountryCallingCode(select.value).length), pos=0,count=0;
101                while(pos<value.length && count<target){if(/\d/.test(value[pos]))count++;pos++;}
102                input.setSelectionRange(pos,pos);
103            }
104        }
105        input.addEventListener('beforeinput',e=>{
106            if(e.inputType!=='deleteContentBackward'||input.selectionStart!==input.selectionEnd)return;
107            let end=input.selectionStart,start=end-1;
108            if(start>=0 && /[\s()-]/.test(input.value[start])){while(start>0 && /[\s()-]/.test(input.value[start]))start--;input.setSelectionRange(start,end);}
109        });
110        input.addEventListener('input',format);
111        if(input.form)input.form.addEventListener('reset',()=>setTimeout(format,0));
112        select.addEventListener('change',()=>{
113            const old=lib.parsePhoneNumberFromString(input.value);
114            if(old){input.value='+'+lib.getCountryCallingCode(select.value)+old.nationalNumber;format();notify(input);}
115            input.placeholder='+'+lib.getCountryCallingCode(select.value)+' …';input.focus();
116        });
117        // Render existing valid numbers; no input/change event and no persistence on page load.
118        if(initial && initial.isPossible() && !initial.ext){input.value=initial.formatInternational();format();}
119        const group=input.closest('.form-group');
120        if(group)group.querySelectorAll('.help-block').forEach(el=>{if(el.textContent.includes('+XXX'))el.textContent='Выберите страну и укажите номер телефона.';});
121    }
122    function phoneCountries(select) {
123        const supported=new Set(lib.getCountries());
124        countries.filter(c=>supported.has(c.iso)).forEach(c=>option(select,c.iso,c.name+' (+'+lib.getCountryCallingCode(c.iso)+')'));
125    }
126    function splitPhone(input,prefix,area) {
127        ready.add(prefix);ready.add(area);
128        const select=document.createElement('select');select.className=(prefix.className||'')+' ccg-phone-country';select.setAttribute('aria-label','Страна и телефонный код');phoneCountries(select);
129        const code=String(prefix.value||'7').replace(/\D/g,'');
130        const candidate=countries.find(c=>lib.getCountries().includes(c.iso)&&lib.getCountryCallingCode(c.iso)===code);
131        const parsed=lib.parsePhoneNumberFromString('+'+code+area.value.replace(/\D/g,'')+input.value.replace(/\D/g,''));
132        select.value=parsed&&parsed.country?parsed.country:code==='7'?'RU':candidate?candidate.iso:'RU';Array.from(select.options).forEach(o=>o.defaultSelected=o.selected);
133        prefix.before(select);if(!prefix.value)prefix.value='+7';prefix.type='hidden';prefix.removeAttribute('required');
134        select.addEventListener('change',()=>{prefix.value='+'+lib.getCountryCallingCode(select.value);notify(prefix);});
135        [area,input].forEach(el=>{el.type='tel';el.inputMode='numeric';el.classList.add('ccg-phone-number');el.removeAttribute('pattern');el.style.width='100%';});
136        if(area.id){const areaLabel=document.querySelector('label[for="'+CSS.escape(area.id)+'"]');if(areaLabel)areaLabel.textContent='Код города / оператора';}
137        area.placeholder='Код города / оператора';input.placeholder='Номер абонента';
138        area.addEventListener('input',()=>{area.value=area.value.replace(/\D/g,'');});
139        input.addEventListener('input',()=>{
140            const d=input.value.replace(/\D/g,'');
141            // Existing schema stores country/area/subscriber separately.
142            input.value=d.length===7?d.replace(/^(\d{3})(\d{2})(\d{2})$/,'$1-$2-$3'):d.replace(/(\d{3})(?=\d)/g,'$1 ');
143        });
144    }
145    async function explicitGeo(city) {
146        if(ready.has(city)||city.disabled||city.readOnly)return;
147        ready.add(city);
148        const country=selectCountry(sibling(city,'country')),region=sibling(city,'region');
149        if(!country||!region)return;
150        ready.add(region);
151        const saved={region:region.value,city:city.value};
152        const regionGroup=region.closest('.form-group')||region.parentElement;
153        const regionSelect=document.createElement('select');regionSelect.className='form-control ccg-region-select';
154        regionSelect.id=region.id+'-dictionary';region.before(regionSelect);region.type='hidden';
155        const regionLabel=regionGroup.querySelector('label[for="'+CSS.escape(region.id)+'"]');if(regionLabel)regionLabel.htmlFor=regionSelect.id;
156        const otherRegion=document.createElement('input');otherRegion.type='text';otherRegion.className='form-control ccg-geo-other';otherRegion.maxLength=255;
157        region.after(label('Другой регион:',otherRegion),otherRegion);
158        const cityInput=document.createElement('input');cityInput.className='form-control ccg-city-input';cityInput.autocomplete='off';cityInput.id=city.id+'-dictionary';
159        city.before(cityInput);city.type='hidden';
160        const cityGroup=city.closest('.form-group')||city.parentElement;
161        const cityLabel=cityGroup.querySelector('label[for="'+CSS.escape(city.id)+'"]');if(cityLabel)cityLabel.htmlFor=cityInput.id;
162        const list=document.createElement('datalist');list.id='ccg-cities-'+(++sequence);cityInput.setAttribute('list',list.id);cityInput.after(list);
163        const otherCity=document.createElement('input');otherCity.type='text';otherCity.className='form-control ccg-geo-other';otherCity.maxLength=255;
164        city.after(label('Другой город:',otherCity),otherCity);
165        const hint=document.createElement('small');hint.className='ccg-contact-hint';hint.setAttribute('role','status');otherCity.after(hint);
166        const credit=document.createElement('small');credit.className='ccg-geo-credit';
167        const creditLink=document.createElement('a');creditLink.href='https://www.geonames.org/';creditLink.target='_blank';creditLink.rel='noopener';creditLink.textContent='Справочник GeoNames';credit.append(creditLink);hint.after(credit);
168        let groups=[],regions=[],items=[],version=0;
169        const knownRegion=r=>r.id!=='_' && r.name && !/^Регион\s/.test(r.name);
170        function addSuggestions(){
171            list.replaceChildren();const q=norm(cityInput.value);
172            items.filter(r=>!q||norm(r[2]).includes(q)).slice(0,100).forEach(r=>{const o=document.createElement('option');o.value=r[1];list.append(o);});
173        }
174        function restoreCity(){
175            const known=items.find(r=>norm(r[1])===norm(city.value));
176            cityInput.value=known?known[1]:'';otherCity.value=known?'':city.value;addSuggestions();
177        }
178        async function towns(clear){
179            const ticket=++version,c=countryCode(country.value);
180            items=[];list.replaceChildren();
181            if(clear){city.value='';cityInput.value='';otherCity.value='';notify(city);}
182            if(!c){hint.textContent='Выберите страну.';return;}
183            hint.textContent='Загружаем населённые пункты…';
184            const selected=regions.find(r=>r.name===regionSelect.value);
185            const sources=selected?[selected]:groups;
186            try {
187                const results=await Promise.all(sources.map(r=>load(c+'/'+r.id+'.json')));
188                if(ticket!==version||!city.isConnected)return;
189                const unique=new Map();results.forEach(rows=>rows.forEach(r=>{if(!unique.has(r[0]))unique.set(r[0],r);}));items=Array.from(unique.values());
190                const priority=r=>c==='RU'?(Number(r[0])===524901?0:Number(r[0])===498817?1:2):2;
191                items.sort((a,b)=>priority(a)-priority(b)||a[1].localeCompare(b[1],'ru'));
192                restoreCity();hint.textContent=items.length?'Выберите город из подсказок. Если его нет, заполните «Другой город».':'В справочнике нет городов. Заполните «Другой город».';
193            } catch(e){if(ticket===version){restoreCity();hint.textContent='Справочник временно недоступен. Город можно указать в поле «Другой город».';}}
194        }
195        async function update(clear){
196            const ticket=++version,c=countryCode(country.value);
197            groups=[];regions=[];items=[];regionGroup.hidden=false;regionSelect.replaceChildren();option(regionSelect,'','Все регионы / другой регион');
198            if(clear){region.value='';otherRegion.value='';city.value='';cityInput.value='';otherCity.value='';notify(region);notify(city);}
199            else {otherRegion.value=region.value;otherCity.value=city.value;}
200            list.replaceChildren();
201            if(!c){hint.textContent='Выберите страну.';return;}
202            hint.textContent='Загружаем регионы…';
203            try {
204                const loaded=await load(c+'/regions.json');if(ticket!==version||!city.isConnected)return;groups=loaded;
205                regions=groups.filter(knownRegion);regions.forEach(r=>option(regionSelect,r.name,r.name));
206                const selected=regions.find(r=>norm(r.name)===norm(region.value)||norm(r.english)===norm(region.value)||r.id===region.value);
207                regionSelect.value=selected?selected.name:'';otherRegion.value=selected?'':region.value;
208                regionGroup.hidden=regions.length===0;
209                await towns(false);
210            } catch(e){if(ticket===version)hint.textContent='Справочник временно недоступен. Заполните поля «Другой регион» и «Другой город».';}
211        }
212        country.addEventListener('change',()=>update(true));
213        regionSelect.addEventListener('change',()=>{region.value=regionSelect.value;otherRegion.value='';notify(region);towns(true);});
214        otherRegion.addEventListener('input',()=>{
215            const previous=regionSelect.value;regionSelect.value='';region.value=otherRegion.value;notify(region);
216            if(previous)towns(true);
217        });
218        cityInput.addEventListener('input',()=>{otherCity.value='';city.value=cityInput.value;notify(city);addSuggestions();});
219        cityInput.addEventListener('change',()=>{
220            const known=items.find(r=>norm(r[1])===norm(cityInput.value));
221            if(!known&&cityInput.value){otherCity.value=cityInput.value;cityInput.value='';addSuggestions();}
222        });
223        otherCity.addEventListener('input',()=>{cityInput.value='';city.value=otherCity.value;notify(city);addSuggestions();});
224        if(city.form)city.form.addEventListener('reset',()=>setTimeout(()=>{region.value=saved.region;city.value=saved.city;update(false);},0));
225        await update(false);
226    }
227    async function geo(city) {
228        if(city.hasAttribute('data-ccg-explicit-geo'))return explicitGeo(city);
229        if(ready.has(city)||city.disabled||city.readOnly)return;ready.add(city);
230        let country=sibling(city,'country'),region=sibling(city,'region');
231        const box=document.createElement('div');box.className='ccg-geo-assist';
232        // Add selectors only where the original schema has no corresponding fields.
233        city.before(box);
234        if(country)country=selectCountry(country);
235        else {country=document.createElement('select');option(country,'','Выберите страну');countries.forEach(c=>option(country,c.iso,c.name));box.append(label('Страна',country),country);ready.add(country);}
236        let originalRegion=region;const initialRegion=region?region.value:'';
237        if(region && region.tagName!=='SELECT'){
238            const replacement=document.createElement('select');
239            ['id','name','class','required','disabled','aria-describedby'].forEach(a=>{if(region.hasAttribute(a))replacement.setAttribute(a,region.getAttribute(a));});
240            if(region.value)option(replacement,region.value,region.value);replacement.value=region.value;region.replaceWith(replacement);region=replacement;
241        }
242        if(!region){region=document.createElement('select');box.append(label('Область / регион',region),region);}
243        ready.add(region);region.classList.add('ccg-region-select');
244        if(!originalRegion && key(city).prefix==='User'){
245            region.addEventListener('change',()=>{region.name='User[region]';});
246            country.addEventListener('change',()=>{region.name='User[region]';});
247        }
248        const list=document.createElement('datalist');list.id='ccg-cities-'+(++sequence);city.classList.add('ccg-city-input');city.setAttribute('list',list.id);city.autocomplete='off';city.after(list);
249        const hint=document.createElement('small');hint.className='ccg-contact-hint';hint.setAttribute('role','status');list.after(hint);
250        const credit=document.createElement('small');credit.className='ccg-geo-credit';
251        const creditLink=document.createElement('a');creditLink.href='https://www.geonames.org/';creditLink.target='_blank';creditLink.rel='noopener';creditLink.textContent='Справочник GeoNames';credit.append(creditLink);hint.after(credit);
252        let regions=[],items=[],prominent=[],version=0;
253        function suggestions(){
254            const q=norm(city.value);list.replaceChildren();
255            items.filter(r=>!q||norm(r[2]).includes(q)).slice(0,80).forEach(r=>{const o=document.createElement('option');o.value=r[1];list.append(o);});
256        }
257        async function towns(clear){
258            const v=++version;items=[];list.replaceChildren();if(clear){city.value='';notify(city);}
259            const c=countryCode(country.value),r=region.value && regions.find(r=>norm(r.name)===norm(region.value) || norm(r.english)===norm(region.value) || r.id===region.value);
260            if(!c||!r){
261                if(c==='RU'){try{prominent=await load('RU/prominent.json');if(v!==version)return;items=prominent;suggestions();}catch(e){}}
262                hint.textContent='Выберите регион или один из предложенных городов.';return;
263            }
264            hint.textContent='Загружаем населённые пункты…';
265            try{const rows=await load(c+'/'+r.id+'.json');if(v!==version||!city.isConnected)return;items=rows;suggestions();hint.textContent='Начните вводить название. Если его Ð
265½ÐµÑ‚ в списке, сохраните своё значение.';}
266            catch(e){if(v===version)hint.textContent='Список временно недоступен. Название можно ввести вручную.';}
267        }
268        async function update(clear){
269            const v=++version,c=countryCode(country.value),old=clear?'':region.value;
270            items=[];list.replaceChildren();regions=[];region.replaceChildren();option(region,'','Выберите область / регион');
271            if(old)option(region,old,old),region.value=old;
272            if(clear){city.value='';notify(city);if(originalRegion)region.dispatchEvent(new Event('input',{bubbles:true}));}
273            if(!c){hint.textContent='Выберите страну для поиска региона и населённого пункта.';return;}
274            try{
275                const data=await load(c+'/regions.json');if(v!==version||!city.isConnected)return;regions=data;
276                data.forEach(r=>{if(norm(r.name)!==norm(old))option(region,r.name,r.name);});region.value=old;
277                if(data.length===1&&!old){region.value=data[0].name;if(originalRegion)region.dispatchEvent(new Event('input',{bubbles:true}));}
278                await towns(false);
279            }catch(e){if(v===version)hint.textContent='Справочник временно недоступен. Сохранённые данные оставлены без изменений.';}
280        }
281        country.addEventListener('change',()=>update(true));region.addEventListener('change',()=>towns(true));city.addEventListener('input',suggestions);
282        city.addEventListener('change',()=>{
283            if(countryCode(country.value)!=='RU'||region.value)return;
284            const chosen=prominent.find(r=>norm(r[1])===norm(city.value));
285            if(chosen){const r=regions.find(r=>r.id===chosen[4]);if(r){region.value=r.name;if(key(city).prefix==='User'&&!originalRegion)region.name='User[region]';towns(false);}}
286        });
287        if(city.form)city.form.addEventListener('reset',()=>setTimeout(()=>{
288            if(initialRegion && !Array.from(region.options).some(o=>o.value===initialRegion))option(region,initialRegion,initialRegion);
289            region.value=initialRegion;update(false);
290        },0));
291        await update(false);
292    }
293    function scan(scope) {
294        scope.querySelectorAll('.ccg-phone').forEach(w=>{const i=w.querySelector('input'),s=w.querySelector('select');if(i&&s&&s.disabled!==(i.disabled||i.readOnly))s.disabled=i.disabled||i.readOnly;});
295        const inputs=Array.from(scope.querySelectorAll('input,select'));
296        inputs.filter(el=>el.type!=='hidden'&&key(el).field==='country'&&!/(Search|Filter)/i.test(key(el).prefix)).forEach(selectCountry);
297        inputs.filter(el=>el.tagName==='INPUT'&&el.type!=='hidden'&&(phoneNames.has(key(el).field)||el.type==='tel'))
298            .filter(el=>!/(Search|Filter)/i.test(key(el).prefix)).forEach(phone);
299        inputs.filter(el=>el.tagName==='INPUT'&&cityNames.has(key(el).field)&&el.type!=='hidden')
300            .filter(el=>!/(Search|Filter)/i.test(key(el).prefix)).forEach(geo);
301    }
302    load('countries.json').then(data=>{
303        countries=data;countries.sort((a,b)=>rankCountry(a.iso)-rankCountry(b.iso)||a.name.localeCompare(b.name,'ru'));scan(document);
304        let scheduled=false;
305        new MutationObserver(records=>{
306            if(!records.some(r=>r.type==='attributes'||Array.from(r.addedNodes).some(n=>n.nodeType===1&&(n.matches('input,select,form')||n.querySelector('input,select')))))return;
307            if(!scheduled){scheduled=true;setTimeout(()=>{scheduled=false;scan(document);},0);}
308        }).observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:['disabled','readonly']});
309    }).catch(()=>{/* Native existing controls stay usable when dictionaries cannot be loaded. */});
310})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.