# π Π ΡΠΊΠΎΠ²ΠΎΠ΄ΡΡΠ²ΠΎ ΠΏΠΎ JavaScript ΠΌΠΎΠ΄ΡΠ»ΡΠΌ EUSRR
**ΠΠ΅ΡΡΠΈΡ**: 1.0
**ΠΠ°ΡΠ°**: 4 Π½ΠΎΡΠ±ΡΡ 2025 Π³.
---
## π Π‘ΠΎΠ΄Π΅ΡΠΆΠ°Π½ΠΈΠ΅
1. [ΠΠ±Π·ΠΎΡ Π°ΡΡ
ΠΈΡΠ΅ΠΊΡΡΡΡ](#ΠΎΠ±Π·ΠΎΡ-Π°ΡΡ
ΠΈΡΠ΅ΠΊΡΡΡΡ)
2. [Π£ΡΠΈΠ»ΠΈΡΡ (utils)](#ΡΡΠΈΠ»ΠΈΡΡ)
3. [ΠΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΡ (components)](#ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΡ)
4. [ΠΡΠΈΠΌΠ΅ΡΡ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΡ](#ΠΏΡΠΈΠΌΠ΅ΡΡ-ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΡ)
5. [Best Practices](#best-practices)
---
## ποΈ ΠΠ±Π·ΠΎΡ Π°ΡΡ
ΠΈΡΠ΅ΠΊΡΡΡΡ
### Π‘ΡΡΡΠΊΡΡΡΠ° ΡΠ°ΠΉΠ»ΠΎΠ²
```
static/js/
βββ utils/ # Π£ΡΠΈΠ»ΠΈΡΠ°ΡΠ½ΡΠ΅ ΡΡΠ½ΠΊΡΠΈΠΈ
β βββ stringUtils.js # Π Π°Π±ΠΎΡΠ° ΡΠΎ ΡΡΡΠΎΠΊΠ°ΠΌΠΈ
β βββ timing.js # Debounce, throttle, delay
β βββ scroll.js # Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΡΠΊΡΠΎΠ»Π»ΠΎΠΌ
βββ components/ # ΠΠ΅ΡΠ΅ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΠΌΡΠ΅ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΡ
βββ calendarWidget.js # ΠΠΈΠ΄ΠΆΠ΅Ρ ΠΊΠ°Π»Π΅Π½Π΄Π°ΡΡ
βββ deptRoleEditor.js # Π Π΅Π΄Π°ΠΊΡΠΎΡ ΡΠΎΠ»Π΅ΠΉ ΠΎΡΠ΄Π΅Π»ΠΎΠ²
βββ employeeFormHandler.js # Π€ΠΎΡΠΌΠ° ΡΠΎΡΡΡΠ΄Π½ΠΈΠΊΠ°
βββ employeeGroupsManager.js # ΠΡΡΠΏΠΏΡ ΡΠΎΡΡΡΠ΄Π½ΠΈΠΊΠ°
βββ groupPickers.js # ΠΡΠ±ΠΎΡ Π³ΡΡΠΏΠΏ Π΄ΠΎΠ»ΠΆΠ½ΠΎΡΡΠ΅ΠΉ
βββ headPicker.js # Autocomplete ΡΡΠΊΠΎΠ²ΠΎΠ΄ΠΈΡΠ΅Π»Ρ
βββ likeHandler.js # ΠΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊ Π»Π°ΠΉΠΊΠΎΠ²
βββ listFilter.js # Π€ΠΈΠ»ΡΡΡΠ°ΡΠΈΡ ΡΠΏΠΈΡΠΊΠΎΠ²
βββ navbarHeight.js # CSS ΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½Π½Π°Ρ navbar
βββ positionManager.js # Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎΡΡΡΠΌΠΈ
βββ roleManager.js # Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΡΠΎΠ»ΡΠΌΠΈ
βββ skillsManager.js # Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ Π½Π°Π²ΡΠΊΠ°ΠΌΠΈ
βββ teamWheel.js # ΠΠΎΠ»Π΅ΡΠΎ ΠΊΠΎΠΌΠ°Π½Π΄Ρ
βββ textareaAutogrow.js # ΠΠ²ΡΠΎΡΠ²Π΅Π»ΠΈΡΠ΅Π½ΠΈΠ΅ textarea
βββ index.js # ΠΠΊΡΠΏΠΎΡΡ Π²ΡΠ΅Ρ
ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΠΎΠ²
```
---
## π οΈ Π£ΡΠΈΠ»ΠΈΡΡ
### `stringUtils.js`
Π£ΡΠΈΠ»ΠΈΡΡ Π΄Π»Ρ ΡΠ°Π±ΠΎΡΡ ΡΠΎ ΡΡΡΠΎΠΊΠ°ΠΌΠΈ ΠΈ HTML.
#### `esc(s: string): string`
ΠΠΊΡΠ°Π½ΠΈΡΡΠ΅Ρ HTML-ΡΠΈΠΌΠ²ΠΎΠ»Ρ Π΄Π»Ρ Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΠ³ΠΎ Π²ΡΠ²ΠΎΠ΄Π°.
```javascript
import { esc } from '{% static "js/utils/stringUtils.js" %}';
const userInput = '';
const safe = esc(userInput);
// => '<script>alert("xss")</script>'
```
#### `norm(s: string): string`
ΠΠΎΡΠΌΠ°Π»ΠΈΠ·ΡΠ΅Ρ ΡΡΡΠΎΠΊΡ Π΄Π»Ρ ΠΏΠΎΠΈΡΠΊΠ° (lowercase, trim).
```javascript
import { norm } from '{% static "js/utils/stringUtils.js" %}';
const search = norm(' Hello World ');
// => 'hello world'
```
#### `escAttr(s: string): string`
ΠΠΊΡΠ°Π½ΠΈΡΡΠ΅Ρ ΡΡΡΠΎΠΊΡ Π΄Π»Ρ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΡ Π² Π°ΡΡΠΈΠ±ΡΡΠ°Ρ
.
```javascript
const attr = escAttr('some "quoted" text');
// => 'some "quoted" text'
```
#### `truncate(s: string, maxLen: number): string`
ΠΠ±ΡΠ΅Π·Π°Π΅Ρ ΡΡΡΠΎΠΊΡ Π΄ΠΎ ΡΠΊΠ°Π·Π°Π½Π½ΠΎΠΉ Π΄Π»ΠΈΠ½Ρ Ρ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ΠΌ "...".
```javascript
const short = truncate('Very long text here', 10);
// => 'Very lon...'
```
#### `getInitials(name: string): string`
ΠΠΎΠ»ΡΡΠ°Π΅Ρ ΠΈΠ½ΠΈΡΠΈΠ°Π»Ρ ΠΈΠ· ΠΈΠΌΠ΅Π½ΠΈ.
```javascript
const initials = getInitials('ΠΠ²Π°Π½ ΠΠ΅ΡΡΠΎΠ²ΠΈΡ Π‘ΠΈΠ΄ΠΎΡΠΎΠ²');
// => 'ΠΠΠ‘'
```
---
### `timing.js`
Π£ΡΠΈΠ»ΠΈΡΡ Π΄Π»Ρ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π²ΡΠ΅ΠΌΠ΅Π½Π΅ΠΌ Π²ΡΠΏΠΎΠ»Π½Π΅Π½ΠΈΡ.
#### `debounce(fn: Function, ms: number): Function`
ΠΡΠΊΠ»Π°Π΄ΡΠ²Π°Π΅Ρ Π²ΡΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ ΡΡΠ½ΠΊΡΠΈΠΈ Π΄ΠΎ ΠΏΡΠ΅ΠΊΡΠ°ΡΠ΅Π½ΠΈΡ Π²ΡΠ·ΠΎΠ²ΠΎΠ².
```javascript
import { debounce } from '{% static "js/utils/timing.js" %}';
const search = debounce((query) => {
console.log('Searching for:', query);
}, 300);
input.addEventListener('input', (e) => search(e.target.value));
```
#### `throttle(fn: Function, ms: number): Function`
ΠΠ³ΡΠ°Π½ΠΈΡΠΈΠ²Π°Π΅Ρ ΡΠ°ΡΡΠΎΡΡ Π²ΡΠ·ΠΎΠ²Π° ΡΡΠ½ΠΊΡΠΈΠΈ.
```javascript
import { throttle } from '{% static "js/utils/timing.js" %}';
const onScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.addEventListener('scroll', onScroll);
```
#### `delay(ms: number): Promise`
ΠΡΠΎΠΌΠΈΡΠΈΡΠΈΡΠΈΡΠΎΠ²Π°Π½Π½Π°Ρ Π·Π°Π΄Π΅ΡΠΆΠΊΠ°.
```javascript
import { delay } from '{% static "js/utils/timing.js" %}';
async function animateSequence() {
element.classList.add('step1');
await delay(300);
element.classList.add('step2');
await delay(300);
element.classList.add('complete');
}
```
---
### `scroll.js`
Π£ΡΠΈΠ»ΠΈΡΡ Π΄Π»Ρ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ ΡΠΊΡΠΎΠ»Π»ΠΎΠΌ.
#### `smoothScrollTo(element: HTMLElement, options?: Object): void`
ΠΠ»Π°Π²Π½ΠΎ ΠΏΡΠΎΠΊΡΡΡΠΈΠ²Π°Π΅Ρ ΠΊ ΡΠ»Π΅ΠΌΠ΅Π½ΡΡ.
```javascript
import { smoothScrollTo } from '{% static "js/utils/scroll.js" %}';
const target = document.getElementById('section');
smoothScrollTo(target, {
offset: 80, // ΠΡΡΡΡΠΏ ΡΠ²Π΅ΡΡ
Ρ
duration: 500 // ΠΠ»ΠΈΡΠ΅Π»ΡΠ½ΠΎΡΡΡ Π°Π½ΠΈΠΌΠ°ΡΠΈΠΈ
});
```
#### `scrollToTop(duration: number = 300): void`
ΠΠ»Π°Π²Π½ΠΎ ΠΏΡΠΎΠΊΡΡΡΠΈΠ²Π°Π΅Ρ ΡΡΡΠ°Π½ΠΈΡΡ Π½Π°Π²Π΅ΡΡ
.
```javascript
button.addEventListener('click', () => scrollToTop(500));
```
#### `isElementInViewport(el: HTMLElement): boolean`
ΠΡΠΎΠ²Π΅ΡΡΠ΅Ρ, Π²ΠΈΠ΄Π΅Π½ Π»ΠΈ ΡΠ»Π΅ΠΌΠ΅Π½Ρ Π² ΠΎΠΊΠ½Π΅ ΠΏΡΠΎΡΠΌΠΎΡΡΠ°.
```javascript
if (isElementInViewport(element)) {
element.classList.add('animate');
}
```
---
## π¨ ΠΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΡ
### `navbarHeight.js`
Π£ΡΡΠ°Π½Π°Π²Π»ΠΈΠ²Π°Π΅Ρ CSS-ΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½Π½ΡΡ Ρ Π²ΡΡΠΎΡΠΎΠΉ Π½Π°Π²Π±Π°ΡΠ°.
#### API
```javascript
import { initNavbarHeight } from '{% static "js/components/navbarHeight.js" %}';
const navbarHeight = initNavbarHeight();
// ΠΠ²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈ ΡΠΎΠ·Π΄Π°ΡΡ CSS ΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½Π½ΡΡ --navbar-height
```
**ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅ Π² CSS:**
```css
.content {
margin-top: var(--navbar-height);
}
```
---
### `likeHandler.js`
ΠΠ»ΠΎΠ±Π°Π»ΡΠ½ΡΠΉ ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊ Π»Π°ΠΉΠΊΠΎΠ² Π΄Π»Ρ ΠΏΠΎΡΡΠΎΠ² ΠΈ ΠΊΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠ΅Π².
#### API
```javascript
import { initLikeHandler } from '{% static "js/components/likeHandler.js" %}';
const likeHandler = initLikeHandler({
postUrl: '/api/v1/feed/posts/{id}/like/',
commentUrl: '/api/v1/feed/comments/{id}/like/'
});
```
#### HTML ΡΠ°Π·ΠΌΠ΅ΡΠΊΠ°
```django
```
#### ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΡΠΉ API
- `refresh()` - ΠΏΠ΅ΡΠ΅ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊΠΎΠ²
---
### `textareaAutogrow.js`
ΠΠ²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈ ΡΠ²Π΅Π»ΠΈΡΠΈΠ²Π°Π΅Ρ Π²ΡΡΠΎΡΡ textarea ΠΏΡΠΈ Π²Π²ΠΎΠ΄Π΅ ΡΠ΅ΠΊΡΡΠ°.
#### API
```javascript
import { initTextareaAutogrow } from '{% static "js/components/textareaAutogrow.js" %}';
const autogrow = initTextareaAutogrow({
selector: '[data-autogrow]',
minHeight: 40,
maxHeight: 300
});
```
#### HTML ΡΠ°Π·ΠΌΠ΅ΡΠΊΠ°
```html
```
#### ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΡΠΉ API
- `refresh()` - ΠΏΠ΅ΡΠ΅ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ Π΄Π»Ρ Π½ΠΎΠ²ΡΡ
ΡΠ»Π΅ΠΌΠ΅Π½ΡΠΎΠ²
- `destroy()` - ΠΎΡΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊΠΎΠ²
---
### `employeeFormHandler.js`
Π£ΠΏΡΠ°Π²Π»ΡΠ΅Ρ ΡΠΎΡΠΌΠΎΠΉ ΡΠΎΡΡΡΠ΄Π½ΠΈΠΊΠ° (Π²Π°Π»ΠΈΠ΄Π°ΡΠΈΡ, ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ, AJAX).
#### API
```javascript
import { initEmployeeForm } from '{% static "js/components/employeeFormHandler.js" %}';
const employeeForm = initEmployeeForm();
```
#### ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ
- β
ΠΡΡΠ»Π΅ΠΆΠΈΠ²Π°Π½ΠΈΠ΅ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ ΠΏΠΎΠ»Π΅ΠΉ
- β
ΠΠ°Π»ΠΈΠ΄Π°ΡΠΈΡ ΠΏΠ΅ΡΠ΅Π΄ ΠΎΡΠΏΡΠ°Π²ΠΊΠΎΠΉ
- β
ΠΡΠ΅Π΄ΡΠΏΡΠ΅ΠΆΠ΄Π΅Π½ΠΈΠ΅ ΠΎ Π½Π΅ΡΠΎΡ
ΡΠ°Π½ΡΠ½Π½ΡΡ
ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡΡ
- β
AJAX ΠΎΡΠΏΡΠ°Π²ΠΊΠ° ΡΠΎΡΠΌΡ
- β
ΠΠ±ΡΠ°Π±ΠΎΡΠΊΠ° ΠΎΡΠΈΠ±ΠΎΠΊ
#### ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΡΠΉ API
- `hasChanges()` - ΠΏΡΠΎΠ²Π΅ΡΠΊΠ° Π½Π°Π»ΠΈΡΠΈΡ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ
- `resetTracking()` - ΡΠ±ΡΠΎΡ ΠΎΡΡΠ»Π΅ΠΆΠΈΠ²Π°Π½ΠΈΡ
- `validateForm()` - Π²Π°Π»ΠΈΠ΄Π°ΡΠΈΡ ΡΠΎΡΠΌΡ
- `isDirty` - ΡΠ»Π°Π³ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ
---
### `positionManager.js`
Π£ΠΏΡΠ°Π²Π»ΡΠ΅Ρ CRUD ΠΎΠΏΠ΅ΡΠ°ΡΠΈΡΠΌΠΈ Ρ Π΄ΠΎΠ»ΠΆΠ½ΠΎΡΡΡΠΌΠΈ ΡΠ΅ΡΠ΅Π· ΠΌΠΎΠ΄Π°Π»ΡΠ½ΡΠ΅ ΠΎΠΊΠ½Π°.
#### API
```javascript
import { initPositionManager } from '{% static "js/components/positionManager.js" %}';
const positionMgr = initPositionManager();
```
#### HTML ΡΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ
- `#positionBlock[data-update-url-tpl]` - ΡΠ°Π±Π»ΠΎΠ½ URL ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΡ
- `#positionBlock[data-detail-url-tpl]` - ΡΠ°Π±Π»ΠΎΠ½ URL ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ
- `#positionBlock[data-create-url]` - URL ΡΠΎΠ·Π΄Π°Π½ΠΈΡ
- ΠΠΎΠ΄Π°Π»Ρ: `#posEditModal`, `#posCreateModal`
#### ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΡΠΉ API
- `refresh()` - ΠΏΠ΅ΡΠ΅ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ
- `openCreateModal()` - ΠΎΡΠΊΡΡΡΡ ΠΌΠΎΠ΄Π°Π» ΡΠΎΠ·Π΄Π°Π½ΠΈΡ
- `openEditModal(positionId)` - ΠΎΡΠΊΡΡΡΡ ΠΌΠΎΠ΄Π°Π» ΡΠ΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΡ
---
### `groupPickers.js`
ΠΠΈΠΊΠ΅ΡΡ Π΄Π»Ρ Π²ΡΠ±ΠΎΡΠ° Π³ΡΡΠΏΠΏ Π΄ΠΎΠ»ΠΆΠ½ΠΎΡΡΠ΅ΠΉ (ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΈ ΡΠ΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅).
#### API
```javascript
import {
initPositionGroupPicker,
initPositionEditGroupPicker
} from '{% static "js/components/groupPickers.js" %}';
// ΠΠ»Ρ ΡΠΎΡΠΌΡ ΡΠΎΠ·Π΄Π°Π½ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½ΠΎΡΡΠΈ
const createPicker = initPositionGroupPicker();
// ΠΠ»Ρ ΡΠΎΡΠΌΡ ΡΠ΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½ΠΎΡΡΠΈ
const editPicker = initPositionEditGroupPicker();
```
#### ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ
- β
ΠΡΠ±ΠΎΡ Π½Π΅ΡΠΊΠΎΠ»ΡΠΊΠΈΡ
Π³ΡΡΠΏΠΏ
- β
ΠΡΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ ΡΠΈΠΏΠΎΠ² Π²ΡΠ±ΡΠ°Π½Π½ΡΡ
Π³ΡΡΠΏΠΏ
- β
ΠΠΎΠΈΡΠΊ ΠΏΠΎ Π³ΡΡΠΏΠΏΠ°ΠΌ
- β
Π‘ΠΈΠ½Ρ
ΡΠΎΠ½ΠΈΠ·Π°ΡΠΈΡ Ρ hidden input'Π°ΠΌΠΈ
---
### `employeeGroupsManager.js`
Π£ΠΏΡΠ°Π²Π»ΡΠ΅Ρ ΠΏΠ΅ΡΡΠΎΠ½Π°Π»ΡΠ½ΡΠΌΠΈ Π³ΡΡΠΏΠΏΠ°ΠΌΠΈ ΠΏΡΠ°Π² ΡΠΎΡΡΡΠ΄Π½ΠΈΠΊΠ°.
#### API
```javascript
import { initEmployeeGroupsManager } from '{% static "js/components/employeeGroupsManager.js" %}';
const groupsMgr = initEmployeeGroupsManager();
```
#### HTML ΡΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ
- `#empGroupsAssignBtn` - ΠΊΠ½ΠΎΠΏΠΊΠ° Π½Π°Π·Π½Π°ΡΠ΅Π½ΠΈΡ Π³ΡΡΠΏΠΏ
- `#empGroupsList` - ΡΠΏΠΈΡΠΎΠΊ ΡΠ΅ΠΊΡΡΠΈΡ
Π³ΡΡΠΏΠΏ
- `#empGroupsModal` - ΠΌΠΎΠ΄Π°Π» Π²ΡΠ±ΠΎΡΠ° Π³ΡΡΠΏΠΏ
#### ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΡΠΉ API
- `refresh()` - ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΡΠΏΠΈΡΠΊΠ°
- `assignGroups(groupIds)` - Π½Π°Π·Π½Π°ΡΠΈΡΡ Π³ΡΡΠΏΠΏΡ
- `removeGroup(groupId)` - ΡΠ΄Π°Π»ΠΈΡΡ Π³ΡΡΠΏΠΏΡ
---
### `skillsManager.js`
Π£ΠΏΡΠ°Π²Π»ΡΠ΅Ρ Π½Π°Π²ΡΠΊΠ°ΠΌΠΈ ΡΠΎΡΡΡΠ΄Π½ΠΈΠΊΠ° (Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅/ΡΠ΄Π°Π»Π΅Π½ΠΈΠ΅ ΡΠ΅ΡΠ΅Π· AJAX).
#### API
```javascript
import { initSkillsManager } from '{% static "js/components/skillsManager.js" %}';
const skillsManager = initSkillsManager({
blockId: 'skillsBlock',
formId: 'skillAddForm',
collapseId: 'skillsForm',
removeUrl: '/employees/1/skills/remove/'
});
```
#### ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ
- β
AJAX Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Π½Π°Π²ΡΠΊΠΎΠ²
- β
AJAX ΡΠ΄Π°Π»Π΅Π½ΠΈΠ΅ ΠΏΠΎ ΠΊΠ»ΠΈΠΊΡ Π½Π° ΡΠΈΠΏ
- β
ΠΠ½ΠΈΠΌΠ°ΡΠΈΡ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΡ/ΡΠ΄Π°Π»Π΅Π½ΠΈΡ
- β
ΠΠ°Π»ΠΈΠ΄Π°ΡΠΈΡ Π΄ΡΠ±Π»ΠΈΠΊΠ°ΡΠΎΠ²
#### ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΡΠΉ API
- `addSkill(name)` - Π΄ΠΎΠ±Π°Π²ΠΈΡΡ Π½Π°Π²ΡΠΊ (Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ Promise)
- `refresh()` - ΠΏΠ΅ΡΠ΅ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ
---
### `deptRoleEditor.js`
Π Π΅Π΄Π°ΠΊΡΠΎΡ ΡΠΎΠ»ΠΈ ΡΠΎΡΡΡΠ΄Π½ΠΈΠΊΠ° Π² ΠΎΡΠ΄Π΅Π»Π΅ Ρ Π²Π°Π»ΠΈΠ΄Π°ΡΠΈΠ΅ΠΉ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ.
#### API
```javascript
import { initDeptRoleEditor } from '{% static "js/components/deptRoleEditor.js" %}';
const deptRoleEditor = initDeptRoleEditor();
```
#### HTML ΡΠ°Π·ΠΌΠ΅ΡΠΊΠ°
```html
```
#### ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ
- β
ΠΠΊΡΠΈΠ²Π°ΡΠΈΡ ΠΊΠ½ΠΎΠΏΠΊΠΈ OK ΡΠΎΠ»ΡΠΊΠΎ ΠΏΡΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ
- β
ΠΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ tooltip'ΠΎΠ²
- β
ΠΠ½ΠΎΠΆΠ΅ΡΡΠ²Π΅Π½Π½ΡΠ΅ ΡΠΊΠ·Π΅ΠΌΠΏΠ»ΡΡΡ Π½Π° ΡΡΡΠ°Π½ΠΈΡΠ΅
#### ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΡΠΉ API
- `instances` - ΠΌΠ°ΡΡΠΈΠ² Π²ΡΠ΅Ρ
ΡΠ΅Π΄Π°ΠΊΡΠΎΡΠΎΠ²
- `refresh()` - ΠΏΠ΅ΡΠ΅ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ
---
### `headPicker.js`
Autocomplete Π΄Π»Ρ Π²ΡΠ±ΠΎΡΠ° ΡΡΠΊΠΎΠ²ΠΎΠ΄ΠΈΡΠ΅Π»Ρ ΠΎΡΠ΄Π΅Π»Π° ΠΏΠΎ ΠΈΠΌΠ΅Π½ΠΈ ΠΈΠ»ΠΈ email.
#### API
```javascript
import { initHeadPicker } from '{% static "js/components/headPicker.js" %}';
const headPicker = initHeadPicker({
selector: '[data-head-picker]'
});
```
#### HTML ΡΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ
```html
```
#### ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ
- β
ΠΠΎΠΈΡΠΊ ΠΏΠΎ ΠΈΠΌΠ΅Π½ΠΈ ΠΈ email
- β
ΠΠ°Π»ΠΈΠ΄Π°ΡΠΈΡ Π²ΡΠ±ΠΎΡΠ° ΠΏΡΠΈ submit
- β
ΠΠ²ΡΠΎΠ·Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ ΠΏΡΠΈ Π·Π°Π³ΡΡΠ·ΠΊΠ΅
- β
ΠΠ»Π°Π²ΠΈΠ°ΡΡΡΠ½Π°Ρ Π½Π°Π²ΠΈΠ³Π°ΡΠΈΡ (Enter, Escape)
---
### `roleManager.js`
Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΡΠΎΠ»ΡΠΌΠΈ ΠΈ ΠΏΡΠ°Π²Π°ΠΌΠΈ ΠΎΡΠ΄Π΅Π»ΠΎΠ² (ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅, ΡΠ΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅, ΡΠ΄Π°Π»Π΅Π½ΠΈΠ΅).
#### API
```javascript
import { initRoleManager } from '{% static "js/components/roleManager.js" %}';
const roleManager = initRoleManager();
```
#### ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ
- β
Autocomplete Π²ΡΠ±ΠΎΡΠ° ΡΠΎΠ»ΠΈ
- β
Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΠΏΡΠ°Π²Π°ΠΌΠΈ ΡΠ΅ΡΠ΅Π· ΡΠΈΠΏΡ
- β
Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Π½ΠΎΠ²ΠΎΠΉ ΡΠΎΠ»ΠΈ
- β
Π Π΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΡΡΡΠ΅ΡΡΠ²ΡΡΡΠ΅ΠΉ ΡΠΎΠ»ΠΈ
- β
Π£Π΄Π°Π»Π΅Π½ΠΈΠ΅ ΡΠΎΠ»ΠΈ Ρ ΠΏΠΎΠ΄ΡΠ²Π΅ΡΠΆΠ΄Π΅Π½ΠΈΠ΅ΠΌ
- β
ΠΠΎΠ΄Π°Π»Ρ Ρ Π²ΡΠ±ΠΎΡΠΎΠΌ ΠΏΡΠ°Π² (permissions)
#### ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΡΠΉ API
- `refresh()` - ΠΏΠ΅ΡΠ΅ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ
- `getSelectedRole()` - ΠΏΠΎΠ»ΡΡΠΈΡΡ Π²ΡΠ±ΡΠ°Π½Π½ΡΡ ΡΠΎΠ»Ρ
- `getEditPermissions()` - ΠΏΠΎΠ»ΡΡΠΈΡΡ Π²ΡΠ±ΡΠ°Π½Π½ΡΠ΅ ΠΏΡΠ°Π²Π° ΡΠ΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΡ
- `getCreatePermissions()` - ΠΏΠΎΠ»ΡΡΠΈΡΡ Π²ΡΠ±ΡΠ°Π½Π½ΡΠ΅ ΠΏΡΠ°Π²Π° ΡΠΎΠ·Π΄Π°Π½ΠΈΡ
---
### `calendarWidget.js`
ΠΠΎΠ»Π½ΠΎΡΡΠ½ΠΊΡΠΈΠΎΠ½Π°Π»ΡΠ½ΡΠΉ Π²ΠΈΠ΄ΠΆΠ΅Ρ ΠΊΠ°Π»Π΅Π½Π΄Π°ΡΡ Ρ FullCalendar.js.
#### API
```javascript
import { initCalendarWidget } from '{% static "js/components/calendarWidget.js" %}';
const widget = initCalendarWidget();
```
#### ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ
- β
ΠΡΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ ΡΠΎΠ±ΡΡΠΈΠΉ
- β
Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Π½ΠΎΠ²ΡΡ
ΡΠΎΠ±ΡΡΠΈΠΉ
- β
Π Π΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΡΠΎΠ±ΡΡΠΈΠΉ
- β
Drag & drop ΡΠΎΠ±ΡΡΠΈΠΉ
- β
ΠΠ½ΡΠ΅Π³ΡΠ°ΡΠΈΡ Ρ ΠΌΠΎΠ΄Π°Π»Π°ΠΌΠΈ
- β
AJAX ΡΠΎΡ
ΡΠ°Π½Π΅Π½ΠΈΠ΅
---
### `teamWheel.js`
ΠΠ½ΠΈΠΌΠΈΡΠΎΠ²Π°Π½Π½ΠΎΠ΅ ΠΊΠΎΠ»Π΅ΡΠΎ ΠΊΠΎΠΌΠ°Π½Π΄Ρ ΠΎΡΠ΄Π΅Π»Π° Ρ Π°Π²ΡΠΎΡΠΊΡΠΎΠ»Π»ΠΎΠΌ.
#### API
```javascript
import { initTeamWheel } from '{% static "js/components/teamWheel.js" %}';
const wheel = initTeamWheel({
wheelId: 'teamWheel-1',
dataSourceId: 'teamData-1',
fallback: {
avatar: '/static/img/default-avatar.png',
name: 'Π ΡΠΊΠΎΠ²ΠΎΠ΄ΠΈΡΠ΅Π»Ρ',
role: 'Π ΡΠΊΠΎΠ²ΠΎΠ΄ΠΈΡΠ΅Π»Ρ ΠΎΡΠ΄Π΅Π»Π°'
}
});
```
#### ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΡΠΉ API
- `start()` - Π·Π°ΠΏΡΡΡΠΈΡΡ Π°Π²ΡΠΎΡΠΊΡΠΎΠ»Π»
- `stop()` - ΠΎΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ Π°Π²ΡΠΎΡΠΊΡΠΎΠ»Π»
- `next()` - ΡΠ»Π΅Π΄ΡΡΡΠΈΠΉ ΡΡΠ°ΡΡΠ½ΠΈΠΊ
- `prev()` - ΠΏΡΠ΅Π΄ΡΠ΄ΡΡΠΈΠΉ ΡΡΠ°ΡΡΠ½ΠΈΠΊ
---
### `listFilter.js`
Π£Π½ΠΈΠ²Π΅ΡΡΠ°Π»ΡΠ½ΡΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ ΡΠΈΠ»ΡΡΡΠ°ΡΠΈΠΈ ΡΠΏΠΈΡΠΊΠΎΠ².
#### API
```javascript
import { ListFilter, createDataAttrMatcher } from '{% static "js/components/listFilter.js" %}';
new ListFilter({
listSelector: '#empList',
itemSelector: '.emp-row',
searchInputSelector: '#empFilter',
clearButtonSelector: '.ios-search-clear',
matchFn: createDataAttrMatcher(['name', 'depts']),
debounceMs: 300
});
```
#### ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ
- β
Debounced ΠΏΠΎΠΈΡΠΊ
- β
ΠΠ°ΡΡΠΎΠΌΠ½Π°Ρ ΡΡΠ½ΠΊΡΠΈΡ ΡΠΎΠΏΠΎΡΡΠ°Π²Π»Π΅Π½ΠΈΡ
- β
ΠΠΎΠ΄ΡΡΡΡ ΡΠ΅Π·ΡΠ»ΡΡΠ°ΡΠΎΠ²
- β
ΠΠ½ΠΎΠΏΠΊΠ° ΠΎΡΠΈΡΡΠΊΠΈ
- β
ΠΠ»Π°Π²ΠΈΠ°ΡΡΡΠ½ΡΠ΅ ΡΠΎΠ±ΡΡΠΈΡ
---
## π‘ ΠΡΠΈΠΌΠ΅ΡΡ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΡ
### ΠΡΠΈΠΌΠ΅Ρ 1: ΠΠ°Π·ΠΎΠ²Π°Ρ ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ Π² ΡΠ°Π±Π»ΠΎΠ½Π΅
```django
{% load static %}
```
### ΠΡΠΈΠΌΠ΅Ρ 2: ΠΠ½ΠΎΠΆΠ΅ΡΡΠ²Π΅Π½Π½Π°Ρ ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ
```django
```
### ΠΡΠΈΠΌΠ΅Ρ 3: Π Π°Π±ΠΎΡΠ° Ρ API ΠΌΠΎΠ΄ΡΠ»Ρ
```javascript
import { initEmployeeForm } from '/static/js/components/employeeFormHandler.js';
const form = initEmployeeForm();
// ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ ΠΏΠ΅ΡΠ΅Π΄ ΡΡ
ΠΎΠ΄ΠΎΠΌ ΡΠΎ ΡΡΡΠ°Π½ΠΈΡΡ
window.addEventListener('beforeunload', (e) => {
if (form && form.hasChanges()) {
e.preventDefault();
e.returnValue = 'Π£ Π²Π°Ρ Π΅ΡΡΡ Π½Π΅ΡΠΎΡ
ΡΠ°Π½ΡΠ½Π½ΡΠ΅ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ';
}
});
// ΠΡΠΎΠ³ΡΠ°ΠΌΠΌΠ½Π°Ρ ΠΎΡΠΏΡΠ°Π²ΠΊΠ° ΡΠΎΡΠΌΡ
document.getElementById('customSaveBtn').addEventListener('click', async () => {
if (form && form.validateForm()) {
try {
await form.submit();
alert('Π‘ΠΎΡ
ΡΠ°Π½Π΅Π½ΠΎ!');
} catch (error) {
alert('ΠΡΠΈΠ±ΠΊΠ°: ' + error.message);
}
}
});
```
---
## β
Best Practices
### 1. ΠΡΠ΅Π³Π΄Π° ΠΏΡΠΎΠ²Π΅ΡΡΠΉΡΠ΅ Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΠΎΠ΅ Π·Π½Π°ΡΠ΅Π½ΠΈΠ΅
```javascript
const component = initSomeComponent();
if (component) {
// ΠΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ ΡΡΠΏΠ΅ΡΠ½ΠΎ ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·ΠΈΡΠΎΠ²Π°Π½
component.doSomething();
} else {
// ΠΠ΅ΠΎΠ±Ρ
ΠΎΠ΄ΠΈΠΌΡΠ΅ ΡΠ»Π΅ΠΌΠ΅Π½ΡΡ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ
console.warn('Component not initialized');
}
```
### 2. ΠΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅ DOMContentLoaded
```javascript
document.addEventListener('DOMContentLoaded', () => {
// ΠΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ ΠΏΠΎΡΠ»Π΅ Π·Π°Π³ΡΡΠ·ΠΊΠΈ DOM
initAllComponents();
});
```
### 3. ΠΠΊΡΠΏΠΎΡΡΠΈΡΡΠΉΡΠ΅ Π² window Π΄Π»Ρ ΠΎΡΠ»Π°Π΄ΠΊΠΈ
```javascript
const component = initComponent();
if (component) {
window.debugComponent = component; // ΠΠΎΡΡΡΠΏ ΠΈΠ· ΠΊΠΎΠ½ΡΠΎΠ»ΠΈ
}
```
### 4. ΠΡΡΠΏΠΏΠΈΡΡΠΉΡΠ΅ ΠΈΠΌΠΏΠΎΡΡΡ
```javascript
// Utils
import { esc, norm } from '{% static "js/utils/stringUtils.js" %}';
import { debounce } from '{% static "js/utils/timing.js" %}';
// Components
import { initEmployeeForm } from '{% static "js/components/employeeFormHandler.js" %}';
import { initPositionManager } from '{% static "js/components/positionManager.js" %}';
```
### 5. ΠΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅ Π΄Π΅ΡΡΡΡΠΊΡΡΡΠΈΠ·Π°ΡΠΈΡ
```javascript
// β
Π₯ΠΎΡΠΎΡΠΎ
import { initComponent } from './component.js';
// β ΠΠ»ΠΎΡ
ΠΎ
import * as Component from './component.js';
const init = Component.initComponent;
```
### 6. ΠΠ±ΡΠ°Π±Π°ΡΡΠ²Π°ΠΉΡΠ΅ ΠΎΡΠΈΠ±ΠΊΠΈ
```javascript
try {
const result = await component.someAsyncOperation();
handleSuccess(result);
} catch (error) {
console.error('Operation failed:', error);
handleError(error);
}
```
### 7. ΠΡΠΈΡΠ°ΠΉΡΠ΅ ΡΠ΅ΡΡΡΡΡ
```javascript
// ΠΡΠ»ΠΈ ΠΌΠΎΠ΄ΡΠ»Ρ Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ destroy()
const component = initComponent();
// ΠΡΠΈ ΡΠ΄Π°Π»Π΅Π½ΠΈΠΈ ΡΠΎ ΡΡΡΠ°Π½ΠΈΡΡ
function cleanup() {
if (component && component.destroy) {
component.destroy();
}
}
```
---
## π§ ΠΡΠ»Π°Π΄ΠΊΠ°
### ΠΠΎΡΡΡΠΏ ΠΊ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΠ°ΠΌ ΠΈΠ· ΠΊΠΎΠ½ΡΠΎΠ»ΠΈ
ΠΡΠ΅ ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·ΠΈΡΠΎΠ²Π°Π½Π½ΡΠ΅ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΡ Π΄ΠΎΡΡΡΠΏΠ½Ρ ΡΠ΅ΡΠ΅Π· `window`:
```javascript
// Π ΠΊΠΎΠ½ΡΠΎΠ»ΠΈ Π±ΡΠ°ΡΠ·Π΅ΡΠ°
window.employeeForm.hasChanges() // true/false
window.positionManager.refresh() // ΠΏΠ΅ΡΠ΅ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΡ
window.likeHandler // ΠΎΠ±ΡΠ΅ΠΊΡ API
```
### ΠΠΎΠ³ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅
ΠΠΎΠ΄ΡΠ»ΠΈ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΡΡ `console.warn()` Π΄Π»Ρ ΠΏΡΠ΅Π΄ΡΠΏΡΠ΅ΠΆΠ΄Π΅Π½ΠΈΠΉ:
```javascript
// ΠΡΠ»ΠΈ ΡΠ»Π΅ΠΌΠ΅Π½ΡΡ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ
console.warn('EmployeeForm: required elements not found');
```
### ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΠΈ
```javascript
// ΠΡΠΎΠ²Π΅ΡΠΈΡΡ, Π±ΡΠ» Π»ΠΈ ΠΌΠΎΠ΄ΡΠ»Ρ ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·ΠΈΡΠΎΠ²Π°Π½
if (window.__employeeFormInitialized) {
console.log('EmployeeForm already initialized');
}
```
---
## π ΠΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠ°
ΠΡΠΈ Π²ΠΎΠ·Π½ΠΈΠΊΠ½ΠΎΠ²Π΅Π½ΠΈΠΈ Π²ΠΎΠΏΡΠΎΡΠΎΠ²:
1. ΠΡΠΎΠ²Π΅ΡΡΡΠ΅ JSDoc Π² ΠΈΡΡ
ΠΎΠ΄Π½ΠΎΠΌ ΠΊΠΎΠ΄Π΅ ΠΌΠΎΠ΄ΡΠ»Ρ
2. ΠΠ·ΡΡΠΈΡΠ΅ ΠΏΡΠΈΠΌΠ΅ΡΡ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΡ Π² ΡΠ°Π±Π»ΠΎΠ½Π°Ρ
3. ΠΡΠΎΠ²Π΅ΡΡΡΠ΅ ΠΊΠΎΠ½ΡΠΎΠ»Ρ Π±ΡΠ°ΡΠ·Π΅ΡΠ° Π½Π° ΠΏΡΠ΅Π΄ΡΠΏΡΠ΅ΠΆΠ΄Π΅Π½ΠΈΡ
4. ΠΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅ `window.componentName` Π΄Π»Ρ ΠΎΡΠ»Π°Π΄ΠΊΠΈ
---
**ΠΠ°ΡΠ° Π°ΠΊΡΡΠ°Π»ΠΈΠ·Π°ΡΠΈΠΈ**: 4 Π½ΠΎΡΠ±ΡΡ 2025 Π³.
**ΠΠ΅ΡΡΠΈΡ**: 1.0