From afaa99021b193cef60cb6354033e53de7c47839e Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 31 Oct 2024 21:05:59 +0000 Subject: [PATCH 1/2] fix: dead click fixes from watching in prod --- ...azy-loaded-dead-clicks-autocapture.test.ts | 228 ++++++++++-------- src/autocapture-utils.ts | 42 +--- src/autocapture.ts | 5 +- src/entrypoints/dead-clicks-autocapture.ts | 49 ++-- src/heatmaps.ts | 4 +- src/utils/element-utils.ts | 57 +++++ 6 files changed, 216 insertions(+), 169 deletions(-) diff --git a/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts b/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts index 95edc40f1..48486df62 100644 --- a/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts +++ b/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts @@ -139,6 +139,16 @@ describe('LazyLoadedDeadClicksAutocapture', () => { expect(lazyLoadedDeadClicksAutocapture['_clicks'].length).toBe(0) }) + it('ignores clicks on disabled nodes', () => { + const div = document.createElement('div') + div.setAttribute('disabled', 'true') + document.body.append(div) + + triggerMouseEvent(div, 'click') + + expect(lazyLoadedDeadClicksAutocapture['_clicks'].length).toBe(0) + }) + it.each(autocaptureCompatibleElements)('click on %s node is never a deadclick', (element) => { const el = document.createElement(element) document.body.append(el) @@ -211,32 +221,35 @@ describe('LazyLoadedDeadClicksAutocapture', () => { lazyLoadedDeadClicksAutocapture['_checkClicks']() expect(lazyLoadedDeadClicksAutocapture['_clicks']).toHaveLength(0) - expect(fakeInstance.capture).toHaveBeenCalledWith('$dead_click', { - // faked system timestamp isn't moving so this is negative - $dead_click_absolute_delay_ms: -900, - $dead_click_absolute_timeout: false, - $dead_click_event_timestamp: 900, - $dead_click_last_mutation_timestamp: undefined, - $dead_click_last_scroll_timestamp: undefined, - $dead_click_mutation_delay_ms: undefined, - $dead_click_mutation_timeout: false, - $dead_click_scroll_delay_ms: undefined, - $dead_click_scroll_timeout: false, - $dead_click_selection_changed_delay_ms: 100, - $dead_click_selection_changed_timeout: true, - timestamp: 900, - $ce_version: 1, - $el_text: 'text', - $elements: [ - { - $el_text: 'text', - nth_child: 2, - nth_of_type: 1, - tag_name: 'body', - }, - ], - $event_type: 'click', - }) + expect(fakeInstance.capture).toHaveBeenCalledWith( + '$dead_click', + { + // faked system timestamp isn't moving so this is negative + $dead_click_absolute_delay_ms: -900, + $dead_click_absolute_timeout: false, + $dead_click_event_timestamp: 900, + $dead_click_last_mutation_timestamp: undefined, + $dead_click_last_scroll_timestamp: undefined, + $dead_click_mutation_delay_ms: undefined, + $dead_click_mutation_timeout: false, + $dead_click_scroll_delay_ms: undefined, + $dead_click_scroll_timeout: false, + $dead_click_selection_changed_delay_ms: 100, + $dead_click_selection_changed_timeout: true, + $ce_version: 1, + $el_text: 'text', + $elements: [ + { + $el_text: 'text', + nth_child: 2, + nth_of_type: 1, + tag_name: 'body', + }, + ], + $event_type: 'click', + }, + { timestamp: new Date(900) } + ) }) it('click followed by a mutation after threshold, dead click', () => { @@ -250,32 +263,35 @@ describe('LazyLoadedDeadClicksAutocapture', () => { lazyLoadedDeadClicksAutocapture['_checkClicks']() expect(lazyLoadedDeadClicksAutocapture['_clicks']).toHaveLength(0) - expect(fakeInstance.capture).toHaveBeenCalledWith('$dead_click', { - // faked system timestamp isn't moving so this is negative - $dead_click_absolute_delay_ms: -900, - $dead_click_absolute_timeout: false, - $dead_click_event_timestamp: 900, - $dead_click_last_mutation_timestamp: 3401, - $dead_click_last_scroll_timestamp: undefined, - $dead_click_mutation_delay_ms: 2501, - $dead_click_mutation_timeout: true, - $dead_click_scroll_delay_ms: undefined, - $dead_click_scroll_timeout: false, - $dead_click_selection_changed_delay_ms: undefined, - $dead_click_selection_changed_timeout: false, - timestamp: 900, - $ce_version: 1, - $el_text: 'text', - $elements: [ - { - $el_text: 'text', - nth_child: 2, - nth_of_type: 1, - tag_name: 'body', - }, - ], - $event_type: 'click', - }) + expect(fakeInstance.capture).toHaveBeenCalledWith( + '$dead_click', + { + // faked system timestamp isn't moving so this is negative + $dead_click_absolute_delay_ms: -900, + $dead_click_absolute_timeout: false, + $dead_click_event_timestamp: 900, + $dead_click_last_mutation_timestamp: 3401, + $dead_click_last_scroll_timestamp: undefined, + $dead_click_mutation_delay_ms: 2501, + $dead_click_mutation_timeout: true, + $dead_click_scroll_delay_ms: undefined, + $dead_click_scroll_timeout: false, + $dead_click_selection_changed_delay_ms: undefined, + $dead_click_selection_changed_timeout: false, + $ce_version: 1, + $el_text: 'text', + $elements: [ + { + $el_text: 'text', + nth_child: 2, + nth_of_type: 1, + tag_name: 'body', + }, + ], + $event_type: 'click', + }, + { timestamp: new Date(900) } + ) }) it('click followed by a scroll after threshold, dead click', () => { @@ -290,31 +306,34 @@ describe('LazyLoadedDeadClicksAutocapture', () => { lazyLoadedDeadClicksAutocapture['_checkClicks']() expect(lazyLoadedDeadClicksAutocapture['_clicks']).toHaveLength(0) - expect(fakeInstance.capture).toHaveBeenCalledWith('$dead_click', { - // faked system timestamp isn't moving so this is negative - $dead_click_absolute_delay_ms: -900, - $dead_click_absolute_timeout: false, - $dead_click_event_timestamp: 900, - $dead_click_last_mutation_timestamp: undefined, - $dead_click_mutation_delay_ms: undefined, - $dead_click_mutation_timeout: false, - $dead_click_scroll_delay_ms: 2501, - $dead_click_scroll_timeout: true, - $dead_click_selection_changed_delay_ms: undefined, - $dead_click_selection_changed_timeout: false, - $ce_version: 1, - $el_text: 'text', - $elements: [ - { - $el_text: 'text', - nth_child: 2, - nth_of_type: 1, - tag_name: 'body', - }, - ], - $event_type: 'click', - timestamp: 900, - }) + expect(fakeInstance.capture).toHaveBeenCalledWith( + '$dead_click', + { + // faked system timestamp isn't moving so this is negative + $dead_click_absolute_delay_ms: -900, + $dead_click_absolute_timeout: false, + $dead_click_event_timestamp: 900, + $dead_click_last_mutation_timestamp: undefined, + $dead_click_mutation_delay_ms: undefined, + $dead_click_mutation_timeout: false, + $dead_click_scroll_delay_ms: 2501, + $dead_click_scroll_timeout: true, + $dead_click_selection_changed_delay_ms: undefined, + $dead_click_selection_changed_timeout: false, + $ce_version: 1, + $el_text: 'text', + $elements: [ + { + $el_text: 'text', + nth_child: 2, + nth_of_type: 1, + tag_name: 'body', + }, + ], + $event_type: 'click', + }, + { timestamp: new Date(900) } + ) }) it('click followed by nothing for too long, dead click', () => { @@ -325,35 +344,38 @@ describe('LazyLoadedDeadClicksAutocapture', () => { }) lazyLoadedDeadClicksAutocapture['_lastMutation'] = undefined - jest.setSystemTime(2501 + 900) + jest.setSystemTime(3001 + 900) lazyLoadedDeadClicksAutocapture['_checkClicks']() expect(lazyLoadedDeadClicksAutocapture['_clicks']).toHaveLength(0) - expect(fakeInstance.capture).toHaveBeenCalledWith('$dead_click', { - $dead_click_absolute_delay_ms: 2501, - $dead_click_absolute_timeout: true, - $dead_click_event_timestamp: 900, - $dead_click_last_mutation_timestamp: undefined, - $dead_click_last_scroll_timestamp: undefined, - $dead_click_mutation_delay_ms: undefined, - $dead_click_mutation_timeout: false, - $dead_click_scroll_delay_ms: undefined, - $dead_click_scroll_timeout: false, - $dead_click_selection_changed_delay_ms: undefined, - $dead_click_selection_changed_timeout: false, - $ce_version: 1, - $el_text: 'text', - $elements: [ - { - $el_text: 'text', - nth_child: 2, - nth_of_type: 1, - tag_name: 'body', - }, - ], - $event_type: 'click', - timestamp: 900, - }) + expect(fakeInstance.capture).toHaveBeenCalledWith( + '$dead_click', + { + $dead_click_absolute_delay_ms: 3001, + $dead_click_absolute_timeout: true, + $dead_click_event_timestamp: 900, + $dead_click_last_mutation_timestamp: undefined, + $dead_click_last_scroll_timestamp: undefined, + $dead_click_mutation_delay_ms: undefined, + $dead_click_mutation_timeout: false, + $dead_click_scroll_delay_ms: undefined, + $dead_click_scroll_timeout: false, + $dead_click_selection_changed_delay_ms: undefined, + $dead_click_selection_changed_timeout: false, + $ce_version: 1, + $el_text: 'text', + $elements: [ + { + $el_text: 'text', + nth_child: 2, + nth_of_type: 1, + tag_name: 'body', + }, + ], + $event_type: 'click', + }, + { timestamp: new Date(900) } + ) }) it('click not followed by anything within threshold, rescheduled for next check', () => { diff --git a/src/autocapture-utils.ts b/src/autocapture-utils.ts index c2e776f0b..00ca3883f 100644 --- a/src/autocapture-utils.ts +++ b/src/autocapture-utils.ts @@ -4,6 +4,7 @@ import { each, entries, includes, trim } from './utils' import { isArray, isNullish, isString, isUndefined } from './utils/type-utils' import { logger } from './utils/logger' import { window } from './utils/globals' +import { isDocumentFragment, isElementNode, isTag, isTextNode } from './utils/element-utils' export function splitClassString(s: string): string[] { return s ? trim(s).split(/\s+/) : [] @@ -94,47 +95,6 @@ export function getEventTarget(e: Event): Element | null { } } -/* - * Check whether an element has nodeType Node.ELEMENT_NODE - * @param {Element} el - element to check - * @returns {boolean} whether el is of the correct nodeType - */ -export function isElementNode(el: Node | Element | undefined | null): el is Element { - return !!el && el.nodeType === 1 // Node.ELEMENT_NODE - use integer constant for browser portability -} - -/* - * Check whether an element is of a given tag type. - * Due to potential reference discrepancies (such as the webcomponents.js polyfill), - * we want to match tagNames instead of specific references because something like - * element === document.body won't always work because element might not be a native - * element. - * @param {Element} el - element to check - * @param {string} tag - tag name (e.g., "div") - * @returns {boolean} whether el is of the given tag type - */ -export function isTag(el: Element | undefined | null, tag: string): el is HTMLElement { - return !!el && !!el.tagName && el.tagName.toLowerCase() === tag.toLowerCase() -} - -/* - * Check whether an element has nodeType Node.TEXT_NODE - * @param {Element} el - element to check - * @returns {boolean} whether el is of the correct nodeType - */ -export function isTextNode(el: Element | undefined | null): el is HTMLElement { - return !!el && el.nodeType === 3 // Node.TEXT_NODE - use integer constant for browser portability -} - -/* - * Check whether an element has nodeType Node.DOCUMENT_FRAGMENT_NODE - * @param {Element} el - element to check - * @returns {boolean} whether el is of the correct nodeType - */ -export function isDocumentFragment(el: Element | ParentNode | undefined | null): el is DocumentFragment { - return !!el && el.nodeType === 11 // Node.DOCUMENT_FRAGMENT_NODE - use integer constant for browser portability -} - export const autocaptureCompatibleElements = ['a', 'button', 'form', 'input', 'select', 'textarea', 'label'] /* diff --git a/src/autocapture.ts b/src/autocapture.ts index 96262452d..8fc25d580 100644 --- a/src/autocapture.ts +++ b/src/autocapture.ts @@ -7,11 +7,7 @@ import { getEventTarget, getSafeText, isAngularStyleAttr, - isDocumentFragment, - isElementNode, isSensitiveElement, - isTag, - isTextNode, makeSafeText, shouldCaptureDomEvent, shouldCaptureElement, @@ -27,6 +23,7 @@ import { isBoolean, isFunction, isNull, isObject } from './utils/type-utils' import { logger } from './utils/logger' import { document, window } from './utils/globals' import { convertToURL } from './utils/request-utils' +import { isDocumentFragment, isElementNode, isTag, isTextNode } from './utils/element-utils' const COPY_AUTOCAPTURE_EVENT = '$copy_autocapture' diff --git a/src/entrypoints/dead-clicks-autocapture.ts b/src/entrypoints/dead-clicks-autocapture.ts index 80da3d6c7..6033835f4 100644 --- a/src/entrypoints/dead-clicks-autocapture.ts +++ b/src/entrypoints/dead-clicks-autocapture.ts @@ -1,10 +1,10 @@ import { assignableWindow, LazyLoadedDeadClicksAutocaptureInterface } from '../utils/globals' import { PostHog } from '../posthog-core' import { isNull, isNumber, isUndefined } from '../utils/type-utils' -import { autocaptureCompatibleElements, getEventTarget, isElementNode, isTag } from '../autocapture-utils' +import { autocaptureCompatibleElements, getEventTarget } from '../autocapture-utils' import { DeadClicksAutoCaptureConfig, Properties } from '../types' import { autocapturePropertiesForElement } from '../autocapture' -import { isElementInToolbar } from '../utils/element-utils' +import { isDisabledElement, isElementInToolbar, isElementNode, isTag } from '../utils/element-utils' const DEFAULT_CONFIG: Required = { element_attribute_ignorelist: [], @@ -157,6 +157,10 @@ class LazyLoadedDeadClicksAutocapture implements LazyLoadedDeadClicksAutocapture return true } + if (isDisabledElement(click.node)) { + return true + } + const alreadyClickedInLastSecond = this._clicks.some((c) => { return c.node === click.node && Math.abs(c.timestamp - click.timestamp) < 1000 }) @@ -205,7 +209,9 @@ class LazyLoadedDeadClicksAutocapture implements LazyLoadedDeadClicksAutocapture this._config.selection_change_threshold_ms ) const mutationTimeout = checkTimeout(click.mutationDelayMs, this._config.mutation_threshold_ms) - const absoluteTimeout = checkTimeout(click.absoluteDelayMs, this._config.mutation_threshold_ms) + // we want to timeout eventually even if nothing else catches it... + // we leave a little longer than the maximum threshold to give the other checks a chance to catch it + const absoluteTimeout = checkTimeout(click.absoluteDelayMs, this._config.mutation_threshold_ms * 1.1) const hadScroll = isNumber(click.scrollDelayMs) && click.scrollDelayMs < this._config.scroll_threshold_ms const hadMutation = @@ -244,22 +250,27 @@ class LazyLoadedDeadClicksAutocapture implements LazyLoadedDeadClicksAutocapture private _captureDeadClick(click: Click, properties: Properties) { // TODO need to check safe and captur-able as with autocapture // TODO autocaputure config - this.instance.capture('$dead_click', { - ...properties, - ...autocapturePropertiesForElement(click.node, { - e: click.originalEvent, - maskAllElementAttributes: this.instance.config.mask_all_element_attributes, - maskAllText: this.instance.config.mask_all_text, - elementAttributeIgnoreList: this._config.element_attribute_ignorelist, - // TRICKY: it appears that we were moving to elementsChainAsString, but the UI still depends on elements, so :shrug: - elementsChainAsString: false, - }).props, - $dead_click_scroll_delay_ms: click.scrollDelayMs, - $dead_click_mutation_delay_ms: click.mutationDelayMs, - $dead_click_absolute_delay_ms: click.absoluteDelayMs, - $dead_click_selection_changed_delay_ms: click.selectionChangedDelayMs, - timestamp: click.timestamp, - }) + this.instance.capture( + '$dead_click', + { + ...properties, + ...autocapturePropertiesForElement(click.node, { + e: click.originalEvent, + maskAllElementAttributes: this.instance.config.mask_all_element_attributes, + maskAllText: this.instance.config.mask_all_text, + elementAttributeIgnoreList: this._config.element_attribute_ignorelist, + // TRICKY: it appears that we were moving to elementsChainAsString, but the UI still depends on elements, so :shrug: + elementsChainAsString: false, + }).props, + $dead_click_scroll_delay_ms: click.scrollDelayMs, + $dead_click_mutation_delay_ms: click.mutationDelayMs, + $dead_click_absolute_delay_ms: click.absoluteDelayMs, + $dead_click_selection_changed_delay_ms: click.selectionChangedDelayMs, + }, + { + timestamp: new Date(click.timestamp), + } + ) } } diff --git a/src/heatmaps.ts b/src/heatmaps.ts index 6f9ac31c8..ca6b99d5a 100644 --- a/src/heatmaps.ts +++ b/src/heatmaps.ts @@ -4,11 +4,11 @@ import { DecideResponse, Properties } from './types' import { PostHog } from './posthog-core' import { document, window } from './utils/globals' -import { getEventTarget, getParentElement, isElementNode, isTag } from './autocapture-utils' +import { getEventTarget, getParentElement } from './autocapture-utils' import { HEATMAPS_ENABLED_SERVER_SIDE } from './constants' import { isEmptyObject, isObject, isUndefined } from './utils/type-utils' import { logger } from './utils/logger' -import { isElementInToolbar } from './utils/element-utils' +import { isElementInToolbar, isElementNode, isTag } from './utils/element-utils' const DEFAULT_FLUSH_INTERVAL = 5000 const HEATMAPS = 'heatmaps' diff --git a/src/utils/element-utils.ts b/src/utils/element-utils.ts index 02cf7eaca..497aaa125 100644 --- a/src/utils/element-utils.ts +++ b/src/utils/element-utils.ts @@ -1,6 +1,63 @@ import { TOOLBAR_ID } from '../constants' +import { window } from './globals' export function isElementInToolbar(el: Element): boolean { // NOTE: .closest is not supported in IE11 hence the operator check return el.id === TOOLBAR_ID || !!el.closest?.('#' + TOOLBAR_ID) } + +/** + * checks if the element or one of its ancestors is disabled + * has the aria-disabled attribute set to true + * is a fieldset with disabled set + * or has pointer-events set to none + */ +export function isDisabledElement(el: Element): boolean { + if (!isElementNode(el)) { + // if not an element, it can't be disabled + return false + } + const disabledByStyle = window?.getComputedStyle(el).pointerEvents === 'none' + return !!el.closest?.('[disabled],[aria-disabled="true"],fieldset[disabled]') || disabledByStyle +} + +/* + * Check whether an element has nodeType Node.ELEMENT_NODE + * @param {Element} el - element to check + * @returns {boolean} whether el is of the correct nodeType + */ +export function isElementNode(el: Node | Element | undefined | null): el is Element { + return !!el && el.nodeType === 1 // Node.ELEMENT_NODE - use integer constant for browser portability +} + +/* + * Check whether an element is of a given tag type. + * Due to potential reference discrepancies (such as the webcomponents.js polyfill), + * we want to match tagNames instead of specific references because something like + * element === document.body won't always work because element might not be a native + * element. + * @param {Element} el - element to check + * @param {string} tag - tag name (e.g., "div") + * @returns {boolean} whether el is of the given tag type + */ +export function isTag(el: Element | undefined | null, tag: string): el is HTMLElement { + return !!el && !!el.tagName && el.tagName.toLowerCase() === tag.toLowerCase() +} + +/* + * Check whether an element has nodeType Node.TEXT_NODE + * @param {Element} el - element to check + * @returns {boolean} whether el is of the correct nodeType + */ +export function isTextNode(el: Element | undefined | null): el is HTMLElement { + return !!el && el.nodeType === 3 // Node.TEXT_NODE - use integer constant for browser portability +} + +/* + * Check whether an element has nodeType Node.DOCUMENT_FRAGMENT_NODE + * @param {Element} el - element to check + * @returns {boolean} whether el is of the correct nodeType + */ +export function isDocumentFragment(el: Element | ParentNode | undefined | null): el is DocumentFragment { + return !!el && el.nodeType === 11 // Node.DOCUMENT_FRAGMENT_NODE - use integer constant for browser portability +} From 01154fd8766467e252f1661e5b1b504c7a0fc0ef Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 1 Nov 2024 08:57:25 +0000 Subject: [PATCH 2/2] don't ignore disabled --- .../lazy-loaded-dead-clicks-autocapture.test.ts | 10 ---------- src/entrypoints/dead-clicks-autocapture.ts | 6 +----- src/utils/element-utils.ts | 16 ---------------- 3 files changed, 1 insertion(+), 31 deletions(-) diff --git a/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts b/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts index 48486df62..7588cb264 100644 --- a/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts +++ b/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts @@ -139,16 +139,6 @@ describe('LazyLoadedDeadClicksAutocapture', () => { expect(lazyLoadedDeadClicksAutocapture['_clicks'].length).toBe(0) }) - it('ignores clicks on disabled nodes', () => { - const div = document.createElement('div') - div.setAttribute('disabled', 'true') - document.body.append(div) - - triggerMouseEvent(div, 'click') - - expect(lazyLoadedDeadClicksAutocapture['_clicks'].length).toBe(0) - }) - it.each(autocaptureCompatibleElements)('click on %s node is never a deadclick', (element) => { const el = document.createElement(element) document.body.append(el) diff --git a/src/entrypoints/dead-clicks-autocapture.ts b/src/entrypoints/dead-clicks-autocapture.ts index 6033835f4..f60c33177 100644 --- a/src/entrypoints/dead-clicks-autocapture.ts +++ b/src/entrypoints/dead-clicks-autocapture.ts @@ -4,7 +4,7 @@ import { isNull, isNumber, isUndefined } from '../utils/type-utils' import { autocaptureCompatibleElements, getEventTarget } from '../autocapture-utils' import { DeadClicksAutoCaptureConfig, Properties } from '../types' import { autocapturePropertiesForElement } from '../autocapture' -import { isDisabledElement, isElementInToolbar, isElementNode, isTag } from '../utils/element-utils' +import { isElementInToolbar, isElementNode, isTag } from '../utils/element-utils' const DEFAULT_CONFIG: Required = { element_attribute_ignorelist: [], @@ -157,10 +157,6 @@ class LazyLoadedDeadClicksAutocapture implements LazyLoadedDeadClicksAutocapture return true } - if (isDisabledElement(click.node)) { - return true - } - const alreadyClickedInLastSecond = this._clicks.some((c) => { return c.node === click.node && Math.abs(c.timestamp - click.timestamp) < 1000 }) diff --git a/src/utils/element-utils.ts b/src/utils/element-utils.ts index 497aaa125..9a9b43768 100644 --- a/src/utils/element-utils.ts +++ b/src/utils/element-utils.ts @@ -1,26 +1,10 @@ import { TOOLBAR_ID } from '../constants' -import { window } from './globals' export function isElementInToolbar(el: Element): boolean { // NOTE: .closest is not supported in IE11 hence the operator check return el.id === TOOLBAR_ID || !!el.closest?.('#' + TOOLBAR_ID) } -/** - * checks if the element or one of its ancestors is disabled - * has the aria-disabled attribute set to true - * is a fieldset with disabled set - * or has pointer-events set to none - */ -export function isDisabledElement(el: Element): boolean { - if (!isElementNode(el)) { - // if not an element, it can't be disabled - return false - } - const disabledByStyle = window?.getComputedStyle(el).pointerEvents === 'none' - return !!el.closest?.('[disabled],[aria-disabled="true"],fieldset[disabled]') || disabledByStyle -} - /* * Check whether an element has nodeType Node.ELEMENT_NODE * @param {Element} el - element to check