diff --git a/src/components/__tests__/button.test.js b/src/components/__tests__/button.test.js new file mode 100644 index 0000000..997026f --- /dev/null +++ b/src/components/__tests__/button.test.js @@ -0,0 +1,290 @@ +import { JSDOM } from 'jsdom' +import { Button } from '@components/button' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement +global.MouseEvent = window.MouseEvent + +describe('Button', () => { + beforeEach(() => { + document.body.innerHTML = null + }) + + describe('Button', () => { + beforeEach(() => { + document.body.innerHTML = null + jest.spyOn(console, 'log').mockImplementation(() => {}) + }) + + describe('create()', () => { + describe('should create a button with a valid configuration', () => { + it('should create a rounded-square type button', () => { + const config = { + icon: 'plus', + id: 'add-quiz', + class_name: 'maker-button', + type: 'rounded-square', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + } + + const button = new Button(config) + const button_node = button.create() + document.body.appendChild(button_node) + + const element = document.getElementById('button-add-quiz') + element.click() + + expect(document.body.innerHTML).not.toHaveLength(0) + expect(button).toBeDefined() + expect(mock_log).toHaveBeenCalledWith('It worked!') + expect(element.id).toBe('button-add-quiz') + expect(element.className).toBe('maker-button button rounded-square-button') + expect(element.querySelector('[data-lucide="plus"]')).not.toBeNull() + }) + + describe('should create a slab type button', () => { + it('should create a button with icon and text', () => { + const mock_log = jest.spyOn(console, 'log').mockImplementation(() => {}) + + const config = { + icon: 'attachment', + text: 'Import File', + id: 'import-file', + class_name: 'maker-button', + type: 'slab', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + } + + const button = new Button(config) + const button_node = button.create() + document.body.appendChild(button_node) + + const element = document.getElementById('button-import-file') + element.click() + + expect(document.body.innerHTML).not.toHaveLength(0) + expect(button).toBeDefined() + expect(mock_log).toHaveBeenCalledWith('It worked!') + expect(element.id).toBe('button-import-file') + expect(element.className).toBe('maker-button button slab-button') + expect(element.querySelector('[data-lucide="attachment"]')).not.toBeNull() + }) + + it('should create a button with text only', () => { + const mock_log = jest.spyOn(console, 'log').mockImplementation(() => {}) + + const config = { + text: 'Import File', + id: 'import-file', + class_name: 'maker-button', + type: 'slab', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + } + + const button = new Button(config) + const button_node = button.create() + document.body.appendChild(button_node) + + const element = document.getElementById('button-import-file') + element.click() + + expect(document.body.innerHTML).not.toHaveLength(0) + expect(button).toBeDefined() + expect(mock_log).toHaveBeenCalledWith('It worked!') + expect(element.id).toBe('button-import-file') + expect(element.className).toBe('maker-button button slab-button') + }) + + it('should not create a button without icon and text', () => { + const mock_log = jest.spyOn(console, 'log').mockImplementation(() => {}) + + const config = { + id: 'import-file', + class_name: 'maker-button', + type: 'slab', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + } + + const button = new Button(config) + const button_node = button.create() + + expect(button_node).toBeFalsy() + }) + }) + + describe('should create a default type button', () => { + it('should create a button with icon and text', () => { + const mock_log = jest.spyOn(console, 'log').mockImplementation(() => {}) + + const config = { + icon: 'attachment', + text: 'Import File', + id: 'import-file', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + } + + const button = new Button(config) + const button_node = button.create() + document.body.appendChild(button_node) + + const element = document.getElementById('button-import-file') + element.click() + + expect(document.body.innerHTML).not.toHaveLength(0) + expect(button).toBeDefined() + expect(mock_log).toHaveBeenCalledWith('It worked!') + expect(element.id).toBe('button-import-file') + expect(element.className).toBe('maker-button button transparent-button') + expect(element.querySelector('[data-lucide="attachment"]')).not.toBeNull() + }) + + it('should create a button with text only', () => { + const mock_log = jest.spyOn(console, 'log').mockImplementation(() => {}) + + const config = { + text: 'Import File', + id: 'import-file', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + } + + const button = new Button(config) + const button_node = button.create() + document.body.appendChild(button_node) + + const element = document.getElementById('button-import-file') + element.click() + + expect(document.body.innerHTML).not.toHaveLength(0) + expect(button).toBeDefined() + expect(mock_log).toHaveBeenCalledWith('It worked!') + expect(element.id).toBe('button-import-file') + expect(element.className).toBe('maker-button button transparent-button') + }) + + it('should not create a button without icon and text', () => { + const mock_log = jest.spyOn(console, 'log').mockImplementation(() => {}) + + const config = { + id: 'import-file', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + } + + const button = new Button(config) + const button_node = button.create() + + expect(button_node).toBeFalsy() + }) + }) + }) + }) + + describe('remove()', () => { + it('should remove a button with a valid configuration', () => { + const mock_log = jest.spyOn(console, 'log').mockImplementation(() => {}) + + const config = { + icon: 'plus', + id: 'add-quiz', + class_name: 'maker-button', + type: 'rounded-square', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + } + + let button = new Button(config) + document.body.appendChild(button.create()) + + const element = document.getElementById('button-add-quiz') + element.click() + + expect(document.body.innerHTML).not.toHaveLength(0) + expect(button).toBeDefined() + expect(mock_log).toHaveBeenCalledWith('It worked!') + expect(element.id).toBe('button-add-quiz') + expect(element.className).toBe('maker-button button rounded-square-button') + expect(element.querySelector('[data-lucide="plus"]')).not.toBeNull() + + button.remove() + button = null + + expect(document.body.innerHTML).toHaveLength(0) + expect(button).toBeNull() + }) + + it('should return gracefully when id is not in configuration', () => { + const config = {} + const button = new Button(config).remove() + + expect(button).toBeFalsy() + }) + + it('should return gracefully when id is not found in the DOM', () => { + const config = { + id: 'add-quiz' + } + const button = new Button(config).remove() + + expect(button).toBeFalsy() + }) + }) +}) diff --git a/src/components/__tests__/checkbox.test.js b/src/components/__tests__/checkbox.test.js new file mode 100644 index 0000000..bac550d --- /dev/null +++ b/src/components/__tests__/checkbox.test.js @@ -0,0 +1,218 @@ + +import { JSDOM } from 'jsdom' +import { Checkbox } from '@components/checkbox' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement +global.MouseEvent = window.MouseEvent + +const localStorageMock = (() => { + let store = {} + + return { + getItem(key) { + return store[key] + }, + + setItem(key, value) { + store[key] = value + }, + + clear() { + store = {} + }, + + removeItem(key) { + delete store[key] + }, + + getAll() { + return store + } + } +})() + +Object.defineProperty(window, 'localStorage', { value: localStorageMock }) +global.localStorage = window.localStorage + +describe('Checkbox', () => { + const setLocalStorage = (id, data) => { + window.localStorage.setItem(id, JSON.stringify([data])) + } + + const removeLocalStorage = (id, data) => { + let storedData = JSON.parse(window.localStorage.getItem(id)) || [] + const index = storedData.indexOf(data) + if (index > -1) { + storedData.splice(index, 1) + } + window.localStorage.setItem(id, JSON.stringify(storedData)) + } + + beforeEach(() => { + document.body.innerHTML = null + localStorage.clear() + }) + + describe('create()', () => { + it('should create a checkbox with valid configurations', () => { + const config = { + id: 'en-file-checkbox-1', + class_name: 'en-checkboxes', + target_id: 'en-qp-1', + group_name: 'en-qp' + } + + const checkbox = new Checkbox(config).create() + document.body.appendChild(checkbox) + + expect(checkbox).toBeDefined() + expect(document.body).not.toBeNull() + expect(document.body.querySelector('#en-file-checkbox-1').id).toBe('en-file-checkbox-1') + expect(document.body.querySelector('#en-file-checkbox-1').className).toBe( + 'en-checkboxes checkboxes' + ) + expect(document.body.querySelector('#en-file-checkbox-1').dataset.state).toBe('false') + expect(document.body.querySelector('#en-file-checkbox-1').dataset.targetId).toBe('en-qp-1') + expect(document.body.querySelector('#en-file-checkbox-1').dataset.groupName).toBe( + 'en-qp-checkboxes' + ) + expect( + document.body.querySelector('#en-file-checkbox-1').querySelector('i').dataset.lucide + ).toBe('square') + }) + + it('should change state when clicked', () => { + const config = { + id: 'en-file-checkbox-1', + class_name: 'en-checkboxes', + target_id: 'en-qp-1', + group_name: 'en-qp' + } + + const checkbox = new Checkbox(config).create() + document.body.appendChild(checkbox) + + const element = document.getElementById('en-file-checkbox-1') + + expect(element.dataset.state).toBe('false') + + element.click() + expect(element.dataset.state).toBe('true') + + element.click() + expect(element.dataset.state).toBe('false') + + element.click() + expect(element.dataset.state).toBe('true') + + element.click() + expect(element.dataset.state).toBe('false') + }) + + it('should change icon when clicked', () => { + const config = { + id: 'en-file-checkbox-1', + class_name: 'en-checkboxes', + target_id: 'en-qp-1', + group_name: 'en-qp' + } + + const checkbox = new Checkbox(config).create() + document.body.appendChild(checkbox) + + const element = document.getElementById('en-file-checkbox-1') + const icon = element.querySelector('i') + + expect(icon.dataset.lucide).toBe('square') + + element.click() + expect(icon.dataset.lucide).toBe('square-check') + + element.click() + expect(icon.dataset.lucide).toBe('square') + + element.click() + expect(icon.dataset.lucide).toBe('square-check') + + element.click() + expect(icon.dataset.lucide).toBe('square') + }) + + it('should be true when icon is square-check, and false when icon is square', () => { + const config = { + id: 'en-file-checkbox-1', + class_name: 'en-checkboxes', + target_id: 'en-qp-1', + group_name: 'en-qp' + } + + const checkbox = new Checkbox(config).create() + document.body.appendChild(checkbox) + + const element = document.getElementById('en-file-checkbox-1') + const icon = element.querySelector('i') + + expect(element.dataset.state).toBe('false') + expect(icon.dataset.lucide).toBe('square') + + element.click() + expect(element.dataset.state).toBe('true') + expect(icon.dataset.lucide).toBe('square-check') + + element.click() + expect(element.dataset.state).toBe('false') + expect(icon.dataset.lucide).toBe('square') + + element.click() + expect(element.dataset.state).toBe('true') + expect(icon.dataset.lucide).toBe('square-check') + + element.click() + expect(element.dataset.state).toBe('false') + expect(icon.dataset.lucide).toBe('square') + }) + + it('should manage target id in localStorage when state changes', () => { + const config = { + id: 'en-file-checkbox-1', + class_name: 'en-checkboxes', + target_id: 'en-qp-1', + group_name: 'en-qp' + } + + const checkbox = new Checkbox(config).create() + document.body.appendChild(checkbox) + + const element = document.getElementById('en-file-checkbox-1') + + element.click() + setLocalStorage(`${config.group_name}-checkboxes`, config.target_id) + expect(JSON.parse(localStorage.getItem('en-qp-checkboxes'))).toEqual(['en-qp-1']) + + element.click() + removeLocalStorage(`${config.group_name}-checkboxes`, config.target_id) + expect(JSON.parse(localStorage.getItem('en-qp-checkboxes'))).toEqual([]) + }) + }) + + describe('remove()', () => { + it('should remove a checkbox with valid configurations', () => { + const config = { + id: 'en-file-checkbox-1', + class_name: 'en-checkboxes', + target_id: 'en-qp-1', + group_name: 'en-qp' + } + + const checkbox = new Checkbox(config).create() + document.body.appendChild(checkbox) + + checkbox.remove() + + expect(document.body.innerHTML).toBe('') + }) + }) +}) diff --git a/src/components/__tests__/container.test.js b/src/components/__tests__/container.test.js new file mode 100644 index 0000000..732d77a --- /dev/null +++ b/src/components/__tests__/container.test.js @@ -0,0 +1,93 @@ +import { JSDOM } from 'jsdom' +import { Container } from '@components/container' +import { Button } from '@components/button' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement + +describe('Container', () => { + beforeEach(() => { + document.body.innerHTML = '' + }) + + it('should create the container when initiated and call the create method', () => { + const config = { + id: 'test-container', + class_name: 'container-class' + } + + const containerInstance = new Container(config) + const containerElement = containerInstance.create() + + document.body.appendChild(containerElement) + const container = document.getElementById('test-container') + + expect(document.body.innerHTML).not.toBeNull() + expect(containerElement).toBeDefined() + expect(container).not.toBeNull() + expect(container.id).toBe('test-container') + expect(container.className).toBe('container-class') + }) + + it('should append plain text to the container', () => { + const config = { + id: 'test-container', + class_name: 'container-class', + text: 'Hello, world!' + } + + const containerInstance = new Container(config) + const containerElement = containerInstance.create() + document.body.appendChild(containerElement) + + expect(containerElement.textContent).toBe('Hello, world!') + }) + + it('should append any external element to the container', () => { + const config = { + icon: 'plus', + id: 'add-quiz', + class_name: 'maker-button', + type: 'rounded-square', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + } + + const button = new Button(config).create() + + const another_config = { + id: 'test-container', + class_name: 'container-class', + elements: [button] + } + + const containerInstance = new Container(another_config) + const containerElement = containerInstance.create() + document.body.appendChild(containerElement) + + expect(containerElement.querySelector('#button-add-quiz')).not.toBeNull() + }) + + it('should remove the container when the remove method is called', () => { + const config = { + id: 'test-container', + class_name: 'container-class' + } + + const containerInstance = new Container(config) + const containerElement = containerInstance.create() + document.body.appendChild(containerElement) + + containerInstance.remove() + + expect(document.getElementById('test-container')).toBeNull() + }) +}) diff --git a/src/components/__tests__/modal.test.js b/src/components/__tests__/modal.test.js new file mode 100644 index 0000000..1c413a9 --- /dev/null +++ b/src/components/__tests__/modal.test.js @@ -0,0 +1,179 @@ +import { JSDOM } from 'jsdom' +import { Modal } from '@components/modal' +import { Button } from '@components/button' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement +global.MouseEvent = window.MouseEvent + +describe('Modal', () => { + beforeEach(() => { + document.body.innerHTML = null + }) + + describe('create()', () => { + it('should create modal with valid configuration', () => { + const button_1 = new Button({ + icon: 'copy', + id: 'duplicate-quiz', + text: 'Duplicate', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + }).create() + + const button_2 = new Button({ + icon: 'heart', + text: 'Favorite', + id: 'add-quiz-2', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked again!') + } + } + ] + }).create() + + const config = { + id: 'modal', + title: 'Sample Modal', + icon: 'heart', + buttons: [button_1, button_2] + } + + const modal = new Modal(config) + + document.body.appendChild(modal.create()) + + expect(document.body.innerHTML).not.toBeNull() + expect(document.body.querySelector('#modal')).toBeTruthy() + expect(document.body.querySelector('#modal').querySelector('#modal-button-0')).toBeTruthy() + expect( + document.body.querySelector('#modal').querySelector('#modal-button-0').querySelector('i') + .dataset.lucide + ).toBe('copy') + expect(document.body.querySelector('#modal').querySelector('#modal-button-1')).toBeTruthy() + expect( + document.body.querySelector('#modal').querySelector('#modal-button-1').querySelector('i') + .dataset.lucide + ).toBe('heart') + }) + + it('should buttons function well with valid configuration', () => { + let log_spy = jest.spyOn(console, 'log') + + const button_1 = new Button({ + icon: 'copy', + id: 'duplicate-quiz', + text: 'Duplicate', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + }).create() + + const button_2 = new Button({ + icon: 'heart', + text: 'Favorite', + id: 'add-quiz-2', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked again!') + } + } + ] + }).create() + + const config = { + id: 'modal', + title: 'Sample Modal', + icon: 'heart', + buttons: [button_1, button_2] + } + + const modal = new Modal(config) + + document.body.appendChild(modal.create()) + + const first = document.body.querySelector('#modal').querySelector('#modal-button-0') + const second = document.body.querySelector('#modal').querySelector('#modal-button-1') + + first.click() + expect(log_spy).toHaveBeenCalledWith('It worked!') + + second.click() + expect(log_spy).toHaveBeenCalledWith('It worked again!') + + log_spy = null + }) + }) + + describe('remove()', () => { + it('should remove a modal with valid configuration', () => { + const button_1 = new Button({ + icon: 'copy', + id: 'duplicate-quiz', + text: 'Duplicate', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + }).create() + + const button_2 = new Button({ + icon: 'heart', + text: 'Favorite', + id: 'add-quiz-2', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked again!') + } + } + ] + }).create() + + const config = { + id: 'modal', + title: 'Sample Modal', + icon: 'heart', + buttons: [button_1, button_2] + } + + const modal = new Modal(config) + + document.body.appendChild(modal.create()) + + modal.remove() + + expect(document.body.innerHTML).toBe('') + expect(document.body.querySelector('#modal')).toBeNull() + }) + }) +}) diff --git a/src/components/__tests__/page.test.js b/src/components/__tests__/page.test.js new file mode 100644 index 0000000..b253ec3 --- /dev/null +++ b/src/components/__tests__/page.test.js @@ -0,0 +1,300 @@ +import { JSDOM } from 'jsdom' +import { Page } from '@components/page' +import { Modal } from '@components/modal' +import { Button } from '@components/button' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement + +describe('Page', () => { + beforeEach(() => { + document.body.innerHTML = null + }) + + describe('create()', () => { + it('should create with valid configuration', () => { + const config = { + id: 'maker', + elements: { + header: [], + body: [] + }, + z_index: 4 + } + + const page = new Page(config) + + document.body.appendChild(page.create()) + + expect(document.body.innerHTML).not.toBeNull() + expect(document.body.querySelector('#page-maker')).toBeTruthy() + expect( + document.body + .querySelector('#page-maker') + .getElementsByClassName('.page-header') + ).toBeTruthy() + expect( + document.body + .querySelector('#page-maker') + .getElementsByClassName('.page-body') + ).toBeTruthy() + }) + + it('should create exit button when the z-index is less than to 0 and more than 6', () => { + const config = { + id: 'maker', + elements: { + header: [], + body: [] + }, + z_index: 6 + } + + const page = new Page(config) + + document.body.appendChild(page.create()) + + const headers = Array.from( + document.body + .querySelector('#page-maker') + .getElementsByClassName('page-header') + ) + headers.forEach((header) => { + expect(header.querySelector('#button-exit-page-6')).not.toBeNull() + }) + }) + + it('should not create exit button when the z-index is equal to 0 and less than 6', () => { + const config = { + id: 'maker', + elements: { + header: [], + body: [] + }, + z_index: 4 + } + + const page = new Page(config) + + document.body.appendChild(page.create()) + + const headers = Array.from( + document.body + .querySelector('#page-maker') + .getElementsByClassName('page-header') + ) + headers.forEach((header) => { + expect(header.querySelector('#exit-button-page-4')).toBeNull() + }) + }) + + it('should create with elements added in config', () => { + let button_1 = new Button({ + icon: 'badge', + id: 'add-quiz-1', + text: 'Badge', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('Hi, I am working!') + } + } + ] + }) + + let button_2 = new Button({ + icon: 'cookie', + text: 'Cookie', + id: 'add-quiz-2', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('You are happy and very genius!') + } + } + ] + }) + + let config_modal = { + id: 'modal', + title: 'Sample Modal', + icon: 'heart', + buttons: [button_1.create(), button_2.create()] + } + + let button_3 = new Button({ + icon: 'anchor', + text: 'Anchor', + id: 'add-quiz-3', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('Anchor') + } + } + ] + }) + + let button_4 = new Button({ + icon: 'grid-2x2-check', + text: 'Grid Check', + id: 'add-quiz-4', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('Check') + } + } + ] + }) + + let modal = new Modal(config_modal) + + let config_page = { + id: 'maker', + elements: { + header: [button_4], + body: [modal, button_3] + }, + z_index: 4 + } + + let page = new Page(config_page) + document.body.appendChild(page.create()) + + expect(document.body.innerHTML).not.toBeNull() + expect(document.body.querySelector('#page-maker')).toBeTruthy() + expect( + document.body + .querySelector('#page-maker') + .getElementsByClassName('.page-header') + ).not.toBeNull() + + const headers = Array.from( + document.body + .querySelector('#page-maker') + .getElementsByClassName('.page-header') + ) + + headers.forEach((header) => { + expect(header.getElementById('button-exit-page-4')).not.toBeNull() + expect(header.getElementById('button-add-quiz-4')).not.toBeNull() + }) + + expect( + document.body + .querySelector('#page-maker') + .getElementsByClassName('.page-body') + ).toBeTruthy() + + const bodies = Array.from( + document.body + .querySelector('#page-maker') + .getElementsByClassName('page-body') + ) + bodies.forEach((body) => { + expect(body.querySelector('#modal')).not.toBeNull() + expect(body.querySelector('#button-add-quiz-3')).not.toBeNull() + }) + }) + }) + + describe('remove()', () => { + it('should remove page with valid config', () => { + let button_1 = new Button({ + icon: 'badge', + id: 'add-quiz-3', + text: 'Badge', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('Hi, I am working!') + } + } + ] + }).create() + + let button_2 = new Button({ + icon: 'cookie', + text: 'Cookie', + id: 'add-quiz-4', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('You are happy and very genius!') + } + } + ] + }).create() + + let config_modal = { + id: 'modal', + title: 'Sample Modal', + icon: 'heart', + buttons: [button_1, button_2] + } + + let button_3 = new Button({ + icon: 'anchor', + text: 'Anchor', + id: 'add-quiz-4', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('Anchor') + } + } + ] + }) + + let button_4 = new Button({ + icon: 'grid-2x2-check', + text: 'Grid Check', + id: 'add-quiz-5', + class_name: 'maker-button', + events: [ + { + event_name: 'click', + func: () => { + console.log('Check') + } + } + ] + }) + + let modal = new Modal(config_modal) + + let config_page = { + id: 'maker', + elements: { + header: [button_4], + body: [modal, button_3] + }, + z_index: 4 + } + + let page = new Page(config_page) + document.body.appendChild(page.create()) + page.remove() + + expect(document.body.innerHTML).toBe('') + expect(document.body.querySelector('#page-maker')).toBeNull() + }) + }) +}) diff --git a/src/components/__tests__/snackbar.test.js b/src/components/__tests__/snackbar.test.js new file mode 100644 index 0000000..2063bd2 --- /dev/null +++ b/src/components/__tests__/snackbar.test.js @@ -0,0 +1,39 @@ +import { JSDOM } from 'jsdom' +import { Snackbar } from '@components/snackbar' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement +global.requestAnimationFrame = (callback) => { + setTimeout(callback, 1000 / 60) +} + +describe('Snackbar', () => { + beforeEach(() => { + document.body.innerHTML = null + }) + + describe('renderThenRemove', () => { + it('should create a Snackbar instance with valid config', () => { + const config = { message: 'The quiz pack is successfully imported!' } + + const snackbar = new Snackbar(config) + + expect(snackbar).toBeInstanceOf(Snackbar) + expect(snackbar.renderThenRemove).toBeInstanceOf(Function) + }) + + it('should render and remove snackbar with valid config', () => { + const config = { message: 'The quiz pack is successfully imported!' } + + const snackbar = new Snackbar(config) + snackbar.renderThenRemove() + + expect(document.body).not.toBeNull() + expect(document.body.querySelector('#snackbar').textContent).toBe( + 'The quiz pack is successfully imported!' + ) + }) + }) +}) diff --git a/src/components/__tests__/tab.test.js b/src/components/__tests__/tab.test.js new file mode 100644 index 0000000..27294fb --- /dev/null +++ b/src/components/__tests__/tab.test.js @@ -0,0 +1,164 @@ +import { JSDOM } from 'jsdom' +import { Tab } from '@components/tab' +import { Button } from '@components/button' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement +global.MouseEvent = window.MouseEvent + +describe('Tab', () => { + beforeEach(() => { + document.body.innerHTML = null + }) + + describe('create()', () => { + it('should create tab with valid configuration', () => { + const button_1 = new Button({ + icon: 'cookie', + id: 'add-quiz', + class_name: 'maker-button', + type: 'rounded-square', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + }).create() + + const button_2 = new Button({ + icon: 'heart', + id: 'add-quiz-2', + class_name: 'maker-button', + type: 'rounded-square', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked again!') + } + } + ] + }).create() + + const buttons = [button_1, button_2] + + const tab = new Tab({ buttons: buttons }) + + document.body.appendChild(tab.create()) + + expect(document.body.innerHTML).not.toBeNull() + expect(document.body.querySelector('#tab')).toBeTruthy() + expect(document.body.querySelector('#tab').querySelector('#tab-button-0')).toBeTruthy() + expect( + document.body.querySelector('#tab').querySelector('#tab-button-0').querySelector('i') + .dataset.lucide + ).toBe('cookie') + expect(document.body.querySelector('#tab').querySelector('#tab-button-1')).toBeTruthy() + expect( + document.body.querySelector('#tab').querySelector('#tab-button-1').querySelector('i') + .dataset.lucide + ).toBe('heart') + }) + + it('should buttons function well with valid configuration', () => { + let log_spy = jest.spyOn(console, 'log') + + const button_1 = new Button({ + icon: 'cookie', + id: 'add-quiz', + class_name: 'maker-button', + type: 'rounded-square', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + }).create() + + const button_2 = new Button({ + icon: 'heart', + id: 'add-quiz-2', + class_name: 'maker-button', + type: 'rounded-square', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked again!') + } + } + ] + }).create() + + const buttons = [button_1, button_2] + + const tab = new Tab({ buttons: buttons }) + + document.body.appendChild(tab.create()) + + const first = document.body.querySelector('#tab').querySelector('#tab-button-0') + const second = document.body.querySelector('#tab').querySelector('#tab-button-1') + + first.click() + expect(log_spy).toHaveBeenCalledWith('It worked!') + + second.click() + expect(log_spy).toHaveBeenCalledWith('It worked again!') + + log_spy = null + }) + }) + + describe('remove()', () => { + it('should remove a tab with valid configuration', () => { + const button_1 = new Button({ + icon: 'cookie', + id: 'add-quiz', + class_name: 'maker-button', + type: 'rounded-square', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked!') + } + } + ] + }).create() + + const button_2 = new Button({ + icon: 'heart', + id: 'add-quiz-2', + class_name: 'maker-button', + type: 'rounded-square', + events: [ + { + event_name: 'click', + func: () => { + console.log('It worked again!') + } + } + ] + }).create() + + const buttons = [button_1, button_2] + + const tab = new Tab({ buttons: buttons }) + + document.body.appendChild(tab.create()) + + tab.remove() + + expect(document.body.innerHTML).toBe('') + expect(document.body.querySelector('#tab')).toBeNull() + }) + }) +}) diff --git a/src/components/__tests__/textarea.test.js b/src/components/__tests__/textarea.test.js new file mode 100644 index 0000000..176328e --- /dev/null +++ b/src/components/__tests__/textarea.test.js @@ -0,0 +1,85 @@ +import { JSDOM } from 'jsdom' +import { Textarea } from '@components/textarea' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement + +describe('Textarea', () => { + beforeEach(() => { + document.body.innerHTML = null + }) + + describe('create()', () => { + it('should create with valid config', () => { + const config = { + id: 'question-1', + class_name: 'question', + placeholder: 'Question', + text: '', + readonly: false + } + + const textarea = new Textarea(config) + + document.body.appendChild(textarea.create()) + + expect(document.body.innerHTML).not.toHaveLength(0) + expect(textarea).toBeDefined() + expect(document.querySelector('#textarea-question-1')).not.toBeNull() + expect(document.querySelector('#textarea-question-1').placeholder).toBe( + 'Question' + ) + expect(document.querySelector('#textarea-question-1').value).toBe('') + expect(document.querySelector('#textarea-question-1').readOnly).toBe( + false + ) + }) + }) + + describe('lock() and unlock()', () => { + it('should lock and unlock existing textarea', () => { + const config = { + id: 'question-1', + class_name: 'question', + placeholder: 'Question', + text: '', + readonly: false + } + + const textarea = new Textarea(config) + + document.body.appendChild(textarea.create()) + + expect(document.querySelector('#textarea-question-1').readOnly).toBe( + false + ) + textarea.lock() + expect(document.querySelector('#textarea-question-1').readOnly).toBe(true) + textarea.unlock() + expect(document.querySelector('#textarea-question-1').readOnly).toBe( + false + ) + }) + }) + + describe('remove()', () => { + it('should remove with valid config', () => { + const config = { + id: 'question-1', + class_name: 'question', + placeholder: 'Question', + text: '', + readonly: false + } + + const textarea = new Textarea(config) + + document.body.appendChild(textarea.create()) + textarea.remove() + + expect(document.body.innerHTML).toBe('') + }) + }) +}) diff --git a/src/components/base-component.js b/src/components/base-component.js new file mode 100644 index 0000000..7ed508e --- /dev/null +++ b/src/components/base-component.js @@ -0,0 +1,106 @@ +import { setAttributes } from '@components/utilities/set-attributes' +import { sanitizeValue } from '@utilities/sanitize-value' +import { setProperties } from './utilities/set-properties' + +/** + * Creates a base component with utility methods for creating container elements, icons, text elements, and removing elements from the DOM. + * Utilizes the `sanitizeValue` function to validate inputs and the `setAttributes` function to set attributes on elements. + */ +class BaseComponent { + /** + * Initializes the base component instance with the provided configuration. + * + * @param {Object} config - The configuration object. + */ + constructor(config) { + this.config = sanitizeValue(config, 'object') || {} + } + + /** + * Creates a container element with the specified tag name and attributes. + * + * @param {string} tagName - The tag name for the container element. + * @param {object} attributes - The attributes to be set on the container element. + * @param {object} [booleanAttributes] - The boolean attributes to be set on the container element. + * @returns {HTMLElement | null} The created container element or null if invalid inputs. + */ + _createContainer(tagName, attributes, booleanAttributes) { + const isTagName = sanitizeValue(tagName, 'string') + const isAttributes = sanitizeValue(attributes, 'object') + + const container = document.createElement(tagName) + + if (booleanAttributes) { + sanitizeValue(booleanAttributes, 'object') + setProperties(container, booleanAttributes) + } + + if (!isTagName || !isAttributes) { + return null + } + + setAttributes(container, attributes) + return container + } + + /** + * Creates an icon element with the specified icon name. + * + * @param {string} iconName - The name of the icon to be displayed. + * @returns {HTMLElement | null} The created icon element or null if the icon name is invalid. + */ + _createIcon(iconName) { + const isIconName = sanitizeValue(iconName, 'string') + + if (!isIconName) { + return null + } + + const iconElement = document.createElement('i') + setAttributes(iconElement, { 'data-lucide': iconName }) + return iconElement + } + + /** + * Creates a text element with the specified text content. + * + * @param {string} text - The text content for the text element. + * @returns {HTMLParagraphElement | null} The created text element or null if the text content is invalid. + */ + _createText(text) { + const isText = sanitizeValue(text, 'string') + + if (!isText) { + return null + } + + const textElement = document.createElement('p') + textElement.textContent = text + return textElement + } + + /** + * Removes an element from the DOM by its ID. + * + * @param {string} id - The ID of the element to be removed. + * @returns {boolean} Returns true if the element is successfully removed, false otherwise. + */ + _removeById(id) { + const isId = sanitizeValue(id, 'string') + + if (!isId) { + return false + } + + const element = document.getElementById(id) + + if (!element) { + return false + } + + element.remove() + return true + } +} + +export { BaseComponent } diff --git a/src/components/button.js b/src/components/button.js new file mode 100644 index 0000000..3c13a91 --- /dev/null +++ b/src/components/button.js @@ -0,0 +1,80 @@ +import './styles/button.scss' +import { BaseComponent } from './base-component' +import { sanitizeValue } from '@utilities/sanitize-value' +import { addEventListeners } from './utilities/add-event-listeners' +import { removeEventListeners } from './utilities/remove-event-listeners' + +/** + * @typedef {Object} ButtonConfig + * @property {string} id - The ID of the button. If not provided, a default ID will be used. + * @property {string} className - The class name(s) to be applied to the button. + * @property {string} [icon] - The name of the icon to be displayed on the button. + * @property {string} [text] - The text content of the button. + * @property {string} type='transparent' - The type of the button, which affects its styling. Default is 'transparent'. + * @property {Array<{type: string, func: Function}>} events - An array of event listener objects with `type` and `func` properties. + */ + +/** + * Represents a Button component. + */ +class Button extends BaseComponent { + /** + * Initializes the button instance with the provided configuration. + * + * @param {ButtonConfig} config - The configuration object for the button. + */ + constructor(config) { + super(config) + this.config = sanitizeValue(config, 'object') + } + + /** + * Creates and returns the button element based on the provided configuration. + * If either an icon or text is present in the configuration, it adds them to the button. + * Attaches event listeners to the button based on the events provided in the configuration. + * + * @returns {Element|null} The created button element or null if neither icon nor text is provided. + */ + create() { + const { + className, + events, + icon, + id, + text, + type = 'transparent' + } = this.config + const button = this._createContainer('div', { + id: `button-${id}`, + class: `${className} button ${type}-button` + }) + + if (!icon && !text) return null + if (icon) button.appendChild(this._createIcon(icon)) + if (text) button.appendChild(this._createText(text)) + addEventListeners(button, events) + + return button + } + + /** + * Removes the button element from the DOM along with its event listeners based on the provided configuration. + * + * @returns {boolean} Returns true if the button element is successfully removed, otherwise false. + */ + remove() { + const { events, id } = this.config + const elementId = `button-${id}` + const button = document.getElementById(elementId) + + if (button) { + removeEventListeners(button, events) + button.remove() + return true + } + + return false + } +} + +export { Button } diff --git a/src/components/checkbox.js b/src/components/checkbox.js new file mode 100644 index 0000000..ce5fec2 --- /dev/null +++ b/src/components/checkbox.js @@ -0,0 +1,71 @@ +import { sanitizeValue } from '@utilities/sanitize-value' +import { BaseComponent } from './base-component' + +/** + * @typedef {Object} CheckboxConfig + * @property {string} id - The ID of the checkbox. + * @property {string} className - The class name(s) to be applied to the checkbox. + * @property {string} groupName - The group name of the checkbox. + * @property {boolean} hidden - The visibility of the checkbox. + * @property {string} targetId - The target ID of the checkbox. + */ + +/** + * Represents a Checkbox component. + */ +class Checkbox extends BaseComponent { + /** + * Initializes the checkbox instance with the provided configuration. + * + * @param {CheckboxConfig} config - The configuration object for the checkbox. + */ + constructor(config) { + super(config) + this.config = sanitizeValue(config, 'object') + } + + /** + * Creates a checkbox element based on the provided configuration. + * + * @returns {HTMLElement} The created checkbox element. + */ + create() { + const { id, className, targetId, groupName, hidden } = this.config + + const checkbox = this._createContainer( + 'div', + { + 'id': `checkbox-${id}`, + 'class': `${className} checkboxes`, + 'data-state': 'false', + 'data-group-name': `checkboxes-${groupName}`, + 'data-target-id': targetId + }, + { + hidden: hidden + } + ) + + return checkbox + } + + /** + * Removes the checkbox element from the DOM based on the configuration ID. + * + * @returns {boolean} Returns true if the checkbox element is successfully removed, otherwise false. + */ + remove() { + const { id } = this.config + const elementId = `checkbox-${id}` + const checkbox = document.getElementById(elementId) + + if (checkbox) { + checkbox.remove() + return true + } + + return false + } +} + +export { Checkbox } diff --git a/src/components/container.js b/src/components/container.js new file mode 100644 index 0000000..fda0dd9 --- /dev/null +++ b/src/components/container.js @@ -0,0 +1,53 @@ +import { isConfigVerified } from '@utilities/config/config-verifier' +import { setAttributes } from '@utilities/components/set-attributes' + +class Container { + #config + + constructor(config) { + this.#config = isConfigVerified('container', config) ? config : {} + } + + create() { + const { id, class_name, elements, text } = this.#config + const CONTAINER = document.createElement('div') + setAttributes(CONTAINER, { + id: `${id}`, + class: `${class_name}` + }) + + if (elements) { + this.#appendElements(elements, CONTAINER) + } + + if (text) { + this.#appendText(text, CONTAINER) + } + + return CONTAINER + } + + remove() { + const { id } = this.#config + if (!id) return + + let CONTAINER = document.getElementById(`${id}`) + + if (!CONTAINER) return + + CONTAINER.remove() + CONTAINER = null + } + + #appendElements(elements, container) { + elements.forEach((element) => { + container.appendChild(element) + }) + } + + #appendText(text, container) { + container.textContent = text + } +} + +export { Container } diff --git a/src/components/modal.js b/src/components/modal.js new file mode 100644 index 0000000..d1b18da --- /dev/null +++ b/src/components/modal.js @@ -0,0 +1,82 @@ +import { BaseComponent } from './base-component' +import { sanitizeValue } from '@utilities/sanitize-value' + +/** + * @typedef {Object} ModalConfig + * @property {string} id - The ID of the modal. + * @property {string} icon - The name of the icon to be displayed on the modal. + * @property {string} title - The title of the modal. + * @property {Array} buttonInstances - An array of button instances for modal. + */ + +/** + * Represents a Modal component. + */ +class Modal extends BaseComponent { + /** + * Initializes the modal instance with the provided configuration. + * + * @param {ModalConfig} config - The configuration object for the modal. + */ + constructor(config) { + super(config) + this.config = sanitizeValue(config, 'object') + } + + /** + * Creates the modal element based on the provided configuration. + * + * @returns {HTMLElement|null} The created modal element or null if invalid configuration. + */ + create() { + const { id, title, icon, buttons } = this.config + + const modal = this._createContainer('div', { + id: `modal-${id}`, + class: 'modal' + }) + + const titleContainer = this._createContainer('div', { + class: 'modal-title-container' + }) + const iconHolder = this._createIcon(icon) + const titleText = this._createText(title) + + if (iconHolder) titleContainer.appendChild(iconHolder) + if (titleText) titleContainer.appendChild(titleText) + + modal.appendChild(titleContainer) + + buttons.forEach((button, index) => { + button.id = `modal-button-${index}` + modal.appendChild(button) + }) + + return modal + } + + /** + * Removes the modal element and its buttons from the DOM. + */ + remove() { + const { id, buttonInstances } = this.config + + let allRemoved = true + + buttonInstances.forEach((buttonInstance) => { + try { + if (!buttonInstance.remove()) { + allRemoved = false + } + } catch (error) { + console.error(`Error: ${error}`) + allRemoved = false + } + }) + + const modalRemoved = this._removeById(id) + return allRemoved && modalRemoved + } +} + +export { Modal } diff --git a/src/components/page.js b/src/components/page.js new file mode 100644 index 0000000..aee9e9e --- /dev/null +++ b/src/components/page.js @@ -0,0 +1,117 @@ +import { BaseComponent } from './base-component' +import { sanitizeValue } from '@utilities/sanitize-value' +import { Button } from './button' + +/** + * @typedef {Object} PageConfig + * @property {string} id - The ID of the page. + * @property {object} elements - The object consist of elements for page. + * @property {number} zIndex - The z-index of the page. + */ + +/** + * Represents a Page component. + */ +class Page extends BaseComponent { + /** + * Initializes the page instance with the provided configuration. + * + * @param {PageConfig} config - The configuration object for the page. + */ + constructor(config) { + super(config) + this.config = sanitizeValue(config, 'object') + } + + /** + * Creates a page element based on the provided configuration. + * Adds a header with an exit button if the z-index is greater than 10. + * Appends header and body elements to the page based on the elements in the configuration. + * + * @returns {Element} The created page element. + */ + create() { + const { elements, zIndex, id } = this.config + const page = this._createContainer('div', { + id: `page-${id}`, + class: 'page', + style: `z-index: ${zIndex}` + }) + const header = this._createContainer('div', { + class: 'page-header' + }) + + const zIndexForMainPages = 10 + + if (zIndex > zIndexForMainPages) { + const exitButtonConfig = { + icon: 'chevron-left', + id: `exit-page-${zIndex}`, + className: 'exit', + type: 'rounded-square', + events: [ + { + type: 'click', + func: this.remove.bind(this) + } + ] + } + const exitButton = new Button(exitButtonConfig).create() + header.appendChild(exitButton) + } + + if (elements.header) { + elements.header.forEach((element) => { + header.appendChild(element.create()) + }) + } + + const body = this._createContainer('div', { + class: 'page-body' + }) + + if (elements.body) { + elements.body.forEach((element) => { + body.appendChild(element.create()) + }) + } + + page.appendChild(header) + page.appendChild(body) + + return page + } + + /** + * Removes the page element from the DOM along with its header and body elements if they exist. + * + * @returns {boolean} Returns true if the page element was successfully removed, otherwise false. + */ + remove() { + const { elements, id } = this.config + + const elementId = `page-${id}` + const page = document.getElementById(elementId) + + if (elements.header) { + elements['header'].forEach((element) => { + element.remove() + }) + } + + if (elements.body) { + elements['body'].forEach((element) => { + element.remove() + }) + } + + if (page) { + page.remove() + return true + } + + return false + } +} + +export { Page } diff --git a/src/components/snackbar.js b/src/components/snackbar.js new file mode 100644 index 0000000..33025fb --- /dev/null +++ b/src/components/snackbar.js @@ -0,0 +1,106 @@ +import { sanitizeValue } from '@utilities/sanitize-value' + +/** + * @typedef {Object} SnackbarConfig + * @property {string} message - The title of the snackbar. + */ + +/** + * Represents a Snackbar component. + */ +class Snackbar { + static queue = [] + static isDisplaying = false + + /** + * Initializes the snackbar instance with the provided configuration. + * + * @param {SnackbarConfig} config - The configuration object for the snackbar. + */ + constructor(config) { + this.config = sanitizeValue(config, 'object') + } + + /** + * Renders the snackbar message and removes it after displaying. + */ + renderThenRemove() { + const { message } = this.config + Snackbar.#enqueue(message) + } + + /** + * Adds a message to the snackbar queue and triggers the queue processing. + * + * @param {string} message - The message to be added to the queue. + */ + static #enqueue(message) { + Snackbar.queue.push(message) + Snackbar.#processQueue() + } + + /** + * Processes the snackbar queue by displaying the next message in the queue if not already displaying a message. + */ + static #processQueue() { + if (Snackbar.isDisplaying || Snackbar.queue.length === 0) { + return + } + + Snackbar.isDisplaying = true + const message = Snackbar.queue.shift() + Snackbar.#display(message) + } + + /** + * Displays a snackbar message on the screen. + * + * @param {string} message - The message to be displayed in the snackbar. + */ + static #display(message) { + const isSnackbar = document.getElementById('snackbar') + + if (isSnackbar) { + isSnackbar.remove() + } + + const snackbar = document.createElement('div') + snackbar.classList.add('snackbar') + snackbar.id = snackbar + + snackbar.textContent = message + document.body.appendChild(snackbar) + + Snackbar.#remove(snackbar) + } + + /** + * Removes the snackbar element after a specified duration by animating its removal. + * + * @param {Element} snackbar - The snackbar element to be removed. + */ + static #remove(snackbar) { + let start + const duration = 3000 + + function animate(timestamp) { + if (!start) start = timestamp + + const progress = timestamp - start + + if (progress > duration) { + snackbar.remove() + snackbar = null + Snackbar.isDisplaying = false + Snackbar.#processQueue() + return + } + + requestAnimationFrame(animate) + } + + requestAnimationFrame(animate) + } +} + +export { Snackbar } diff --git a/src/components/tab.js b/src/components/tab.js new file mode 100644 index 0000000..4ba0ed2 --- /dev/null +++ b/src/components/tab.js @@ -0,0 +1,73 @@ +import { BaseComponent } from './base-component' +import { sanitizeValue } from '@utilities/sanitize-value' + +/** + * @typedef {Object} TabConfig + * @property {Array} buttonInstances - An array of button instances that will be added to the tab. + */ + +/** + * Represents a Tab component. + */ +class Tab extends BaseComponent { + /** + * Initializes the Tab instance with the provided configuration. + * + * @param {TabConfig} config - The configuration object for the tab. + */ + constructor(config) { + super(config) + this.config = sanitizeValue(config, 'object') + } + + /** + * Creates and returns a tab element based on the configuration provided. + * Iterates over the button instances in the configuration, creates corresponding buttons, + * adds a specific class to each button, and appends them to the tab element. + * + * @returns {HTMLElement} The created tab element. + */ + create() { + const { buttonInstances } = this.config + const tab = this._createContainer('div', { + id: 'tab', + class: 'tab' + }) + + for (const buttonInstance of buttonInstances) { + const button = buttonInstance.create() + button.classList.add('tab-button') + tab.appendChild(button) + } + + return tab + } + + /** + * Removes all button instances associated with the tab. + * Catches any errors that occur during the removal process and logs them. + * Finally, removes the tab element by its ID 'tab'. + * + * @returns {boolean} Returns true if all button instances are successfully removed, false otherwise. + */ + remove() { + const { buttonInstances } = this.config + let allRemoved = true + + buttonInstances.forEach((buttonInstance) => { + try { + if (!buttonInstance.remove()) { + allRemoved = false + } + } catch (error) { + console.error(`Error: ${error}`) + allRemoved = false + } + }) + + const tabRemoved = this._removeById('tab') + return allRemoved && tabRemoved + } +} + +export { Tab } diff --git a/src/components/textarea.js b/src/components/textarea.js new file mode 100644 index 0000000..3508c34 --- /dev/null +++ b/src/components/textarea.js @@ -0,0 +1,110 @@ +import { BaseComponent } from './base-component' +import { sanitizeValue } from '@utilities/sanitize-value' + +/** + * @typedef {Object} TextareaConfig + * @property {string} id - The ID of the textarea. + * @property {string} className - The class name(s) to be applied to the textarea. + * @property {string} placeholder - The placeholder of the textarea. + * @property {string} [text] - The text value of the textarea. + * @property {boolean} [readOnly] - Indicates whether the textarea is read-only. + * @property {boolean} [hidden] - The visibility of the textarea + */ + +/** + * Represents a Textarea component. + */ +class Textarea extends BaseComponent { + /** + * Initializes the textarea instance with the provided configuration. + * + * @param {TextareaConfig} config - The configuration object for the textarea. + */ + constructor(config) { + super(config) + this.config = sanitizeValue(config, 'object') + } + + /** + * Creates a textarea element based on the provided configuration. + * + * @returns {HTMLElement} The created textarea element. + */ + create() { + const { id, className, placeholder, text, readOnly, hidden } = this.config + const textarea = this._createContainer( + 'textarea', + { + id: `textarea-${id}`, + class: `textarea-${className} textarea`, + placeholder: placeholder + }, + { + readOnly: readOnly, + hidden: hidden + } + ) + + if (text) { + textarea.value = text + } + + return textarea + } + + /** + * Removes the textarea element from the DOM based on the configuration ID. + * + * @returns {boolean} Returns true if the textarea element was successfully removed, otherwise false. + */ + remove() { + const { id } = this.config + const elementId = `textarea-${id}` + const textarea = document.getElementById(elementId) + + if (textarea) { + textarea.remove() + return true + } + + return false + } + + /** + * Locks the textarea element to make it read-only. + * + * @returns {boolean} Returns true if the textarea element was successfully locked, otherwise false. + */ + lock() { + const { id } = this.config + const elementId = `textarea-${id}` + const textarea = document.getElementById(elementId) + + if (textarea) { + textarea.readOnly = true + return true + } + + return false + } + + /** + * Unlocks the textarea element to make it editable. + * + * @returns {boolean} Returns true if the textarea element was successfully unlocked, otherwise false. + */ + unlock() { + const { id } = this.config + const elementId = `textarea-${id}` + const textarea = document.getElementById(elementId) + + if (textarea) { + textarea.readOnly = false + return true + } + + return false + } +} + +export { Textarea } diff --git a/src/components/utilities/__tests__/add-event-listeners.test.js b/src/components/utilities/__tests__/add-event-listeners.test.js new file mode 100644 index 0000000..3caae89 --- /dev/null +++ b/src/components/utilities/__tests__/add-event-listeners.test.js @@ -0,0 +1,76 @@ +import { JSDOM } from 'jsdom' +import { addEventListeners } from '../add-event-listeners' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement +global.MouseEvent = window.MouseEvent + +describe('addEventListeners', () => { + it('should add event listeners to a valid HTML element', () => { + const element = document.createElement('div') + const handleClick = jest.fn() + const events = [{ type: 'click', func: handleClick }] + + const result = addEventListeners(element, events) + + expect(result).toBe(true) + element.click() + expect(handleClick).toHaveBeenCalled() + }) + + it('should return false if the element is not provided', () => { + const spyConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}) + const events = [{ type: 'click', func: () => {} }] + + const result = addEventListeners(null, events) + + expect(result).toBe(false) + expect(spyConsoleError).toHaveBeenCalledWith( + 'Cannot add event listeners! Check if the element or the events is valid.' + ) + spyConsoleError.mockRestore() + }) + + it('should return false if the events array is not provided', () => { + const spyConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}) + const element = document.createElement('div') + + const result = addEventListeners(element, null) + + expect(result).toBe(false) + expect(spyConsoleError).toHaveBeenCalledWith( + 'Cannot add event listeners! Check if the element or the events is valid.' + ) + spyConsoleError.mockRestore() + }) + + it('should return false if any event object in the array is invalid', () => { + const spyConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}) + const element = document.createElement('div') + const events = [ + { type: 'click', func: () => {} }, + { type: 'mouseover', func: 'not a function' } + ] + + const result = addEventListeners(element, events) + + expect(result).toBe(false) + expect(spyConsoleError).toHaveBeenCalledWith( + 'Cannot add event listeners! Check if the element or the events is valid.' + ) + spyConsoleError.mockRestore() + }) + + it('should log an error message when parameters are invalid', () => { + const spyConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}) + const result = addEventListeners(null, null) + + expect(result).toBe(false) + expect(spyConsoleError).toHaveBeenCalledWith( + 'Cannot add event listeners! Check if the element or the events is valid.' + ) + spyConsoleError.mockRestore() + }) +}) diff --git a/src/components/utilities/__tests__/config-verifier.test.js b/src/components/utilities/__tests__/config-verifier.test.js new file mode 100644 index 0000000..335a082 --- /dev/null +++ b/src/components/utilities/__tests__/config-verifier.test.js @@ -0,0 +1,341 @@ +import { JSDOM } from 'jsdom' +import { ConfigVerifier } from '../config-verifier' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement +global.MouseEvent = window.MouseEvent +global.DocumentFragment = window.DocumentFragment + +describe('ConfigVerifier', () => { + describe('Component Name Validation', () => { + it('should validate a valid component name correctly', () => { + const validButtonConfig = new ConfigVerifier('button', { className: 'btn', id: 'btn1' }) + expect(validButtonConfig.initialize()).toBe(true) + }) + + it('should reject an empty component name', () => { + const emptyNameConfig = new ConfigVerifier('', { className: 'btn', id: 'btn1' }) + expect(emptyNameConfig.initialize()).toBe(false) + }) + + it('should reject an invalid component name', () => { + const invalidComponentNameConfig = new ConfigVerifier('unknown', { + className: 'btn', + id: 'btn1' + }) + expect(invalidComponentNameConfig.initialize()).toBe(false) + }) + + it('should reject a non-string component name', () => { + const nonStringNameConfig = new ConfigVerifier(123, { className: 'btn', id: 'btn1' }) + expect(nonStringNameConfig.initialize()).toBe(false) + }) + + it('should handle a component name with leading/trailing spaces', () => { + const spacedNameConfig = new ConfigVerifier(' button ', { className: 'btn', id: 'btn1' }) + expect(spacedNameConfig.initialize()).toBe(false) + }) + }) + + describe('Config Property Validation', () => { + it('should validate a valid config object', () => { + const validButtonConfig = new ConfigVerifier('button', { className: 'btn', id: 'btn1' }) + expect(validButtonConfig.initialize()).toBe(true) + }) + + it('should reject null config', () => { + const nullConfig = new ConfigVerifier('button', null) + expect(nullConfig.initialize()).toBe(false) + }) + + it('should reject an array as config', () => { + const arrayConfig = new ConfigVerifier('button', []) + expect(arrayConfig.initialize()).toBe(false) + }) + + it('should reject an empty config object', () => { + const emptyConfig = new ConfigVerifier('button', {}) + expect(emptyConfig.initialize()).toBe(false) + }) + + it('should reject config with incorrect data type', () => { + const incorrectTypeConfig = new ConfigVerifier('button', 'string') + expect(incorrectTypeConfig.initialize()).toBe(false) + }) + }) + + describe('Valid Attributes Check', () => { + it('should validate all attributes for the button component', () => { + const validButtonConfig = new ConfigVerifier('button', { + className: 'btn', + id: 'btn1', + text: 'Click me', + events: [] + }) + expect(validButtonConfig.initialize()).toBe(true) + }) + + it('should reject config with extra attributes for the button component', () => { + const extraAttributesButtonConfig = new ConfigVerifier('button', { + className: 'btn', + id: 'btn1', + extra: 'extra' + }) + expect(extraAttributesButtonConfig.initialize()).toBe(false) + }) + + it('should validate all attributes for the checkbox component', () => { + const validCheckboxConfig = new ConfigVerifier('checkbox', { + className: 'checkbox', + id: 'chk1', + state: true, + targetId: 'target1' + }) + expect(validCheckboxConfig.initialize()).toBe(true) + }) + + it('should reject config with extra attributes for the checkbox component', () => { + const extraAttributesCheckboxConfig = new ConfigVerifier('checkbox', { + className: 'checkbox', + id: 'chk1', + extra: 'extra' + }) + expect(extraAttributesCheckboxConfig.initialize()).toBe(false) + }) + + it('should validate all attributes for the container component', () => { + const validContainerConfig = new ConfigVerifier('container', { + className: 'container', + id: 'cont1', + text: 'Container text' + }) + expect(validContainerConfig.initialize()).toBe(true) + }) + + it('should reject config with extra attributes for the container component', () => { + const extraAttributesContainerConfig = new ConfigVerifier('container', { + className: 'container', + id: 'cont1', + extra: 'extra' + }) + expect(extraAttributesContainerConfig.initialize()).toBe(false) + }) + + it('should validate all attributes for the modal component', () => { + const validModalConfig = new ConfigVerifier('modal', { + buttons: [], + icon: 'icon', + id: 'modal1', + title: 'Modal Title' + }) + expect(validModalConfig.initialize()).toBe(true) + }) + + it('should reject config with extra attributes for the modal component', () => { + const extraAttributesModalConfig = new ConfigVerifier('modal', { + buttons: [], + icon: 'icon', + id: 'modal1', + extra: 'extra' + }) + expect(extraAttributesModalConfig.initialize()).toBe(false) + }) + + it('should validate all attributes for the page component', () => { + const validPageConfig = new ConfigVerifier('page', { elements: {}, id: 'page1', zIndex: 10 }) + expect(validPageConfig.initialize()).toBe(true) + }) + + it('should reject config with extra attributes for the page component', () => { + const extraAttributesPageConfig = new ConfigVerifier('page', { + elements: [], + id: 'page1', + extra: 'extra' + }) + expect(extraAttributesPageConfig.initialize()).toBe(false) + }) + + it('should validate all attributes for the snackbar component', () => { + const validSnackbarConfig = new ConfigVerifier('snackbar', { message: 'Hello!' }) + expect(validSnackbarConfig.initialize()).toBe(true) + }) + + it('should reject config with extra attributes for the snackbar component', () => { + const extraAttributesSnackbarConfig = new ConfigVerifier('snackbar', { + message: 'Hello!', + extra: 'extra' + }) + expect(extraAttributesSnackbarConfig.initialize()).toBe(false) + }) + + it('should validate all attributes for the tab component', () => { + const validTabConfig = new ConfigVerifier('tab', { buttons: [] }) + expect(validTabConfig.initialize()).toBe(true) + }) + + it('should reject config with missing attributes for the tab component', () => { + const missingAttributesTabConfig = new ConfigVerifier('tab', {}) + expect(missingAttributesTabConfig.initialize()).toBe(false) + }) + + it('should reject config with extra attributes for the tab component', () => { + const extraAttributesTabConfig = new ConfigVerifier('tab', { buttons: [], extra: 'extra' }) + expect(extraAttributesTabConfig.initialize()).toBe(false) + }) + + it('should validate all attributes for the textarea component', () => { + const validTextareaConfig = new ConfigVerifier('textarea', { + id: 'txt1', + placeholder: 'Enter text', + readonly: true, + text: 'Some text' + }) + expect(validTextareaConfig.initialize()).toBe(true) + }) + + it('should reject config with extra attributes for the textarea component', () => { + const extraAttributesTextareaConfig = new ConfigVerifier('textarea', { + id: 'txt1', + placeholder: 'Enter text', + extra: 'extra' + }) + expect(extraAttributesTextareaConfig.initialize()).toBe(false) + }) + }) + + describe('Attribute Types Validation', () => { + it('should validate correct attribute types for button component', () => { + const validButtonConfig = new ConfigVerifier('button', { + className: 'btn', + id: 'btn1', + text: 'Click me', + events: [] + }) + expect(validButtonConfig.initialize()).toBe(true) + }) + + it('should reject invalid types for attributes in button component', () => { + const invalidButtonConfig = new ConfigVerifier('button', { + className: 'btn', + id: 'btn1', + text: 123, + events: 'string' + }) + expect(invalidButtonConfig.initialize()).toBe(false) + }) + + it('should validate correct attribute types for checkbox component', () => { + const validCheckboxConfig = new ConfigVerifier('checkbox', { + className: 'checkbox', + id: 'chk1', + state: true, + targetId: 'target1' + }) + expect(validCheckboxConfig.initialize()).toBe(true) + }) + + it('should reject invalid types for attributes in checkbox component', () => { + const invalidCheckboxConfig = new ConfigVerifier('checkbox', { + className: 'checkbox', + id: 'chk1', + state: 'true', + targetId: 123 + }) + expect(invalidCheckboxConfig.initialize()).toBe(false) + }) + + it('should validate correct attribute types for container component', () => { + const validContainerConfig = new ConfigVerifier('container', { + className: 'container', + id: 'cont1', + text: 'Container text' + }) + expect(validContainerConfig.initialize()).toBe(true) + }) + + it('should reject invalid types for attributes in container component', () => { + const invalidContainerConfig = new ConfigVerifier('container', { + className: 'container', + id: 'cont1', + text: 123 + }) + expect(invalidContainerConfig.initialize()).toBe(false) + }) + + it('should validate correct attribute types for modal component', () => { + const validModalConfig = new ConfigVerifier('modal', { + buttons: [], + icon: 'icon', + id: 'modal1', + title: 'Modal Title' + }) + expect(validModalConfig.initialize()).toBe(true) + }) + + it('should reject invalid types for attributes in modal component', () => { + const invalidModalConfig = new ConfigVerifier('modal', { + buttons: 'string', + icon: 'icon', + id: 'modal1', + title: 123 + }) + expect(invalidModalConfig.initialize()).toBe(false) + }) + + it('should validate correct attribute types for page component', () => { + const validPageConfig = new ConfigVerifier('page', { elements: {}, id: 'page1', zIndex: 10 }) + expect(validPageConfig.initialize()).toBe(true) + }) + + it('should reject invalid types for attributes in page component', () => { + const invalidPageConfig = new ConfigVerifier('page', { + elements: 'not an array', + id: 'page1', + zIndex: 'string' + }) + expect(invalidPageConfig.initialize()).toBe(false) + }) + + it('should validate correct attribute types for snackbar component', () => { + const validSnackbarConfig = new ConfigVerifier('snackbar', { message: 'Hello!' }) + expect(validSnackbarConfig.initialize()).toBe(true) + }) + + it('should reject invalid types for attributes in snackbar component', () => { + const invalidSnackbarConfig = new ConfigVerifier('snackbar', { message: 123 }) + expect(invalidSnackbarConfig.initialize()).toBe(false) + }) + + it('should validate correct attribute types for tab component', () => { + const validTabConfig = new ConfigVerifier('tab', { buttons: [] }) + expect(validTabConfig.initialize()).toBe(true) + }) + + it('should reject invalid types for attributes in tab component', () => { + const invalidTabConfig = new ConfigVerifier('tab', { buttons: 'string' }) + expect(invalidTabConfig.initialize()).toBe(false) + }) + + it('should validate correct attribute types for textarea component', () => { + const validTextareaConfig = new ConfigVerifier('textarea', { + id: 'txt1', + placeholder: 'Enter text', + readonly: true, + text: 'Some text' + }) + expect(validTextareaConfig.initialize()).toBe(true) + }) + + it('should reject invalid types for attributes in textarea component', () => { + const invalidTextareaConfig = new ConfigVerifier('textarea', { + id: 'txt1', + placeholder: 123, + readonly: 'true', + text: 123 + }) + expect(invalidTextareaConfig.initialize()).toBe(false) + }) + }) +}) diff --git a/src/components/utilities/__tests__/remove-event-listeners.test.js b/src/components/utilities/__tests__/remove-event-listeners.test.js new file mode 100644 index 0000000..5b6e154 --- /dev/null +++ b/src/components/utilities/__tests__/remove-event-listeners.test.js @@ -0,0 +1,79 @@ +import { JSDOM } from 'jsdom' +import { removeEventListeners } from '../remove-event-listeners' + +const dom = new JSDOM('') +global.window = dom.window +global.document = window.document +global.HTMLElement = window.HTMLElement +global.MouseEvent = window.MouseEvent +global.DocumentFragment = window.DocumentFragment + +describe('removeEventListeners', () => { + it('should remove event listeners from a valid HTML element', () => { + const element = document.createElement('div') + const handleClick = jest.fn() + element.addEventListener('click', handleClick) + + const events = [{ type: 'click', func: handleClick }] + const result = removeEventListeners(element, events) + + expect(result).toBe(true) + element.click() + expect(handleClick).not.toHaveBeenCalled() + }) + + it('should return false if the element is not provided', () => { + const events = [{ type: 'click', func: () => {} }] + + const result = removeEventListeners(null, events) + + expect(result).toBe(false) + }) + + it('should return false if the events array is not provided', () => { + const element = document.createElement('div') + + const result = removeEventListeners(element, null) + + expect(result).toBe(false) + }) + + it('should return false if any event object in the array is invalid', () => { + const element = document.createElement('div') + const events = [ + { type: 'click', func: () => {} }, + { type: 'mouseover', func: 'not a function' } + ] + + const result = removeEventListeners(element, events) + + expect(result).toBe(false) + }) + + it('should log comprehensive errors if validation fails', () => { + const spyConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}) + const element = document.createElement('div') + // Falsy event type + const result = removeEventListeners(element, [{ type: 12, func: () => {} }]) + expect(result).toBe(false) + + // Comprehensive Errors + expect(spyConsoleError).toHaveBeenNthCalledWith( + 1, + 'Notice: The value and type are not equal. Value is of type number, while type is string.' + ) + expect(spyConsoleError).toHaveBeenNthCalledWith( + 2, + 'Cannot remove event listeners! Check if the element or the events is valid.' + ) + + spyConsoleError.mockRestore() + }) + + it('should handle elements with no event listeners attached', () => { + const element = document.createElement('div') + const events = [{ type: 'click', func: () => {} }] + const result = removeEventListeners(element, events) + expect(result).toBe(true) + }) +}) diff --git a/src/components/utilities/add-event-listeners.js b/src/components/utilities/add-event-listeners.js new file mode 100644 index 0000000..08bad07 --- /dev/null +++ b/src/components/utilities/add-event-listeners.js @@ -0,0 +1,52 @@ +import { sanitizeValue } from '@utilities/sanitize-value' + +/** + * Adds event listeners to a given element based on the provided events. + * + * @param {HTMLElement} element - The element to attach the event listeners to. + * @param {Array} events - An array of objects representing events to be added, each containing `type` (string) and `func` (function as listener). + * @returns {boolean} Returns true if all event listeners were successfully added, false otherwise. + */ +function addEventListeners(element, events) { + if (!validateParameters(element, events)) { + console.error('Cannot add event listeners! Invalid element or events parameter.') + return false + } + + events.forEach((event) => { + element.addEventListener(event.type, event.func) + }) + + return true +} + +/** + * Validates the parameters passed to the function. + * + * @param {HTMLElement} element - The HTML element to validate. + * @param {Array} events - An array of events to validate. + * @returns {boolean} - Returns true if all parameters are valid, false otherwise. + */ +function validateParameters(element, events) { + const isValidElement = sanitizeValue(element, 'HTMLElement') + const isValidEvents = sanitizeValue(events, 'array') + + if (!isValidElement || !isValidEvents) { + return false + } + + let flag = true + events.forEach((event) => { + const isEvent = sanitizeValue(event, 'object') + const isEventType = sanitizeValue(event.type, 'string') + const isEventFunc = sanitizeValue(event.func, 'function') + + if (!isEvent || !isEventType || !isEventFunc) { + flag = false + } + }) + + return flag +} + +export { addEventListeners } diff --git a/src/components/utilities/config-verifier.js b/src/components/utilities/config-verifier.js new file mode 100644 index 0000000..f98cd19 --- /dev/null +++ b/src/components/utilities/config-verifier.js @@ -0,0 +1,186 @@ +/** + * Class for verifying configuration settings of different components. + */ +class ConfigVerifier { + #validAttributesList = [ + { button: ['className', 'events', 'icon', 'id', 'text', 'type'] }, + { checkbox: ['className', 'groupName', 'hidden', 'id', 'state', 'targetId'] }, + { container: ['className', 'elements', 'id', 'text'] }, + { modal: ['buttons', 'icon', 'id', 'title'] }, + { page: ['elements', 'id', 'zIndex'] }, + { snackbar: ['message'] }, + { tab: ['buttons'] }, + { textarea: ['className', 'hidden', 'id', 'placeholder', 'readonly', 'text'] } + ] + + #validAttributesTypesList = { + string: [ + 'className', + 'groupName', + 'icon', + 'id', + 'message', + 'placeholder', + 'text', + 'title', + 'type' + ], + array: ['buttons', 'events'], + boolean: ['hidden', 'readonly', 'state'], + number: ['zIndex'], + object: ['elements'] + } + + /** + * Creates an instance of ConfigVerifier. + * + * @param {string} componentName - The name of the component to be verified. + * @param {Object} config - The configuration object containing attributes to be verified. + */ + constructor(componentName, config) { + this.componentName = componentName + this.config = config + } + + /** + * Initializes the configuration verification process by checking the component name, config property, + * valid attributes, and attribute types. + * + * @returns {boolean} Returns true if all verification checks pass, false otherwise. + */ + initialize() { + return ( + this.#isComponentName() && + this.#isConfig() && + this.#hasValidAttributes() && + this.#hasValidTypes() + ) + } + + /** + * Checks if the provided component name is valid. + * + * @returns {boolean} Returns true if the component name is valid, false otherwise. + */ + #isComponentName() { + if (!this.componentName) { + console.error('Invalid input: componentName is required and must be a non-empty string.') + return false + } + + if (typeof this.componentName !== 'string') { + console.error( + `Invalid input: componentName is not a string but a ${typeof this.componentName}` + ) + return false + } + + return true + } + + /** + * Checks if the config property is valid. + * + * @returns {boolean} Returns true if the config property is valid, false otherwise. + */ + #isConfig() { + if (!this.config || this.config === undefined || Object.keys(this.config).length === 0) { + console.error('Invalid input: config is required and must not be empty.') + return false + } + + if (typeof this.config !== 'object' || Array.isArray(this.config)) { + console.error(`Invalid input: config is not an object but a ${typeof this.config}`) + return false + } + + return true + } + + /** + * Checks if the provided configuration has valid attributes for the component. + * + * @returns {boolean} Returns true if all attributes are valid, false if any invalid attribute is found. + */ + #hasValidAttributes() { + const validAttributesEntry = this.#validAttributesList.find((attributeList) => + Object.hasOwn(attributeList, this.componentName) + ) + + if (!validAttributesEntry) { + console.error(`The ${this.componentName} is not found in the list.`) + return false + } + + const validAttributesForComponent = validAttributesEntry[`${this.componentName}`] + const configAttributeKeys = Object.keys(this.config) + const invalidAttributeKeys = configAttributeKeys.filter( + (key) => !validAttributesForComponent.includes(key) + ) + let isValid = true + + if (invalidAttributeKeys.length > 0) { + invalidAttributeKeys.forEach((invalidKey) => { + console.error(`The '${invalidKey}' is not valid attribute for '${this.componentName}'.`) + isValid = false + }) + } + + return isValid + } + + /** + * Checks if the attributes in the configuration object have valid types based on predefined types. + * + * @returns {boolean} Returns true if all attribute types are valid, false if any attribute has an invalid type. + */ + #hasValidTypes() { + let isValid = true + const typeCheckFunctions = { + string: (key, value) => { + if (typeof value !== 'string') { + console.error(`Invalid type for '${key}': Expected 'string', but got '${typeof value}'.`) + isValid = false + } + }, + array: (key, value) => { + if (!Array.isArray(value)) { + console.error(`Invalid type for '${key}': Expected 'array', but got '${typeof value}'.`) + isValid = false + } + }, + boolean: (key, value) => { + if (typeof value !== 'boolean') { + console.error(`Invalid type for '${key}': Expected 'boolean', but got '${typeof value}'.`) + isValid = false + } + }, + number: (key, value) => { + if (typeof value !== 'number') { + console.error(`Invalid type for '${key}': Expected 'number', but got '${typeof value}'.`) + isValid = false + } + }, + object: (key, value) => { + if (typeof value !== 'object' || Array.isArray(value) || value === null) { + console.error(`Invalid type for '${key}': Expected 'object', but got '${typeof value}'.`) + isValid = false + } + } + } + + Object.entries(this.#validAttributesTypesList).forEach(([type, attributes]) => { + attributes.forEach((attribute) => { + const value = this.config[attribute] + + if (value !== undefined) { + typeCheckFunctions[type](attribute, value) + } + }) + }) + + return isValid + } +} + +export { ConfigVerifier } diff --git a/src/components/utilities/remove-event-listeners.js b/src/components/utilities/remove-event-listeners.js new file mode 100644 index 0000000..80caa65 --- /dev/null +++ b/src/components/utilities/remove-event-listeners.js @@ -0,0 +1,52 @@ +import { sanitizeValue } from '@utilities/sanitize-value' + +/** + * Removes event listeners from a given element based on the provided events. + * + * @param {HTMLElement} element - The element from which to remove event listeners. + * @param {Array} events - An array of objects representing the events to remove. + * @returns {boolean} Returns true if all event listeners were successfully removed, false otherwise. + */ +function removeEventListeners(element, events) { + if (!validateParameters(element, events)) { + console.error('Cannot remove event listeners! Invalid element or events parameter.') + return false + } + + events.forEach((event) => { + element.removeEventListener(event.type, event.func) + }) + + return true +} + +/** + * Validates the parameters passed to the function. + * + * @param {HTMLElement} element - The HTML element to validate. + * @param {Array} events - An array of events to validate. + * @returns {boolean} - Returns true if all parameters are valid, false otherwise. + */ +function validateParameters(element, events) { + const isValidElement = sanitizeValue(element, 'HTMLElement') + const isValidEvents = sanitizeValue(events, 'array') + + if (!isValidElement || !isValidEvents) { + return false + } + + let flag = true + events.forEach((event) => { + const isEvent = sanitizeValue(event, 'object') + const isEventType = sanitizeValue(event.type, 'string') + const isEventFunc = sanitizeValue(event.func, 'function') + + if (!isEvent || !isEventType || !isEventFunc) { + flag = false + } + }) + + return flag +} + +export { removeEventListeners }