Skip to content

Commit

Permalink
📦
Browse files Browse the repository at this point in the history
  • Loading branch information
Keks committed Apr 8, 2024
1 parent 3202f5f commit 569bbd6
Show file tree
Hide file tree
Showing 8 changed files with 459 additions and 0 deletions.
70 changes: 70 additions & 0 deletions src/framework/api-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Класс для отправки запросов к серверу
*/
export default class ApiService {
/**
* @param {string} endPoint Адрес сервера
* @param {string} authorization Авторизационный токен
*/
constructor(endPoint, authorization) {
this._endPoint = endPoint;
this._authorization = authorization;
}

/**
* Метод для отправки запроса к серверу
* @param {Object} config Объект с настройками
* @param {string} config.url Адрес относительно сервера
* @param {string} [config.method] Метод запроса
* @param {string} [config.body] Тело запроса
* @param {Headers} [config.headers] Заголовки запроса
* @returns {Promise<Response>}
*/
async _load({
url,
method = 'GET',
body = null,
headers = new Headers(),
}) {
headers.append('Authorization', this._authorization);

const response = await fetch(
`${this._endPoint}/${url}`,
{method, body, headers},
);

try {
ApiService.checkStatus(response);
return response;
} catch (err) {
ApiService.catchError(err);
}
}

/**
* Метод для обработки ответа
* @param {Response} response Объект ответа
* @returns {Promise<JSON>}
*/
static parseResponse(response) {
return response.json();
}

/**
* Метод для проверки ответа
* @param {Response} response Объект ответа
*/
static checkStatus(response) {
if (!response.ok) {
throw new Error(`${response.status}: ${response.statusText}`);
}
}

/**
* Метод для обработки ошибок
* @param {Error} err Объект ошибки
*/
static catchError(err) {
throw err;
}
}
39 changes: 39 additions & 0 deletions src/framework/observable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Класс, реализующий паттерн Наблюдатель.
*/
export default class Observable {
/** @type {Set<observerCallback>} Множество функций типа observerCallback */
#observers = new Set();

/**
* Метод, позволяющий подписаться на событие
* @param {observerCallback} observer Функция, которая будет вызвана при наступлении события
*/
addObserver(observer) {
this.#observers.add(observer);
}

/**
* Метод, позволяющий отписаться от события
* @param {observerCallback} observer Функция, которую больше не нужно вызывать при наступлении события
*/
removeObserver(observer) {
this.#observers.delete(observer);
}

/**
* Метод для оповещения подписчиков о наступлении события
* @param {*} event Тип события
* @param {*} payload Дополнительная информация
*/
_notify(event, payload) {
this.#observers.forEach((observer) => observer(event, payload));
}
}

/**
* Функция, которая будет вызвана при наступлении события
* @callback observerCallback
* @param {*} event Тип события
* @param {*} [payload] Дополнительная информация
*/
80 changes: 80 additions & 0 deletions src/framework/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import AbstractView from './view/abstract-view.js';

/** @enum {string} Перечисление возможных позиций для отрисовки */
const RenderPosition = {
BEFOREBEGIN: 'beforebegin',
AFTERBEGIN: 'afterbegin',
BEFOREEND: 'beforeend',
AFTEREND: 'afterend',
};

/**
* Функция для создания элемента на основе разметки
* @param {string} template Разметка в виде строки
* @returns {HTMLElement} Созданный элемент
*/
function createElement(template) {
const newElement = document.createElement('div');
newElement.innerHTML = template;

return newElement.firstElementChild;
}

/**
* Функция для отрисовки элемента
* @param {AbstractView} component Компонент, который должен был отрисован
* @param {HTMLElement} container Элемент в котором будет отрисован компонент
* @param {string} place Позиция компонента относительно контейнера. По умолчанию - `beforeend`
*/
function render(component, container, place = RenderPosition.BEFOREEND) {
if (!(component instanceof AbstractView)) {
throw new Error('Can render only components');
}

if (container === null) {
throw new Error('Container element doesn\'t exist');
}

container.insertAdjacentElement(place, component.element);
}

/**
* Функция для замены одного компонента на другой
* @param {AbstractView} newComponent Компонент, который нужно показать
* @param {AbstractView} oldComponent Компонент, который нужно скрыть
*/
function replace(newComponent, oldComponent) {
if (!(newComponent instanceof AbstractView && oldComponent instanceof AbstractView)) {
throw new Error('Can replace only components');
}

const newElement = newComponent.element;
const oldElement = oldComponent.element;

const parent = oldElement.parentElement;

if (parent === null) {
throw new Error('Parent element doesn\'t exist');
}

parent.replaceChild(newElement, oldElement);
}

/**
* Функция для удаления компонента
* @param {AbstractView} component Компонент, который нужно удалить
*/
function remove(component) {
if (component === null) {
return;
}

if (!(component instanceof AbstractView)) {
throw new Error('Can remove only components');
}

component.element.remove();
component.removeElement();
}

