21 KiB
📘 Руководство по JavaScript модулям EUSRR
Версия: 1.0
Дата: 4 ноября 2025 г.
📚 Содержание
🏗️ Обзор архитектуры
Структура файлов
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-символы для безопасного вывода.
import { esc } from '{% static "js/utils/stringUtils.js" %}';
const userInput = '<script>alert("xss")</script>';
const safe = esc(userInput);
// => '<script>alert("xss")</script>'
norm(s: string): string
Нормализует строку для поиска (lowercase, trim).
import { norm } from '{% static "js/utils/stringUtils.js" %}';
const search = norm(' Hello World ');
// => 'hello world'
escAttr(s: string): string
Экранирует строку для использования в атрибутах.
const attr = escAttr('some "quoted" text');
// => 'some "quoted" text'
truncate(s: string, maxLen: number): string
Обрезает строку до указанной длины с добавлением "...".
const short = truncate('Very long text here', 10);
// => 'Very lon...'
getInitials(name: string): string
Получает инициалы из имени.
const initials = getInitials('Иван Петрович Сидоров');
// => 'ИПС'
timing.js
Утилиты для управления временем выполнения.
debounce(fn: Function, ms: number): Function
Откладывает выполнение функции до прекращения вызовов.
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
Ограничивает частоту вызова функции.
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<void>
Промисифицированная задержка.
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
Плавно прокручивает к элементу.
import { smoothScrollTo } from '{% static "js/utils/scroll.js" %}';
const target = document.getElementById('section');
smoothScrollTo(target, {
offset: 80, // Отступ сверху
duration: 500 // Длительность анимации
});
scrollToTop(duration: number = 300): void
Плавно прокручивает страницу наверх.
button.addEventListener('click', () => scrollToTop(500));
isElementInViewport(el: HTMLElement): boolean
Проверяет, виден ли элемент в окне просмотра.
if (isElementInViewport(element)) {
element.classList.add('animate');
}
🎨 Компоненты
navbarHeight.js
Устанавливает CSS-переменную с высотой навбара.
API
import { initNavbarHeight } from '{% static "js/components/navbarHeight.js" %}';
const navbarHeight = initNavbarHeight();
// Автоматически создаёт CSS переменную --navbar-height
Использование в CSS:
.content {
margin-top: var(--navbar-height);
}
likeHandler.js
Глобальный обработчик лайков для постов и комментариев.
API
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 разметка
<button class="like-btn"
data-post-id="{{ post.id }}"
data-liked="{% if post.i_liked %}1{% endif %}">
<i class="bi-heart{% if post.i_liked %}-fill{% endif %}"></i>
<span class="like-count">{{ post.likes_count }}</span>
</button>
Возвращаемый API
refresh()- переинициализация обработчиков
textareaAutogrow.js
Автоматически увеличивает высоту textarea при вводе текста.
API
import { initTextareaAutogrow } from '{% static "js/components/textareaAutogrow.js" %}';
const autogrow = initTextareaAutogrow({
selector: '[data-autogrow]',
minHeight: 40,
maxHeight: 300
});
HTML разметка
<textarea data-autogrow
data-max-height="200"
placeholder="Введите текст..."></textarea>
Возвращаемый API
refresh()- переинициализация для новых элементовdestroy()- отключение обработчиков
employeeFormHandler.js
Управляет формой сотрудника (валидация, изменения, AJAX).
API
import { initEmployeeForm } from '{% static "js/components/employeeFormHandler.js" %}';
const employeeForm = initEmployeeForm();
Возможности
- ✅ Отслеживание изменений полей
- ✅ Валидация перед отправкой
- ✅ Предупреждение о несохранённых изменениях
- ✅ AJAX отправка формы
- ✅ Обработка ошибок
Возвращаемый API
hasChanges()- проверка наличия измененийresetTracking()- сброс отслеживанияvalidateForm()- валидация формыisDirty- флаг изменений
positionManager.js
Управляет CRUD операциями с должностями через модальные окна.
API
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
import {
initPositionGroupPicker,
initPositionEditGroupPicker
} from '{% static "js/components/groupPickers.js" %}';
// Для формы создания должности
const createPicker = initPositionGroupPicker();
// Для формы редактирования должности
const editPicker = initPositionEditGroupPicker();
Возможности
- ✅ Выбор нескольких групп
- ✅ Отображение чипов выбранных групп
- ✅ Поиск по группам
- ✅ Синхронизация с hidden input'ами
employeeGroupsManager.js
Управляет персональными группами прав сотрудника.
API
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
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
import { initDeptRoleEditor } from '{% static "js/components/deptRoleEditor.js" %}';
const deptRoleEditor = initDeptRoleEditor();
HTML разметка
<div data-dept-role-editor>
<select name="role">...</select>
<button data-role-ok disabled>OK</button>
</div>
Возможности
- ✅ Активация кнопки OK только при изменении
- ✅ Инициализация tooltip'ов
- ✅ Множественные экземпляры на странице
Возвращаемый API
instances- массив всех редакторовrefresh()- переинициализация
headPicker.js
Autocomplete для выбора руководителя отдела по имени или email.
API
import { initHeadPicker } from '{% static "js/components/headPicker.js" %}';
const headPicker = initHeadPicker({
selector: '[data-head-picker]'
});
HTML требования
<div data-head-picker data-choices='[{"id":1,"name":"Иван","email":"ivan@mail.ru"}]'>
<input type="text" class="form-control" placeholder="Имя или email">
<input type="hidden" name="head_id">
<div class="dropdown-menu"></div>
</div>
Возможности
- ✅ Поиск по имени и email
- ✅ Валидация выбора при submit
- ✅ Автозаполнение при загрузке
- ✅ Клавиатурная навигация (Enter, Escape)
roleManager.js
Управление ролями и правами отделов (создание, редактирование, удаление).
API
import { initRoleManager } from '{% static "js/components/roleManager.js" %}';
const roleManager = initRoleManager();
Возможности
- ✅ Autocomplete выбора роли
- ✅ Управление правами через чипы
- ✅ Создание новой роли
- ✅ Редактирование существующей роли
- ✅ Удаление роли с подтверждением
- ✅ Модалы с выбором прав (permissions)
Возвращаемый API
refresh()- переинициализацияgetSelectedRole()- получить выбранную рольgetEditPermissions()- получить выбранные права редактированияgetCreatePermissions()- получить выбранные права создания
calendarWidget.js
Полнофункциональный виджет календаря с FullCalendar.js.
API
import { initCalendarWidget } from '{% static "js/components/calendarWidget.js" %}';
const widget = initCalendarWidget();
Возможности
- ✅ Отображение событий
- ✅ Создание новых событий
- ✅ Редактирование событий
- ✅ Drag & drop событий
- ✅ Интеграция с модалами
- ✅ AJAX сохранение
teamWheel.js
Анимированное колесо команды отдела с автоскроллом.
API
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
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: Базовая инициализация в шаблоне
{% load static %}
<script type="module">
import { initEmployeeForm } from '{% static "js/components/employeeFormHandler.js" %}';
document.addEventListener('DOMContentLoaded', () => {
const form = initEmployeeForm();
if (form) {
console.log('Форма инициализирована');
window.employeeForm = form; // Для отладки
}
});
</script>
Пример 2: Множественная инициализация
<script type="module">
import { initSkillsManager } from '{% static "js/components/skillsManager.js" %}';
import { initDeptRoleEditor } from '{% static "js/components/deptRoleEditor.js" %}';
import { smoothScrollTo } from '{% static "js/utils/scroll.js" %}';
document.addEventListener('DOMContentLoaded', () => {
const skills = initSkillsManager({ blockId: 'skillsBlock' });
const roles = initDeptRoleEditor();
// Скролл к форме на мобильных
const editWrap = document.getElementById('editFormWrap');
if (editWrap) {
editWrap.addEventListener('shown.bs.collapse', () => {
if (window.innerWidth < 768) {
smoothScrollTo(editWrap, { offset: 80 });
}
});
}
});
</script>
Пример 3: Работа с API модуля
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. Всегда проверяйте возвращаемое значение
const component = initSomeComponent();
if (component) {
// Компонент успешно инициализирован
component.doSomething();
} else {
// Необходимые элементы не найдены
console.warn('Component not initialized');
}
2. Используйте DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
// Инициализация после загрузки DOM
initAllComponents();
});
3. Экспортируйте в window для отладки
const component = initComponent();
if (component) {
window.debugComponent = component; // Доступ из консоли
}
4. Группируйте импорты
// 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. Используйте деструктуризацию
// ✅ Хорошо
import { initComponent } from './component.js';
// ❌ Плохо
import * as Component from './component.js';
const init = Component.initComponent;
6. Обрабатывайте ошибки
try {
const result = await component.someAsyncOperation();
handleSuccess(result);
} catch (error) {
console.error('Operation failed:', error);
handleError(error);
}
7. Очищайте ресурсы
// Если модуль возвращает destroy()
const component = initComponent();
// При удалении со страницы
function cleanup() {
if (component && component.destroy) {
component.destroy();
}
}
🔧 Отладка
Доступ к компонентам из консоли
Все инициализированные компоненты доступны через window:
// В консоли браузера
window.employeeForm.hasChanges() // true/false
window.positionManager.refresh() // переинициализация
window.likeHandler // объект API
Логирование
Модули используют console.warn() для предупреждений:
// Если элементы не найдены
console.warn('EmployeeForm: required elements not found');
Проверка инициализации
// Проверить, был ли модуль инициализирован
if (window.__employeeFormInitialized) {
console.log('EmployeeForm already initialized');
}
📞 Поддержка
При возникновении вопросов:
- Проверьте JSDoc в исходном коде модуля
- Изучите примеры использования в шаблонах
- Проверьте консоль браузера на предупреждения
- Используйте
window.componentNameдля отладки
Дата актуализации: 4 ноября 2025 г.
Версия: 1.0