PageSourceSearch

https://severstal-hc.ru/local/templates/new_arena/js/simple-phone-mask.js?177131410310514

js severstal-hc.ru collected 2026-10-08 14:10:59 UTC 10,514 bytes, 330 lines download raw bytes

1/**
2 * Class representing a phone number mask
3 */
4class SimplePhoneMask {
5	/**
6	 * Create a phone mask
7	 * @param {string} selector - CSS selector for input elements
8	 * @param {Object} options - Configuration options
9	 * @param {string} [options.countryCode="RU"] - Country code (e.g., 'UA', 'US', 'GB') or phone code (e.g., '+380', '+1', '+44')
10	 * @param {string} [options.maskPattern=null] - Custom mask pattern (overrides default country mask)
11	 * @param {boolean} [options.showFlag=true] - Show country flag
12	 * @param {boolean} [options.allowCountrySelect=true] - Allow country selection from dropdown
13	 * @param {boolean} [options.detectIP=false] - Detect country by IP
14	 */
15	constructor(selector, options = {}) {
16		this.selector = selector;
17
18		// Normalize options with defaults
19		const defaultOptions = {
20			countryCode: 'RU',
21			maskPattern: null,
22			showFlag: true,
23			allowCountrySelect: true,
24			detectIP: false,
25		};
26
27		this.options = { ...defaultOptions, ...options };
28
29		// Get country data
30		let country;
31		if (this.options.countryCode.startsWith('+')) {
32			// Find country by phone code
33			const phoneCode = this.options.countryCode;
34			country = Object.values(countryData).find((c) => c.phoneCode === phoneCode);
35		} else {
36			// Find country by country code
37			country = countryData[this.options.countryCode.toUpperCase()];
38		}
39
40		if (!country) {
41			// console.error(`Invalid country code: ${this.options.countryCode}`);
42			country = countryData.RU; // Default to Ukraine
43		}
44
45		this.countryCode = country.phoneCode.replace('+', '');
46		this.maskPattern = this.options.maskPattern || country.mask;
47		this.prefixLength = this.countryCode.length + 1; // +1 for + symbol
48		this.currentCountry = country;
49
50		// Bind methods to preserve context
51		this.createMask = this.createMask.bind(this);
52		this.handleClick = this.handleClick.bind(this);
53		this.handleKeyDown = this.handleKeyDown.bind(this);
54		this.handleSelect = this.handleSelect.bind(this);
55		this.handleCountrySelect = this.handleCountrySelect.bind(this);
56		this.createDropdown = this.createDropdown.bind(this);
57
58		this.initialize();
59	}
60
61	/**
62	 * Set cursor position in input element
63	 * @param {number} pos - Cursor position
64	 * @param {HTMLElement} elem - Input element
65	 * @param {boolean} [setFocus=false] - Whether to set focus on element
66	 */
67	setCursorPosition(pos, elem, setFocus = false) {
68		if (setFocus) {
69			elem.focus();
70		}
71
72		if (elem.setSelectionRange) {
73			elem.setSelectionRange(pos, pos);
74		} else if (elem.createTextRange) {
75			let range = elem.createTextRange();
76			range.collapse(true);
77			range.moveEnd('character', pos);
78			range.moveStart('character', pos);
79			range.select();
80		}
81	}
82
83	/**
84	 * Handle cursor position on click
85	 * @param {Event} e - Click event
86	 */
87	handleClick(e) {
88		const input = e.target;
89		if (input.selectionStart < this.prefixLength + 1) {
90			// Set focus to true only for click events
91			this.setCursorPosition(this.prefixLength + 1, input, true);
92		}
93	}
94
95	/**
96	 * Handle keyboard navigation
97	 * @param {KeyboardEvent} event - Keyboard event
98	 */
99	handleKeyDown(event) {
100		const input = event.target;
101		const cursorPosition = input.selectionStart;
102
103		// console.log( cursorPosition )
104
105		if (
106			(event.key === 'Backspace' || event.key === 'ArrowLeft' || event.key === 'ArrowUp') &&
107			cursorPosition <= this.prefixLength
108		) {
109			event.preventDefault();
110			// Set focus to true for keyboard navigation
111			this.setCursorPosition(this.prefixLength + 1, input, true);
112		}
113
114		// Prevent cursor from moving up when more than one digit is entered
115		const digitsEntered = input.value.replace(/\D/g, '').length - this.countryCode.length;
116		if (event.key === 'ArrowUp' && digitsEntered > 1) {
117			event.preventDefault();
118		}
119
120		// Ensure the mask is applied correctly and the cursor stays within the allowed range
121		// This is crucial for maintaining the correct mask format and preventing the cursor from moving outside the allowed range
122	}
123
124	/**
125	 * Handle text selection
126	 * @param {Event} e - Select event
127	 */
128	handleSelect(e) {
129		const input = e.target;
130		if (input.selectionStart < this.prefixLength + 1) {
131			// Set focus to true only for select events
132			this.setCursorPosition(this.prefixLength + 1, input, true);
133		}
134	}
135
136	/**
137	 * Create mask for input value
138	 * @param {Event} event - Input event
139	 */
140	createMask(event) {
141		const input = event.target;
142		const matrix = '+' + this.countryCode + ' ' + this.maskPattern;
143		let i = 0;
144		const def = matrix.replace(/\D/g, '');
145		let val = input.value.replace(/\D/g, '');
146
147		// Remove temporary protection of country code for debugging
148		// if (!input.value.startsWith("+" + this.countryCode)) {
149		//   val = def;
150		// }
151
152		// если второй введённый символ 8 или 9, а первый 8, то удалить первый символ
153		if( this.countryCode == 7 && val.length == 3 && val.charAt(1) == 8 && (val.charAt(2) == 8 || val.charAt(2) == 9) )
154			val = val.slice(0, 1) + val.slice(2);
155
156		input.value = matrix.replace(/./g, function (a) {
157			if (/[_\d]/.test(a) && i < val.length) {
158				return val.charAt(i++);
159			} else if (i >= val.length) {
160				return '';
161			} else {
162				return a;
163			}
164		});
165
166		if (event.type === 'blur') {
167			if (input.value.length <= this.prefixLength + 1) {
168				input.value = '+' + this.countryCode;
169			}
170		} else {
171			this.setCursorPosition(input.value.length, input);
172		}
173	}
174
175	/**
176	 * Create country selector dropdown
177	 * @param {HTMLElement} input - Input element
178	 */
179	createDropdown(input) {
180		if (!this.options.showFlag) return;
181
182		const wrapper = document.createElement('div');
183		wrapper.className = 'spm-wrapper';
184		input.parentNode.insertBefore(wrapper, input);
185		wrapper.appendChild(input);
186		input.className += ' spm-input';
187
188		const flagButton = document.createElement('div');
189		flagButton.className = `spm-flag-button ${
190			this.options.allowCountrySelect ? 'spm-flag-button--selectable' : 'spm-flag-button--non-selectable'
191		}`;
192
193		flagButton.innerHTML = `<img class="spm-flag-image" src="${this.currentCountry.flag}" alt="${this.currentCountry.name}"><svg class="spm-arrow-icon" width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2"/></svg>`;
194		wrapper.appendChild(flagButton);
195
196		if (this.options.allowCountrySelect) {
197			const dropdown = document.createElement('div');
198			dropdown.className = 'spm-dropdown';
199
200			Object.entries(countryData).forEach(([code, country]) => {
201				const option = document.createElement('div');
202				option.className = 'spm-dropdown-option';
203
204				option.innerHTML = `
205          <img class="spm-flag-image" src="${country.flag}" alt="${country.name}">
206          <span class="spm-country-name">${country.name}</span>
207          <span class="spm-country-code">${country.phoneCode}</span>
208        `;
209
210				option.addEventListener('click', () => this.handleCountrySelect(country, code, input));
211				dropdown.style.display = 'none';
212				dropdown.appendChild(option);
213			});
214
215			wrapper.appendChild(dropdown);
216
217			flagButton.addEventListener('click', () => {
218				dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
219			});
220
221			document.addEventListener('click', (e) => {
222				if (!wrapper.contains(e.target)) {
223					dropdown.style.display = 'none';
224				}
225			});
226		}
227	}
228
229	/**
230	 * Handle country selection from dropdown
231	 * @param {Object} country - Country data object
232	 * @param {string} code - Country code
233	 * @param {HTMLElement} input - Input element
234	 */
235	handleCountrySelect(country, code, input) {
236		this.countryCode = country.phoneCode.replace('+', '');
237		this.maskPattern = this.options.maskPattern || country.mask;
238		this.prefixLength = this.countryCode.length + 1;
239		this.currentCountry = country;
240
241		const flagButton = input.parentNode.querySelector('.spm-flag-button');
242		flagButton.innerHTML = `<img class="spm-flag-image" src="${country.flag}" alt="${country.name}"><svg class="spm-arrow-icon" width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2"/></svg>`;
243
244		const dropdown = input.parentNode.querySelector('.spm-dropdown');
245		dropdown.style.display = 'none';
246
247		input.value = '+' + this.countryCode;
248
249		this.createMask({
250			type: 'input',
251			target: input,
252		});
253	}
254
255	/**
256	 * Initialize mask for all matching inputs
257	 */
258	async initialize() {
259		if (this.options.detectIP) {
260			try {
261				const countryCode = await this.detectCountryByIP();
262				this.options.countryCode = countryCode;
263
264				// Update country data after IP detection
265				const country = countryData[countryCode];
266				if (country) {
267					this.countryCode = country.phoneCode.replace('+', '');
268					this.maskPattern = this.options.maskPattern || country.mask;
269					this.prefixLength = this.countryCode.length + 1;
270					this.currentCountry = country;
271				}
272			} catch (error) {
273				console.error('Failed to detect country by IP:', error);
274			}
275		}
276
277		// Initialize inputs
278		let inputs = document.querySelectorAll(this.selector);
279
280		inputs.forEach((input) => {
281			if (this.options.showFlag) {
282				this.createDropdown(input);
283			}
284
285			input.addEventListener('input', this.createMask);
286			input.addEventListener('focus', this.createMask);
287			input.addEventListener('blur', this.createMask);
288			input.addEventListener('click', this.handleClick);
289			input.addEventListener('keydown', this.handleKeyDown);
290			input.addEventListener('select', this.handleSelect);
291
292			input.value = '+' + this.countryCode;
293			// Remove focus trigger
294			this.createMask({ type: 'input', target: input });
295		});
296	}
297
298	/**
299	 * Detect country by IP
300	 * @returns {Promise<string>} - Country code
301	 */
302	async detectCountryByIP() {
303		try {
304			const response = await fetch('https://ipapi.co/json/', {
305				cache: 'no-store', // Disable caching
306			});
307			const data = await response.json();
308			return data.country_code;
309		} catch (error) {
310			console.error('Error detecting country by IP:', error);
311			return this.options.countryCode;
312		}
313	}
314
315	/**
316	 * Destroy mask and remove all event listeners
317	 */
318	destroy() {
319		let inputs = document.querySelectorAll(this.selector);
320
321		inputs.forEach((input) => {
322			input.removeEventListener('input', this.createMask);
323			input.removeEventListener('focus', this.createMask);
324			input.removeEventListener('blur', this.createMask);
325			input.removeEventListener('click', this.handleClick);
326			input.removeEventListener('keydown', this.handleKeyDown);
327			input.removeEventListener('select', this.handleSelect);
328		});
329	}
330}

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.