marker-dom.mjs 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. /** overlay 的最小 DOM adapter:只验证节点/样式契约,不模拟浏览器布局或动画。 */
  2. export function markerTestDocument() {
  3. class Element {
  4. constructor(tag) {
  5. this.tagName = tag
  6. this.nodeType = 1
  7. this.children = []
  8. this.dataset = {}
  9. this.attributes = {}
  10. this.className = ''
  11. this.listeners = new Map()
  12. this.style = { setProperty(key, value) { this[key] = value } }
  13. this.classList = { toggle: (name, enabled) => {
  14. const names = new Set(this.className.split(' ').filter(Boolean))
  15. if (enabled) names.add(name)
  16. else names.delete(name)
  17. this.className = [...names].join(' ')
  18. } }
  19. }
  20. get parentElement() { return this.parentNode }
  21. get isConnected() { return this.connected === true || Boolean(this.parentNode?.isConnected) }
  22. appendChild(child) {
  23. child.parentNode?.removeChild(child)
  24. child.parentNode = this
  25. this.children.push(child)
  26. return child
  27. }
  28. removeChild(child) {
  29. this.children = this.children.filter((item) => item !== child)
  30. child.parentNode = null
  31. return child
  32. }
  33. remove() { this.parentNode?.removeChild(this) }
  34. set innerHTML(value) {
  35. // overlay 的水滴 SVG 占位;不解析路径、不伪造像素证据。
  36. this.html = value
  37. this.firstChild = new Element('svg')
  38. this.firstChild.inner = new Element('image')
  39. }
  40. setAttribute(key, value) { this.attributes[key] = value }
  41. querySelector(selector) { return selector === '[data-marker-overlay-inner]' ? this.inner : null }
  42. addEventListener(type, fn) {
  43. const listeners = this.listeners.get(type) || []
  44. this.listeners.set(type, [...listeners, fn])
  45. }
  46. removeEventListener(type, fn) {
  47. this.listeners.set(type, (this.listeners.get(type) || []).filter((item) => item !== fn))
  48. }
  49. contains(node) { return node === this || this.children.some((child) => child.contains(node)) }
  50. closest(selector) {
  51. return this.className.split(' ').includes(selector.slice(1)) ? this : this.parentNode?.closest(selector)
  52. }
  53. getBoundingClientRect() { return { left: 0, top: 0, width: 1000, height: 800 } }
  54. nodes() { return [this, ...this.children.flatMap((child) => child.nodes())] }
  55. fire(type, target) {
  56. for (const fn of this.listeners.get(type) || []) fn({ target, clientX: 100, clientY: 200, stopPropagation() {} })
  57. }
  58. }
  59. const head = new Element('head')
  60. const host = new Element('main')
  61. head.connected = host.connected = true
  62. return {
  63. head, host,
  64. createElement: (tag) => new Element(tag),
  65. getElementById: (id) => head.nodes().find((node) => node.id === id) || null,
  66. }
  67. }