const smButton = document.createElement('template') smButton.innerHTML = `
`; customElements.define('sm-button', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smButton.content.cloneNode(true)) } static get observedAttributes() { return ['disabled']; } get disabled() { return this.hasAttribute('disabled') } set disabled(value) { if (value) { this.setAttribute('disabled', '') } else { this.removeAttribute('disabled') } } handleKeyDown(e) { if (!this.hasAttribute('disabled') && (e.key === 'Enter' || e.code === 'Space')) { e.preventDefault() this.click() } } connectedCallback() { if (!this.hasAttribute('disabled')) { this.setAttribute('tabindex', '0') } this.setAttribute('role', 'button') this.addEventListener('keydown', this.handleKeyDown) } attributeChangedCallback(name, oldVal, newVal) { if (name === 'disabled') { this.removeAttribute('tabindex') this.setAttribute('aria-disabled', 'true') } else { this.setAttribute('tabindex', '0') this.setAttribute('aria-disabled', 'false') } } }) const smForm = document.createElement('template') smForm.innerHTML = `
` customElements.define('sm-form', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smForm.content.cloneNode(true)) this.form = this.shadowRoot.querySelector('form') this.formElements this.requiredElements this.submitButton this.resetButton this.allRequiredValid = false this.debounce = this.debounce.bind(this) this.handleInput = this.handleInput.bind(this) this.handleKeydown = this.handleKeydown.bind(this) this.reset = this.reset.bind(this) } debounce(callback, wait) { let timeoutId = null; return (...args) => { window.clearTimeout(timeoutId); timeoutId = window.setTimeout(() => { callback.apply(null, args); }, wait); }; } handleInput(e) { this.allRequiredValid = this.requiredElements.every(elem => elem.isValid) if (!this.submitButton) return; if (this.allRequiredValid) { this.submitButton.disabled = false; } else { this.submitButton.disabled = true; } } handleKeydown(e) { if (e.key === 'Enter' && e.target.tagName !== 'SM-TEXTAREA') { if (this.allRequiredValid) { this.submitButton.click() } else { this.requiredElements.find(elem => !elem.isValid).vibrate() } } } reset() { this.formElements.forEach(elem => elem.reset()) } connectedCallback() { const slot = this.shadowRoot.querySelector('slot') slot.addEventListener('slotchange', e => { this.formElements = [...this.querySelectorAll('sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio')] this.requiredElements = this.formElements.filter(elem => elem.hasAttribute('required')) this.submitButton = e.target.assignedElements().find(elem => elem.getAttribute('variant') === 'primary' || elem.getAttribute('type') === 'submit'); this.resetButton = e.target.assignedElements().find(elem => elem.getAttribute('type') === 'reset'); if (this.resetButton) { this.resetButton.addEventListener('click', this.reset) } }) this.addEventListener('input', this.debounce(this.handleInput, 100)) this.addEventListener('keydown', this.debounce(this.handleKeydown, 100)) } disconnectedCallback() { this.removeEventListener('input', this.debounce(this.handleInput, 100)) this.removeEventListener('keydown', this.debounce(this.handleKeydown, 100)) } }) const smInput = document.createElement('template') smInput.innerHTML = `

`; customElements.define('sm-input', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smInput.content.cloneNode(true)) this.inputParent = this.shadowRoot.querySelector('.input') this.input = this.shadowRoot.querySelector('input') this.clearBtn = this.shadowRoot.querySelector('.clear') this.label = this.shadowRoot.querySelector('.label') this.feedbackText = this.shadowRoot.querySelector('.feedback-text') this.outerContainer = this.shadowRoot.querySelector('.outer-container') this._helperText this._errorText this.isRequired = false this.validationFunction this.reflectedAttributes = ['value', 'required', 'disabled', 'type', 'inputmode', 'readonly', 'min', 'max', 'pattern', 'minlength', 'maxlength', 'step'] this.reset = this.reset.bind(this) this.focusIn = this.focusIn.bind(this) this.focusOut = this.focusOut.bind(this) this.fireEvent = this.fireEvent.bind(this) this.checkInput = this.checkInput.bind(this) this.vibrate = this.vibrate.bind(this) } static get observedAttributes() { return ['value', 'placeholder', 'required', 'disabled', 'type', 'inputmode', 'readonly', 'min', 'max', 'pattern', 'minlength', 'maxlength', 'step', 'helper-text', 'error-text'] } get value() { return this.input.value } set value(val) { this.input.value = val; this.checkInput() this.fireEvent() } get placeholder() { return this.getAttribute('placeholder') } set placeholder(val) { this.setAttribute('placeholder', val) } get type() { return this.getAttribute('type') } set type(val) { this.setAttribute('type', val) } get validity() { return this.input.validity } set disabled(value) { if (value) this.inputParent.classList.add('disabled') else this.inputParent.classList.remove('disabled') } set readOnly(value) { if (value) { this.setAttribute('readonly', '') } else { this.removeAttribute('readonly') } } set customValidation(val) { this.validationFunction = val } set errorText(val) { this._errorText = val } set helperText(val) { this._helperText = val } get isValid() { if (this.input.value !== '') { const _isValid = this.input.checkValidity() let _customValid = true if (this.validationFunction) { _customValid = Boolean(this.validationFunction(this.input.value)) } if (_isValid && _customValid) { this.feedbackText.classList.remove('error') this.feedbackText.classList.add('success') this.feedbackText.textContent = '' } else { if (this._errorText) { this.feedbackText.classList.add('error') this.feedbackText.classList.remove('success') this.feedbackText.innerHTML = ` ${this._errorText} ` } } return (_isValid && _customValid) } } reset() { this.value = '' } focusIn() { this.input.focus() } focusOut() { this.input.blur() } fireEvent() { let event = new Event('input', { bubbles: true, cancelable: true, composed: true }); this.dispatchEvent(event); } checkInput(e) { if (!this.hasAttribute('readonly')) { if (this.input.value.trim() !== '') { this.clearBtn.classList.remove('hide') } else { this.clearBtn.classList.add('hide') if (this.isRequired) { this.feedbackText.textContent = '* required' } } } if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder').trim() === '') return; if (this.input.value !== '') { if (this.animate) this.inputParent.classList.add('animate-label') else this.label.classList.add('hide') } else { if (this.animate) this.inputParent.classList.remove('animate-label') else this.label.classList.remove('hide') } } vibrate() { this.outerContainer.animate([ { transform: 'translateX(-1rem)' }, { transform: 'translateX(1rem)' }, { transform: 'translateX(-0.5rem)' }, { transform: 'translateX(0.5rem)' }, { transform: 'translateX(0)' }, ], { duration: 300, easing: 'ease' }) } connectedCallback() { this.animate = this.hasAttribute('animate') this.setAttribute('role', 'textbox') this.input.addEventListener('input', this.checkInput) this.clearBtn.addEventListener('click', this.reset) } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { if (this.reflectedAttributes.includes(name)) { if (this.hasAttribute(name)) { this.input.setAttribute(name, this.getAttribute(name) ? this.getAttribute(name) : '') } else { this.input.removeAttribute(name) } } if (name === 'placeholder') { this.label.textContent = newValue; this.setAttribute('aria-label', newValue); } else if (this.hasAttribute('value')) { this.checkInput() } else if (name === 'type') { if (this.hasAttribute('type') && this.getAttribute('type') === 'number') { this.input.setAttribute('inputmode', 'numeric') } } else if (name === 'helper-text') { this._helperText = this.getAttribute('helper-text') } else if (name === 'error-text') { this._errorText = this.getAttribute('error-text') } else if (name === 'required') { this.isRequired = this.hasAttribute('required') if (this.isRequired) { this.feedbackText.textContent = '* required' this.setAttribute('aria-required', 'true') } else { this.feedbackText.textContent = '' this.setAttribute('aria-required', 'false') } } else if (name === 'readonly') { if (this.hasAttribute('readonly')) { this.inputParent.classList.add('readonly') } else { this.inputParent.classList.remove('readonly') } } else if (name === 'disabled') { if (this.hasAttribute('disabled')) { this.inputParent.classList.add('disabled') } else { this.inputParent.classList.remove('disabled') } } } } disconnectedCallback() { this.input.removeEventListener('input', this.checkInput) this.clearBtn.removeEventListener('click', this.reset) } }) const smNotifications = document.createElement('template') smNotifications.innerHTML = `
` customElements.define('sm-notifications', class extends HTMLElement { constructor() { super() this.shadow = this.attachShadow({ mode: 'open' }).append(smNotifications.content.cloneNode(true)) this.notificationPanel = this.shadowRoot.querySelector('.notification-panel') this.animationOptions = { duration: 300, fill: "forwards", easing: "cubic-bezier(0.175, 0.885, 0.32, 1.275)" } this.push = this.push.bind(this) this.createNotification = this.createNotification.bind(this) this.removeNotification = this.removeNotification.bind(this) this.clearAll = this.clearAll.bind(this) } randString(length) { let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * characters.length)); return result; } createNotification(message, options) { const { pinned = false, icon = '' } = options const notification = document.createElement('div') notification.id = this.randString(8) notification.classList.add('notification') let composition = `` composition += `
${icon}

${message}

` if (pinned) { notification.classList.add('pinned') composition += ` ` } notification.innerHTML = composition return notification } push(message, options = {}) { const notification = this.createNotification(message, options) this.notificationPanel.append(notification) notification.animate([ { transform: `translateY(1rem)`, opacity: '0' }, { transform: `none`, opacity: '1' }, ], this.animationOptions) return notification.id } removeNotification(notification) { notification.animate([ { transform: `none`, opacity: '1' }, { transform: `translateY(0.5rem)`, opacity: '0' } ], this.animationOptions).onfinish = () => { notification.remove() } } clearAll() { Array.from(this.notificationPanel.children).forEach(child => { this.removeNotification(child) }) } connectedCallback() { this.notificationPanel.addEventListener('click', e => { if (e.target.closest('.close')) ( this.removeNotification(e.target.closest('.notification')) ) }) const observer = new MutationObserver(mutationList => { mutationList.forEach(mutation => { if (mutation.type === 'childList') { if (mutation.addedNodes.length && !mutation.addedNodes[0].classList.contains('pinned')) { setTimeout(() => { this.removeNotification(mutation.addedNodes[0]) }, 5000); } } }) }) observer.observe(this.notificationPanel, { childList: true, }) } }) const smPopup = document.createElement('template') smPopup.innerHTML = ` `; customElements.define('sm-popup', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smPopup.content.cloneNode(true)) this.allowClosing = false this.isOpen = false this.pinned = false this.popupStack this.offset this.touchStartY = 0 this.touchEndY = 0 this.touchStartTime = 0 this.touchEndTime = 0 this.touchEndAnimataion this.popupContainer = this.shadowRoot.querySelector('.popup-container') this.popup = this.shadowRoot.querySelector('.popup') this.popupBodySlot = this.shadowRoot.querySelector('.popup-body slot') this.popupHeader = this.shadowRoot.querySelector('.popup-top') this.resumeScrolling = this.resumeScrolling.bind(this) this.show = this.show.bind(this) this.hide = this.hide.bind(this) this.handleTouchStart = this.handleTouchStart.bind(this) this.handleTouchMove = this.handleTouchMove.bind(this) this.handleTouchEnd = this.handleTouchEnd.bind(this) this.movePopup = this.movePopup.bind(this) } static get observedAttributes() { return ['open']; } get open() { return this.isOpen } resumeScrolling() { const scrollY = document.body.style.top; window.scrollTo(0, parseInt(scrollY || '0') * -1); setTimeout(() => { document.body.style.overflow = 'auto'; document.body.style.top = 'initial' }, 300); } show(options = {}) { const { pinned = false, popupStack = undefined } = options if (popupStack) this.popupStack = popupStack if (this.popupStack && !this.hasAttribute('open')) { this.popupStack.push({ popup: this, permission: pinned }) if (this.popupStack.items.length > 1) { this.popupStack.items[this.popupStack.items.length - 2].popup.classList.add('stacked') } this.dispatchEvent( new CustomEvent("popupopened", { bubbles: true, detail: { popup: this, popupStack: this.popupStack } }) ) this.setAttribute('open', '') this.pinned = pinned this.isOpen = true } this.popupContainer.classList.remove('hide') this.popup.style.transform = 'none'; document.body.style.overflow = 'hidden'; document.body.style.top = `-${window.scrollY}px` return this.popupStack } hide() { if (window.innerWidth < 640) this.popup.style.transform = 'translateY(100%)'; else this.popup.style.transform = 'translateY(3rem)'; this.popupContainer.classList.add('hide') this.removeAttribute('open') if (typeof this.popupStack !== 'undefined') { this.popupStack.pop() if (this.popupStack.items.length) { this.popupStack.items[this.popupStack.items.length - 1].popup.classList.remove('stacked') } else { this.resumeScrolling() } } else { this.resumeScrolling() } if (this.forms.length) { setTimeout(() => { this.forms.forEach(form => form.reset()) }, 300); } setTimeout(() => { this.dispatchEvent( new CustomEvent("popupclosed", { bubbles: true, detail: { popup: this, popupStack: this.popupStack } }) ) this.isOpen = false }, 300); } handleTouchStart(e) { this.touchStartY = e.changedTouches[0].clientY this.popup.style.transition = 'transform 0.1s' this.touchStartTime = e.timeStamp } handleTouchMove(e) { if (this.touchStartY < e.changedTouches[0].clientY) { this.offset = e.changedTouches[0].clientY - this.touchStartY; this.touchEndAnimataion = window.requestAnimationFrame(() => this.movePopup()) } } handleTouchEnd(e) { this.touchEndTime = e.timeStamp cancelAnimationFrame(this.touchEndAnimataion) this.touchEndY = e.changedTouches[0].clientY this.popup.style.transition = 'transform 0.3s' this.threshold = this.popup.getBoundingClientRect().height * 0.3 if (this.touchEndTime - this.touchStartTime > 200) { if (this.touchEndY - this.touchStartY > this.threshold) { if (this.pinned) { this.show() return } else this.hide() } else { this.show() } } else { if (this.touchEndY > this.touchStartY) if (this.pinned) { this.show() return } else this.hide() } } movePopup() { this.popup.style.transform = `translateY(${this.offset}px)` } connectedCallback() { this.popupBodySlot.addEventListener('slotchange', () => { this.forms = this.querySelectorAll('sm-form') }) this.popupContainer.addEventListener('mousedown', e => { if (e.target === this.popupContainer && !this.pinned) { if (this.pinned) { this.show() } else this.hide() } }) const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { if (entry.contentBoxSize) { // Firefox implements `contentBoxSize` as a single content rect, rather than an array const contentBoxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize; this.threshold = contentBoxSize.blockSize.height * 0.3 } else { this.threshold = entry.contentRect.height * 0.3 } } }); resizeObserver.observe(this) this.popupHeader.addEventListener('touchstart', (e) => { this.handleTouchStart(e) }, { passive: true }) this.popupHeader.addEventListener('touchmove', (e) => { this.handleTouchMove(e) }, { passive: true }) this.popupHeader.addEventListener('touchend', (e) => { this.handleTouchEnd(e) }, { passive: true }) } disconnectedCallback() { this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, { passive: true }) this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, { passive: true }) this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, { passive: true }) resizeObserver.unobserve() } attributeChangedCallback(name, oldVal, newVal) { if (name === 'open') { if (this.hasAttribute('open')) { this.show() } } } }) const spinner = document.createElement('template') spinner.innerHTML = ` ` class SquareLoader extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(spinner.content.cloneNode(true)) } } window.customElements.define('sm-spinner', SquareLoader); const themeToggle = document.createElement('template') themeToggle.innerHTML = ` ` class ThemeToggle extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(themeToggle.content.cloneNode(true)) this.isChecked = false this.hasTheme = 'light' this.toggleState = this.toggleState.bind(this) this.fireEvent = this.fireEvent.bind(this) this.handleThemeChange = this.handleThemeChange.bind(this) } static get observedAttributes() { return ['checked']; } daylight() { this.hasTheme = 'light' document.body.dataset.theme = 'light' this.setAttribute('aria-checked', 'false') } nightlight() { this.hasTheme = 'dark' document.body.dataset.theme = 'dark' this.setAttribute('aria-checked', 'true') } toggleState() { this.toggleAttribute('checked') this.fireEvent() } handleKeyDown(e) { if (e.code === 'Space') { this.toggleState() } } handleThemeChange(e) { if (e.detail.theme !== this.hasTheme) { if (e.detail.theme === 'dark') { this.setAttribute('checked', '') } else { this.removeAttribute('checked') } } } fireEvent() { this.dispatchEvent( new CustomEvent('themechange', { bubbles: true, composed: true, detail: { theme: this.hasTheme } }) ) } connectedCallback() { this.setAttribute('role', 'switch') this.setAttribute('aria-label', 'theme toggle') if (localStorage.getItem(`${window.location.hostname}-theme`) === "dark") { this.nightlight(); this.setAttribute('checked', '') } else if (localStorage.getItem(`${window.location.hostname}-theme`) === "light") { this.daylight(); this.removeAttribute('checked') } else { if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) { this.nightlight(); this.setAttribute('checked', '') } else { this.daylight(); this.removeAttribute('checked') } } this.addEventListener("click", this.toggleState); this.addEventListener("keydown", this.handleKeyDown); document.addEventListener('themechange', this.handleThemeChange) } disconnectedCallback() { this.removeEventListener("click", this.toggleState); this.removeEventListener("keydown", this.handleKeyDown); document.removeEventListener('themechange', this.handleThemeChange) } attributeChangedCallback(name, oldVal, newVal) { if (name === 'checked') { if (this.hasAttribute('checked')) { this.nightlight(); localStorage.setItem(`${window.location.hostname}-theme`, "dark"); } else { this.daylight(); localStorage.setItem(`${window.location.hostname}-theme`, "light"); } } } } window.customElements.define('theme-toggle', ThemeToggle); const smCopy = document.createElement('template') smCopy.innerHTML = `

`; customElements.define('sm-copy', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smCopy.content.cloneNode(true)) this.copyContent = this.shadowRoot.querySelector('.copy-content') this.copyButton = this.shadowRoot.querySelector('.copy-button') this.copy = this.copy.bind(this) } static get observedAttributes() { return ['value'] } set value(val) { this.setAttribute('value', val) } get value() { return this.getAttribute('value') } fireEvent() { this.dispatchEvent( new CustomEvent('copy', { composed: true, bubbles: true, cancelable: true, }) ) } copy() { navigator.clipboard.writeText(this.copyContent.textContent) .then(res => this.fireEvent()) .catch(err => console.error(err)) } connectedCallback() { this.copyButton.addEventListener('click', this.copy) } attributeChangedCallback(name, oldValue, newValue) { if (name === 'value') { this.copyContent.textContent = newValue } } disconnectedCallback() { this.copyButton.removeEventListener('click', this.copy) } }) const stripSelect = document.createElement('template') stripSelect.innerHTML = `
` customElements.define('strip-select', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(stripSelect.content.cloneNode(true)) this.stripSelect = this.shadowRoot.querySelector('.strip-select') this.slottedOptions this._value this.scrollDistance this.scrollLeft = this.scrollLeft.bind(this) this.scrollRight = this.scrollRight.bind(this) this.fireEvent = this.fireEvent.bind(this) } get value() { return this._value } scrollLeft() { this.stripSelect.scrollBy({ left: -this.scrollDistance, behavior: 'smooth' }) } scrollRight() { this.stripSelect.scrollBy({ left: this.scrollDistance, behavior: 'smooth' }) } fireEvent() { this.dispatchEvent( new CustomEvent("change", { bubbles: true, composed: true, detail: { value: this._value } }) ) } connectedCallback() { this.setAttribute('role', 'listbox') const slot = this.shadowRoot.querySelector('slot') const coverLeft = this.shadowRoot.querySelector('.cover--left') const coverRight = this.shadowRoot.querySelector('.cover--right') const navButtonLeft = this.shadowRoot.querySelector('.nav-button--left') const navButtonRight = this.shadowRoot.querySelector('.nav-button--right') slot.addEventListener('slotchange', e => { const assignedElements = slot.assignedElements() assignedElements.forEach(elem => { if (elem.hasAttribute('selected')) { elem.setAttribute('active', '') this._value = elem.value } }) if (!this.hasAttribute('multiline')) { if (assignedElements.length > 0) { firstOptionObserver.observe(slot.assignedElements()[0]) lastOptionObserver.observe(slot.assignedElements()[slot.assignedElements().length - 1]) } else { navButtonLeft.classList.add('hide') navButtonRight.classList.add('hide') coverLeft.classList.add('hide') coverRight.classList.add('hide') firstOptionObserver.disconnect() lastOptionObserver.disconnect() } } }) const resObs = new ResizeObserver(entries => { entries.forEach(entry => { if (entry.contentBoxSize) { // Firefox implements `contentBoxSize` as a single content rect, rather than an array const contentBoxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize; this.scrollDistance = contentBoxSize.inlineSize * 0.6 } else { this.scrollDistance = entry.contentRect.width * 0.6 } }) }) resObs.observe(this) this.stripSelect.addEventListener('option-clicked', e => { if (this._value !== e.target.value) { this._value = e.target.value slot.assignedElements().forEach(elem => elem.removeAttribute('active')) e.target.setAttribute('active', '') e.target.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }) this.fireEvent() } }) const firstOptionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { navButtonLeft.classList.add('hide') coverLeft.classList.add('hide') } else { navButtonLeft.classList.remove('hide') coverLeft.classList.remove('hide') } }) }, { threshold: 0.9, root: this }) const lastOptionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { navButtonRight.classList.add('hide') coverRight.classList.add('hide') } else { navButtonRight.classList.remove('hide') coverRight.classList.remove('hide') } }) }, { threshold: 0.9, root: this }) navButtonLeft.addEventListener('click', this.scrollLeft) navButtonRight.addEventListener('click', this.scrollRight) } disconnectedCallback() { navButtonLeft.removeEventListener('click', this.scrollLeft) navButtonRight.removeEventListener('click', this.scrollRight) } }) //Strip option const stripOption = document.createElement('template') stripOption.innerHTML = ` ` customElements.define('strip-option', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(stripOption.content.cloneNode(true)) this._value this.radioButton = this.shadowRoot.querySelector('input') this.fireEvent = this.fireEvent.bind(this) this.handleKeyDown = this.handleKeyDown.bind(this) } get value() { return this._value } fireEvent() { this.dispatchEvent( new CustomEvent("option-clicked", { bubbles: true, composed: true, detail: { value: this._value } }) ) } handleKeyDown(e) { if (e.key === 'Enter' || e.key === 'Space') { this.fireEvent() } } connectedCallback() { this.setAttribute('role', 'option') this.setAttribute('tabindex', '0') this._value = this.getAttribute('value') this.addEventListener('click', this.fireEvent) this.addEventListener('keydown', this.handleKeyDown) } disconnectedCallback() { this.removeEventListener('click', this.fireEvent) this.removeEventListener('keydown', this.handleKeyDown) } })