diff --git a/projects/dom/index.js b/projects/dom/index.js
new file mode 100644
index 0000000..8247cfe
--- /dev/null
+++ b/projects/dom/index.js
@@ -0,0 +1,241 @@
+/* ДЗ 4 - работа с DOM */
+
+/*
+ Задание 1:
+
+ 1.1: Функция должна создать элемент с тегом DIV
+
+ 1.2: В созданный элемент необходимо поместить текст, переданный в параметр text
+
+ Пример:
+ createDivWithText('loftschool') // создаст элемент div, поместит в него 'loftschool' и вернет созданный элемент
+ */
+function createDivWithText(text) {
+ const element = document.createElement('div');
+ element.textContent = text;
+ return element;
+}
+
+/*
+ Задание 2:
+
+ Функция должна вставлять элемент, переданный в параметре what в начало элемента, переданного в параметре where
+
+ Пример:
+ prepend(document.querySelector('#one'), document.querySelector('#two')) // добавит элемент переданный первым аргументом в начало элемента переданного вторым аргументом
+ */
+function prepend(what, where) {
+ where.prepend(what);
+}
+
+/*
+ Задание 3:
+
+ 3.1: Функция должна перебрать все дочерние элементы узла, переданного в параметре where
+
+ 3.2: Функция должна вернуть массив, состоящий из тех дочерних элементов следующим соседом которых является элемент с тегом P
+
+ Пример:
+ Представим, что есть разметка:
+
+
+
+
+
+
+
+
+ findAllPSiblings(document.body) // функция должна вернуть массив с элементами div и span т.к. следующим соседом этих элементов является элемент с тегом P
+ */
+function findAllPSiblings(where) {
+ const needed_children = [];
+ for (const child of where.children) {
+ if (child.nextElementSibling && child.nextElementSibling.tagName === 'P') {
+ needed_children.push(child);
+ }
+ }
+ return needed_children;
+}
+
+/*
+ Задание 4:
+
+ Функция представленная ниже, перебирает все дочерние узлы типа "элемент" внутри узла переданного в параметре where и возвращает массив из текстового содержимого найденных элементов
+ Но похоже, что в код функции закралась ошибка и она работает не так, как описано.
+
+ Необходимо найти и исправить ошибку в коде так, чтобы функция работала так, как описано выше.
+
+ Пример:
+ Представим, что есть разметка:
+
+ привет
+ loftschool
+
+
+ findError(document.body) // функция должна вернуть массив с элементами 'привет' и 'loftschool'
+ */
+function findError(where) {
+ const result = [];
+
+ for (const child of where.children) {
+ result.push(child.textContent);
+ }
+
+ return result;
+}
+
+/*
+ Задание 5:
+
+ Функция должна перебрать все дочерние узлы элемента переданного в параметре where и удалить из него все текстовые узлы
+
+ Задачу необходимо решить без использования рекурсии, то есть можно не уходить вглубь дерева.
+ Так же будьте внимательны при удалении узлов, т.к. можно получить неожиданное поведение при переборе узлов
+
+ Пример:
+ После выполнения функции, дерево приветloftchool!!!
+ должно быть преобразовано в
+ */
+function deleteTextNodes(where) {
+ for (let i = 0; i < where.childNodes.length; i++) {
+ const el = where.childNodes[i];
+ if (el.nodeType === 3) {
+ where.removeChild(el);
+ i--;
+ }
+ }
+}
+
+/*
+ Задание 6:
+
+ Выполнить предыдущее задание с использование рекурсии - то есть необходимо заходить внутрь каждого дочернего элемента (углубляться в дерево)
+
+ Будьте внимательны при удалении узлов, т.к. можно получить неожиданное поведение при переборе узлов
+
+ Пример:
+ После выполнения функции, дерево привет
loftchool
!!!
+ должно быть преобразовано в
+ */
+function deleteTextNodesRecursive(where) {
+ for (let i = 0; i < where.childNodes.length; i++) {
+ const el = where.childNodes[i];
+ if (el.nodeType === 1) {
+ deleteTextNodesRecursive(el);
+ } else if (el.nodeType === 3) {
+ where.removeChild(el);
+ i--;
+ }
+ }
+}
+
+/*
+ Задание 7 *:
+
+ Необходимо собрать статистику по всем узлам внутри элемента переданного в параметре root и вернуть ее в виде объекта
+ Статистика должна содержать:
+ - количество текстовых узлов
+ - количество элементов каждого класса
+ - количество элементов каждого тега
+ Для работы с классами рекомендуется использовать classList
+ Постарайтесь не создавать глобальных переменных
+
+ Пример:
+ Для дерева привет! loftschool
+ должен быть возвращен такой объект:
+ {
+ tags: { DIV: 1, B: 2},
+ classes: { "some-class-1": 2, "some-class-2": 1 },
+ texts: 3
+ }
+ */
+function collectDOMStat(root) {
+ const stats = {
+ tags: {},
+ classes: {},
+ texts: 0,
+ };
+ function getting_stats(root) {
+ for (const child of root.childNodes) {
+ if (child.nodeType === 1) {
+ if (child.tagName in stats.tags) {
+ stats.tags[child.tagName]++;
+ } else {
+ stats.tags[child.tagName] = 1;
+ }
+
+ for (const className of child.classList) {
+ if (className in stats.classes) {
+ stats.classes[className]++;
+ } else {
+ stats.classes[className] = 1;
+ }
+ }
+ } else if (child.nodeType === 3) {
+ stats.texts++;
+ }
+ getting_stats(child);
+ }
+ }
+ getting_stats(root);
+ return stats;
+}
+
+/*
+ Задание 8 *:
+
+ 8.1: Функция должна отслеживать добавление и удаление элементов внутри элемента переданного в параметре where
+ Как только в where добавляются или удаляются элементы,
+ необходимо сообщать об этом при помощи вызова функции переданной в параметре fn
+
+ 8.2: При вызове fn необходимо передавать ей в качестве аргумента объект с двумя свойствами:
+ - type: типа события (insert или remove)
+ - nodes: массив из удаленных или добавленных элементов (в зависимости от события)
+
+ 8.3: Отслеживание должно работать вне зависимости от глубины создаваемых/удаляемых элементов
+
+ Рекомендуется использовать MutationObserver
+
+ Пример:
+ Если в where или в одного из его детей добавляется элемент div
+ то fn должна быть вызвана с аргументом:
+ {
+ type: 'insert',
+ nodes: [div]
+ }
+
+ ------
+
+ Если из where или из одного из его детей удаляется элемент div
+ то fn должна быть вызвана с аргументом:
+ {
+ type: 'remove',
+ nodes: [div]
+ }
+ */
+function observeChildNodes(where, fn) {
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ if (mutation.type === 'childList') {
+ fn({
+ type: mutation.addedNodes.length ? 'insert' : 'remove',
+ nodes: [
+ ...(mutation.addedNodes.length ? mutation.addedNodes : mutation.removedNodes),
+ ],
+ });
+ }
+ });
+ });
+ observer.observe(where, { childList: true, subtree: true });
+}
+
+export {
+ createDivWithText,
+ prepend,
+ findAllPSiblings,
+ findError,
+ deleteTextNodes,
+ deleteTextNodesRecursive,
+ collectDOMStat,
+ observeChildNodes,
+};
diff --git a/projects/dom/index.spec.js b/projects/dom/index.spec.js
new file mode 100644
index 0000000..0c71099
--- /dev/null
+++ b/projects/dom/index.spec.js
@@ -0,0 +1,240 @@
+import { randomValue } from '../../scripts/helper';
+import {
+ collectDOMStat,
+ createDivWithText,
+ deleteTextNodes,
+ deleteTextNodesRecursive,
+ findAllPSiblings,
+ findError,
+ observeChildNodes,
+ prepend,
+} from './index';
+
+function random(type) {
+ const result = randomValue(type);
+
+ if (type === 'string') {
+ return encodeURIComponent(result);
+ }
+
+ return result;
+}
+
+describe('ДЗ 4 - Работа с DOM', () => {
+ describe('createDivWithText', () => {
+ it('должна возвращать элемент с тегом DIV', () => {
+ const text = random('string');
+ const result = createDivWithText(text);
+
+ expect(result).toBeInstanceOf(Element);
+ expect(result.tagName).toBe('DIV');
+ });
+
+ it('должна добавлять текст в элемент', () => {
+ const text = random('string');
+ const result = createDivWithText(text);
+
+ expect(result.textContent).toBe(text);
+ });
+ });
+
+ describe('prepend', () => {
+ it('должна добавлять элемент в начало', () => {
+ const where = document.createElement('div');
+ const what = document.createElement('p');
+ const whereText = random('string');
+ const whatText = random('string');
+
+ where.innerHTML = `, ${whereText}!`;
+ what.textContent = whatText;
+
+ prepend(what, where);
+
+ expect(where.firstChild).toBe(what);
+ expect(where.innerHTML).toBe(`${whatText}
, ${whereText}!`);
+ });
+ });
+
+ describe('findAllPSiblings', () => {
+ it('должна возвращать массив с элементами, соседями которых являются P', () => {
+ const where = document.createElement('div');
+
+ where.innerHTML = '';
+ const result = findAllPSiblings(where);
+
+ expect(Array.isArray(result));
+ expect(result).toEqual([where.children[0], where.children[3]]);
+ });
+ });
+
+ describe('findError', () => {
+ it('должна возвращать массив из текстового содержимого элементов', () => {
+ const where = document.createElement('div');
+ const text1 = random('string');
+ const text2 = random('string');
+
+ where.innerHTML = ` ${text1}
, ${text2}
!!!`;
+ const result = findError(where);
+
+ expect(Array.isArray(result));
+ expect(result).toEqual([text1, text2]);
+ });
+ });
+
+ describe('deleteTextNodes', () => {
+ it('должна удалить все текстовые узлы', () => {
+ const where = document.createElement('div');
+
+ where.innerHTML = ` ${random('string')}${random('string')}`;
+ deleteTextNodes(where);
+
+ expect(where.innerHTML).toBe('');
+ });
+ });
+
+ describe('deleteTextNodesRecursive', () => {
+ it('должна рекурсивно удалить все текстовые узлы', () => {
+ const where = document.createElement('div');
+ const text1 = random('string');
+ const text2 = random('string');
+ const text3 = random('string');
+
+ where.innerHTML = ` ${text1}
${text2}
${text3}`;
+ deleteTextNodesRecursive(where);
+
+ expect(where.innerHTML).toBe('
');
+ });
+ });
+
+ describe('collectDOMStat', () => {
+ it('должна вернуть статистику по переданному дереву', () => {
+ const where = document.createElement('div');
+ const class1 = `class-${random('number')}`;
+ const class2 = `class-${random('number')}-${random('number')}`;
+ const text1 = random('string');
+ const text2 = random('string');
+ const stat = {
+ tags: { P: 1, B: 2 },
+ classes: { [class1]: 2, [class2]: 1 },
+ texts: 3,
+ };
+
+ where.innerHTML = `${text1} ${text2}
`;
+ const result = collectDOMStat(where);
+
+ expect(result).toEqual(stat);
+ });
+ });
+
+ describe('observeChildNodes', () => {
+ it('должна вызывать fn при добавлении элементов в указанный элемент', (done) => {
+ const where = document.createElement('div');
+ const fn = (info) => {
+ expect(typeof info === 'object' && info && info.constructor === 'Object');
+ expect(info.type).toBe(targetInfo.type);
+ expect(Array.isArray(info.nodes));
+ expect(info.nodes.length).toBe(targetInfo.nodes.length);
+ expect(targetInfo.nodes).toEqual(info.nodes);
+ done();
+ };
+ const elementToInsert = document.createElement('div');
+ const targetInfo = {
+ type: 'insert',
+ nodes: [elementToInsert],
+ };
+
+ document.body.appendChild(where);
+
+ observeChildNodes(where, fn);
+ where.appendChild(elementToInsert);
+
+ document.body.removeChild(where);
+ });
+
+ it('должна вызывать fn при добавлении множества элементов в указанный элемент', (done) => {
+ const where = document.createElement('div');
+ const fn = (info) => {
+ expect(typeof info === 'object' && info && info.constructor === 'Object');
+ expect(info.type).toBe(targetInfo.type);
+ expect(Array.isArray(info.nodes));
+ expect(info.nodes.length).toBe(targetInfo.nodes.length);
+ expect(targetInfo.nodes).toEqual(info.nodes);
+ done();
+ };
+ const elementToInsert1 = document.createElement('div');
+ const elementToInsert2 = document.createElement('div');
+ const elementToInsert3 = document.createElement('div');
+ const targetInfo = {
+ type: 'insert',
+ nodes: [elementToInsert1, elementToInsert2, elementToInsert3],
+ };
+ const fragment = new DocumentFragment();
+
+ document.body.appendChild(where);
+
+ fragment.appendChild(elementToInsert1);
+ fragment.appendChild(elementToInsert2);
+ fragment.appendChild(elementToInsert3);
+
+ observeChildNodes(where, fn);
+ where.appendChild(fragment);
+
+ document.body.removeChild(where);
+ });
+
+ it('должна вызывать fn при удалении элементов из указанного элемента', (done) => {
+ const where = document.createElement('div');
+ const fn = (info) => {
+ expect(typeof info === 'object' && info && info.constructor === 'Object');
+ expect(info.type).toBe(targetInfo.type);
+ expect(Array.isArray(info.nodes));
+ expect(info.nodes.length).toBe(targetInfo.nodes.length);
+ expect(targetInfo.nodes).toEqual(info.nodes);
+ done();
+ };
+ const elementToRemove = document.createElement('div');
+ const targetInfo = {
+ type: 'remove',
+ nodes: [elementToRemove],
+ };
+
+ document.body.appendChild(where);
+
+ where.appendChild(elementToRemove);
+ observeChildNodes(where, fn);
+ where.removeChild(elementToRemove);
+
+ document.body.removeChild(where);
+ });
+
+ it('должна вызывать fn при удалении множества элементов из указанного элемента', (done) => {
+ const where = document.createElement('div');
+ const fn = (info) => {
+ expect(typeof info === 'object' && info && info.constructor === 'Object');
+ expect(info.type).toBe(targetInfo.type);
+ expect(Array.isArray(info.nodes));
+ expect(info.nodes.length).toBe(targetInfo.nodes.length);
+ expect(targetInfo.nodes).toEqual(info.nodes);
+ done();
+ };
+ const elementToRemove1 = document.createElement('div');
+ const elementToRemove2 = document.createElement('div');
+ const elementToRemove3 = document.createElement('div');
+ const targetInfo = {
+ type: 'remove',
+ nodes: [elementToRemove1, elementToRemove2, elementToRemove3],
+ };
+
+ document.body.appendChild(where);
+
+ where.appendChild(elementToRemove1);
+ where.appendChild(elementToRemove2);
+ where.appendChild(elementToRemove3);
+
+ observeChildNodes(where, fn);
+ where.innerHTML = '';
+
+ document.body.removeChild(where);
+ });
+ });
+});