export {RenderPosition, createElement, render, replace, remove};
49 changes: 49 additions & 0 deletions src/framework/ui-blocker/ui-blocker.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.ui-blocker {
display: none;
place-content: center;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
cursor: wait;
background-color: rgba(255, 255, 255, 0.5);
}

.ui-blocker::before {
content: "";
display: block;
border-radius: 50%;
border: 6px solid #4285F4;
box-sizing: border-box;
animation: sweep 1s linear alternate infinite,
rotate 0.8s linear infinite;
width: 65px;
height: 65px;
}

.ui-blocker--on {
display: grid;
}

@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@keyframes sweep {
0% {
clip-path: polygon(0% 0%, 0% 0%, 0% 0%, 50% 50%, 0% 0%, 0% 0%, 0% 0%);
}
50% {
clip-path: polygon(0% 0%, 0% 100%, 0% 100%, 50% 50%, 100% 0%, 100% 0%, 0% 0%);
}
100% {
clip-path: polygon(0% 0%, 0% 100%, 100% 100%, 50% 50%, 100% 100%, 100% 0%, 0% 0%);
}
}
74 changes: 74 additions & 0 deletions src/framework/ui-blocker/ui-blocker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import './ui-blocker.css';

/**
* Класс для блокировки интерфейса
*/
export default class UiBlocker {
/** @type {number} Время до блокировки интерфейса в миллисекундах */
#lowerLimit;

/** @type {number} Минимальное время блокировки интерфейса в миллисекундах */
#upperLimit;

/** @type {HTMLElement|null} Элемент, блокирующий интерфейс */
#element;

/** @type {number} Время вызова метода block */
#startTime;

/** @type {number} Время вызова метода unblock */
#endTime;

/** @type {number} Идентификатор таймера */
#timerId;

/**
* @param {Object} config Объект с настройками блокировщика
* @param {number} config.lowerLimit Время до блокировки интерфейса в миллисекундах. Если вызвать метод unblock раньше, то интерфейс заблокирован не будет
* @param {number} config.upperLimit Минимальное время блокировки в миллисекундах. Минимальная длительность блокировки
*/
constructor({lowerLimit, upperLimit}) {
this.#lowerLimit = lowerLimit;
this.#upperLimit = upperLimit;

this.#element = document.createElement('div');
this.#element.classList.add('ui-blocker');
document.body.append(this.#element);
}

/** Метод для блокировки интерфейса */
block() {
this.#startTime = Date.now();
this.#timerId = setTimeout(() => {
this.#addClass();
}, this.#lowerLimit);
}

/** Метод для разблокировки интерфейса */
unblock() {
this.#endTime = Date.now();
const duration = this.#endTime - this.#startTime;

if (duration < this.#lowerLimit) {
clearTimeout(this.#timerId);
return;
}

if (duration >= this.#upperLimit) {
this.#removeClass();
return;
}

setTimeout(this.#removeClass, this.#upperLimit - duration);
}

/** Метод, добавляющий CSS-класс элементу */
#addClass = () => {
this.#element.classList.add('ui-blocker--on');
};

/** Метод, убирающий CSS-класс с элемента */
#removeClass = () => {
this.#element.classList.remove('ui-blocker--on');
};
}
52 changes: 52 additions & 0 deletions src/framework/view/abstract-stateful-view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import AbstractView from './abstract-view.js';

/**
* Абстрактный класс представления с состоянием
*/
export default class AbstractStatefulView extends AbstractView {
/** @type {Object} Объект состояния */
_state = {};

/**
* Метод для обновления состояния и перерисовки элемента
* @param {Object} update Объект с обновлённой частью состояния
*/
updateElement(update) {
if (!update) {
return;
}

this._setState(update);

this.#rerenderElement();
}

/**
* Метод для восстановления обработчиков после перерисовки элемента
* @abstract
*/
_restoreHandlers() {
throw new Error('Abstract method not implemented: restoreHandlers');
}

/**
* Метод для обновления состояния
* @param {Object} update Объект с обновлённой частью состояния
*/
_setState(update) {
this._state = structuredClone({...this._state, ...update});
}

/** Метод для перерисовки элемента */
#rerenderElement() {
const prevElement = this.element;
const parent = prevElement.parentElement;
this.removeElement();

const newElement = this.element;

parent.replaceChild(newElement, prevElement);

this._restoreHandlers();
}
}
27 changes: 27 additions & 0 deletions src/framework/view/abstract-view.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.shake {
animation: shake 0.6s;
position: relative;
z-index: 10;
}

@keyframes shake {
0%,
100% {
transform: translateX(0);
}

10%,
30%,
50%,
70%,
90% {
transform: translateX(-5px);
}

20%,
40%,
60%,
80% {
transform: translateX(5px);
}
}
Loading

0 comments on commit 569bbd6

Please sign in to comment.