@semantic-ui/query
Advanced tools
Comparing version 0.1.2 to 0.1.3
@@ -26,5 +26,5 @@ { | ||
"dependencies": { | ||
"@semantic-ui/utils": "^0.1.2" | ||
"@semantic-ui/utils": "^0.1.3" | ||
}, | ||
"version": "0.1.2" | ||
"version": "0.1.3" | ||
} |
138
src/query.js
@@ -82,5 +82,4 @@ import { isPlainObject, isString, isArray, isDOM, isFunction, findIndex, inArray, isClient, isObject, each } from '@semantic-ui/utils'; | ||
/* Note this is a naive implementation for performance reasons | ||
we will add all elements across shadow root boundaries but without | ||
matching complex selectors that would match ACROSS shadow root boundaries | ||
/* we will add all elements across shadow root boundaries while matching | ||
intermediate selectors at shadow boundaries | ||
*/ | ||
@@ -116,3 +115,3 @@ querySelectorAllDeep(root, selector, includeRoot = true) { | ||
const addElements = (node) => { | ||
const addElements = (node, selector) => { | ||
if(domSelector && (node === selector || node.contains)) { | ||
@@ -130,4 +129,18 @@ if(node.contains(selector)) { | ||
const findElements = (node, query) => { | ||
const getRemainingSelector = (el, selector) => { | ||
const parts = selector.split(' '); | ||
let partialSelector; | ||
let remainingSelector; | ||
each(parts, (part, index) => { | ||
partialSelector = parts.slice(0, index + 1).join(' '); | ||
if(el.matches(partialSelector)) { | ||
remainingSelector = parts.slice(index + 1).join(' '); | ||
return; | ||
} | ||
}); | ||
return remainingSelector || selector; | ||
}; | ||
const findElements = (node, selector, query) => { | ||
// if we are querying for a DOM element we can stop searching once we've found it | ||
@@ -141,3 +154,3 @@ if(domFound) { | ||
if(query === true) { | ||
addElements(node); | ||
addElements(node, selector); | ||
queriedRoot = true; | ||
@@ -148,14 +161,16 @@ } | ||
if (node.nodeType === Node.ELEMENT_NODE && node.shadowRoot) { | ||
addElements(node.shadowRoot); | ||
findElements(node.shadowRoot, !queriedRoot); | ||
selector = getRemainingSelector(node, selector); | ||
addElements(node.shadowRoot, selector); | ||
findElements(node.shadowRoot, selector, !queriedRoot); | ||
} | ||
if(node.assignedNodes) { | ||
node.assignedNodes().forEach((node) => findElements(node, queriedRoot)); | ||
selector = getRemainingSelector(node, selector); | ||
node.assignedNodes().forEach((node) => findElements(node, selector, queriedRoot)); | ||
} | ||
if (node.childNodes.length) { | ||
node.childNodes.forEach((node) => findElements(node, queriedRoot)); | ||
node.childNodes.forEach((node) => findElements(node, selector, queriedRoot)); | ||
} | ||
}; | ||
findElements(root); | ||
findElements(root, selector); | ||
return [...new Set(elements)]; | ||
@@ -341,3 +356,3 @@ } | ||
on(eventName, targetSelectorOrHandler, handlerOrOptions, options) { | ||
on(eventNames, targetSelectorOrHandler, handlerOrOptions, options) { | ||
const eventHandlers = []; | ||
@@ -359,55 +374,58 @@ | ||
const abortController = options?.abortController || new AbortController(); | ||
const eventSettings = options?.eventSettings || {}; | ||
const signal = abortController.signal; | ||
// Split event names by spaces and attach handlers for each | ||
const events = eventNames.split(' ').filter(Boolean); | ||
this.each((el) => { | ||
let delegateHandler; | ||
if (targetSelector) { | ||
delegateHandler = (event) => { | ||
let target; | ||
// if this event is composed from a web component | ||
// this is required to get the original path | ||
if (event.composed && event.composedPath) { | ||
events.forEach(eventName => { | ||
const abortController = options?.abortController || new AbortController(); | ||
const eventSettings = options?.eventSettings || {}; | ||
const signal = abortController.signal; | ||
// look through composed path bubbling into the attached element to see if any match target | ||
let path = event.composedPath(); | ||
const elIndex = findIndex(path, thisEl => thisEl == el); | ||
path = path.slice(0, elIndex); | ||
target = path.find(el => el instanceof Element && el.matches && el.matches(targetSelector)); | ||
this.each((el) => { | ||
let delegateHandler; | ||
if (targetSelector) { | ||
delegateHandler = (event) => { | ||
let target; | ||
// if this event is composed from a web component | ||
// this is required to get the original path | ||
if (event.composed && event.composedPath) { | ||
// look through composed path bubbling into the attached element to see if any match target | ||
let path = event.composedPath(); | ||
const elIndex = findIndex(path, thisEl => thisEl == el); | ||
path = path.slice(0, elIndex); | ||
target = path.find(el => el instanceof Element && el.matches && el.matches(targetSelector)); | ||
} | ||
else if(targetSelector) { | ||
// keep things simple for most basic uses | ||
target = event.target.closest(targetSelector); | ||
} | ||
else { | ||
// no target selector | ||
target = event.target; | ||
} | ||
} | ||
else if(targetSelector) { | ||
// keep things simple for most basic uses | ||
target = event.target.closest(targetSelector); | ||
} | ||
else { | ||
// no target selector | ||
target = event.target; | ||
} | ||
if (target) { | ||
// If a matching target is found, call the handler with the correct context | ||
handler.call(target, event); | ||
} | ||
}; | ||
} | ||
const eventListener = delegateHandler || handler; | ||
if (target) { | ||
// If a matching target is found, call the handler with the correct context | ||
handler.call(target, event); | ||
} | ||
// will cause illegal invocation if used from proxy object | ||
const domEL = (this.isGlobal) ? globalThis : el; | ||
if (domEL.addEventListener) { | ||
domEL.addEventListener(eventName, eventListener, { signal, ...eventSettings }); | ||
} | ||
const eventHandler = { | ||
el, | ||
eventName, | ||
eventListener, | ||
abortController, | ||
delegated: targetSelector !== undefined, | ||
handler, | ||
abort: (reason) => abortController.abort(reason), | ||
}; | ||
} | ||
const eventListener = delegateHandler || handler; | ||
// will cause illegal invocation if used from proxy object | ||
const domEL = (this.isGlobal) ? globalThis : el; | ||
if (domEL.addEventListener) { | ||
domEL.addEventListener(eventName, eventListener, { signal, ...eventSettings }); | ||
} | ||
const eventHandler = { | ||
el, | ||
eventName, | ||
eventListener, | ||
abortController, | ||
delegated: targetSelector !== undefined, | ||
handler, | ||
abort: (reason) => abortController.abort(reason), | ||
}; | ||
eventHandlers.push(eventHandler); | ||
eventHandlers.push(eventHandler); | ||
}); | ||
}); | ||
@@ -414,0 +432,0 @@ |
import { describe, vi, beforeAll, beforeEach, afterEach, afterAll, it, expect } from 'vitest'; | ||
import { $ } from '@semantic-ui/query'; | ||
import { $, $$ } from '@semantic-ui/query'; | ||
@@ -156,2 +156,156 @@ describe('query', () => { | ||
}); | ||
describe('Shadow DOM Traversal', () => { | ||
beforeAll(() => { | ||
// Register custom elements once in this suite | ||
// Open Shadow DOM component | ||
class TestDOMInnerComponent extends HTMLElement { | ||
constructor() { | ||
super(); | ||
const shadow = this.attachShadow({ mode: 'open' }); | ||
const titleDiv = document.createElement('div'); | ||
titleDiv.className = 'title'; | ||
titleDiv.textContent = 'Inside Nested Component'; | ||
shadow.appendChild(titleDiv); | ||
} | ||
connectedCallback() { | ||
this.dispatchEvent(new CustomEvent('initialized', { bubbles: true })); | ||
} | ||
} | ||
customElements.define('test-dom-inner', TestDOMInnerComponent); | ||
// Nested component | ||
class TestDOMComponent extends HTMLElement { | ||
constructor() { | ||
super(); | ||
const shadow = this.attachShadow({ mode: 'open' }); | ||
const myComponent = document.createElement('test-dom-inner'); | ||
const titleDiv = document.createElement('div'); | ||
titleDiv.className = 'title'; | ||
titleDiv.textContent = 'Inside Web Component'; | ||
shadow.appendChild(myComponent); | ||
shadow.appendChild(titleDiv); | ||
} | ||
async connectedCallback() { | ||
const el = this.shadowRoot.querySelector('test-dom-inner'); | ||
if(el) { | ||
el.addEventListener('initialized', () => { | ||
this.dispatchEvent(new CustomEvent('initialized', { bubbles: true })); | ||
}); | ||
} | ||
} | ||
} | ||
customElements.define('test-dom', TestDOMComponent); | ||
}); | ||
afterEach(() => { | ||
// Clean up after each test | ||
document.body.innerHTML = ''; | ||
}); | ||
it('should return outer elements and nested shadow elements', async () => { | ||
// Create an element with class 'title' outside | ||
const outsideDiv = document.createElement('div'); | ||
outsideDiv.className = 'title'; | ||
outsideDiv.textContent = 'Outside Shadow DOM'; | ||
document.body.appendChild(outsideDiv); | ||
const testComponent = document.createElement('test-dom'); | ||
// Create a Promise that resolves when the 'initialized' event is dispatched | ||
const componentInit = new Promise((resolve) => { | ||
testComponent.addEventListener('initialized', resolve); | ||
}); | ||
document.body.appendChild(testComponent); | ||
// Wait for the component to initialize | ||
await componentInit; | ||
// selects outer title, nested component title and inner component title | ||
const $allElements = $$('.title'); | ||
expect($allElements.length).toBe(3); | ||
}); | ||
it('should select nested items', async () => { | ||
// Create an element with class 'title' outside | ||
const outsideDiv = document.createElement('div'); | ||
outsideDiv.className = 'title'; | ||
outsideDiv.textContent = 'Outside Shadow DOM'; | ||
document.body.appendChild(outsideDiv); | ||
const testComponent = document.createElement('test-dom'); | ||
// Create a Promise that resolves when the 'initialized' event is dispatched | ||
const componentInit = new Promise((resolve) => { | ||
testComponent.addEventListener('initialized', resolve); | ||
}); | ||
document.body.appendChild(testComponent); | ||
// Wait for the component to initialize | ||
await componentInit; | ||
// selects nested component title and inner component title | ||
const $elements = $$('test-dom .title'); | ||
expect($elements.length).toBe(2); | ||
}); | ||
it('should not match items not at shadow root', async () => { | ||
// Create an element with class 'title' outside | ||
const outsideDiv = document.createElement('div'); | ||
outsideDiv.className = 'title'; | ||
outsideDiv.textContent = 'Outside Shadow DOM'; | ||
document.body.appendChild(outsideDiv); | ||
const testComponent = document.createElement('test-dom'); | ||
// Create a Promise that resolves when the 'initialized' event is dispatched | ||
const componentInit = new Promise((resolve) => { | ||
testComponent.addEventListener('initialized', resolve); | ||
}); | ||
document.body.appendChild(testComponent); | ||
// Wait for the component to initialize | ||
await componentInit; | ||
// selects nested component title | ||
const $elements = $$('not-component .title'); | ||
expect($elements.length).toBe(0); | ||
}); | ||
it('should select deeply nested items', async () => { | ||
// Create an element with class 'title' outside | ||
const outsideDiv = document.createElement('div'); | ||
outsideDiv.className = 'title'; | ||
outsideDiv.textContent = 'Outside Shadow DOM'; | ||
document.body.appendChild(outsideDiv); | ||
const testComponent = document.createElement('test-dom'); | ||
// Create a Promise that resolves when the 'initialized' event is dispatched | ||
const componentInit = new Promise((resolve) => { | ||
testComponent.addEventListener('initialized', resolve); | ||
}); | ||
document.body.appendChild(testComponent); | ||
// Wait for the component to initialize | ||
await componentInit; | ||
// selects nested component title | ||
const $innerElements = $$('test-dom test-dom-inner .title'); | ||
expect($innerElements.length).toBe(1); | ||
}); | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
import { describe, beforeEach, afterEach, expect, it, vi } from 'vitest'; | ||
import { describe, beforeEach, afterEach, beforeAll, expect, it, vi } from 'vitest'; | ||
import { $, $$, Query, exportGlobals, restoreGlobals, useAlias } from '@semantic-ui/query'; | ||
@@ -672,2 +672,111 @@ | ||
it('should attach multiple event handlers when passing space-separated events', () => { | ||
const div = document.createElement('div'); | ||
document.body.appendChild(div); | ||
const callback = vi.fn(); | ||
$('div').on('mouseup touchmove', callback); | ||
div.dispatchEvent(new Event('mouseup')); | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
div.dispatchEvent(new Event('touchmove')); | ||
expect(callback).toHaveBeenCalledTimes(2); | ||
}); | ||
it('should handle multiple events with delegation', () => { | ||
const div = document.createElement('div'); | ||
const span = document.createElement('span'); | ||
div.appendChild(span); | ||
document.body.appendChild(div); | ||
const callback = vi.fn(); | ||
$('div').on('mouseup touchmove', 'span', callback); | ||
span.dispatchEvent(new Event('mouseup', { bubbles: true })); | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
span.dispatchEvent(new Event('touchmove', { bubbles: true })); | ||
expect(callback).toHaveBeenCalledTimes(2); | ||
}); | ||
it('should handle multiple events with options', () => { | ||
const div = document.createElement('div'); | ||
document.body.appendChild(div); | ||
const callback = vi.fn(); | ||
const abortController = new AbortController(); | ||
$('div').on('mouseup touchmove', callback, { abortController }); | ||
div.dispatchEvent(new Event('mouseup')); | ||
div.dispatchEvent(new Event('touchmove')); | ||
expect(callback).toHaveBeenCalledTimes(2); | ||
abortController.abort(); | ||
div.dispatchEvent(new Event('mouseup')); | ||
div.dispatchEvent(new Event('touchmove')); | ||
expect(callback).toHaveBeenCalledTimes(2); // Count shouldn't increase | ||
}); | ||
it('should handle multiple events with delegation and options', () => { | ||
const div = document.createElement('div'); | ||
const span = document.createElement('span'); | ||
div.appendChild(span); | ||
document.body.appendChild(div); | ||
const callback = vi.fn(); | ||
const abortController = new AbortController(); | ||
$('div').on('mouseup touchmove', 'span', callback, { abortController }); | ||
span.dispatchEvent(new Event('mouseup', { bubbles: true })); | ||
span.dispatchEvent(new Event('touchmove', { bubbles: true })); | ||
expect(callback).toHaveBeenCalledTimes(2); | ||
abortController.abort(); | ||
span.dispatchEvent(new Event('mouseup', { bubbles: true })); | ||
span.dispatchEvent(new Event('touchmove', { bubbles: true })); | ||
expect(callback).toHaveBeenCalledTimes(2); // Count shouldn't increase | ||
}); | ||
it('should handle empty spaces in event string', () => { | ||
const div = document.createElement('div'); | ||
document.body.appendChild(div); | ||
const callback = vi.fn(); | ||
$('div').on(' mouseup touchmove ', callback); | ||
div.dispatchEvent(new Event('mouseup')); | ||
div.dispatchEvent(new Event('touchmove')); | ||
expect(callback).toHaveBeenCalledTimes(2); | ||
}); | ||
it('should return handlers for all events when returnHandler is true', () => { | ||
const div = document.createElement('div'); | ||
document.body.appendChild(div); | ||
const callback = vi.fn(); | ||
const handlers = $('div').on('mouseup touchmove', callback, { returnHandler: true }); | ||
expect(Array.isArray(handlers)).toBe(true); | ||
expect(handlers.length).toBe(2); | ||
expect(handlers[0].eventName).toBe('mouseup'); | ||
expect(handlers[1].eventName).toBe('touchmove'); | ||
}); | ||
it('should properly remove specific events using off()', () => { | ||
const div = document.createElement('div'); | ||
document.body.appendChild(div); | ||
const callback = vi.fn(); | ||
$('div').on('mouseup touchmove', callback); | ||
div.dispatchEvent(new Event('mouseup')); | ||
div.dispatchEvent(new Event('touchmove')); | ||
expect(callback).toHaveBeenCalledTimes(2); | ||
$('div').off('mouseup'); | ||
div.dispatchEvent(new Event('mouseup')); | ||
div.dispatchEvent(new Event('touchmove')); | ||
expect(callback).toHaveBeenCalledTimes(3); // Only touchmove should trigger | ||
}); | ||
}); | ||
@@ -674,0 +783,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
103680
2551
Updated@semantic-ui/utils@^0.1.3