hexo-theme-stellar 1.42.1 → 1.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.claude/skills/stellar-theme-dev/SKILL.md +1 -0
  2. package/CHANGELOG.md +41 -0
  3. package/THIRD-PARTY-NOTICES.md +28 -0
  4. package/_config.yml +17 -7
  5. package/_data/icons.yml +10 -9
  6. package/languages/en.yml +11 -0
  7. package/languages/zh-CN.yml +11 -0
  8. package/languages/zh-TW.yml +11 -0
  9. package/layout/_partial/comments/artalk/layout.ejs +1 -1
  10. package/layout/_partial/comments/beaudar/layout.ejs +1 -1
  11. package/layout/_partial/comments/giscus/layout.ejs +1 -1
  12. package/layout/_partial/comments/twikoo/layout.ejs +1 -1
  13. package/layout/_partial/comments/utterances/layout.ejs +1 -1
  14. package/layout/_partial/comments/waline/layout.ejs +1 -1
  15. package/layout/_partial/components/collection-item.ejs +44 -0
  16. package/layout/_partial/components/collection.ejs +26 -0
  17. package/layout/_partial/components/widget-frame.ejs +35 -0
  18. package/layout/_partial/cover/wiki_cover.ejs +82 -26
  19. package/layout/_partial/dropdown.ejs +62 -0
  20. package/layout/_partial/head.ejs +9 -0
  21. package/layout/_partial/main/navbar/breadcrumb/blog.ejs +1 -1
  22. package/layout/_partial/main/post_list/archive_item.ejs +12 -0
  23. package/layout/_partial/main/post_list/topic_card.ejs +21 -9
  24. package/layout/_partial/main/post_list/wiki_card.ejs +49 -10
  25. package/layout/_partial/scripts/defines.ejs +2 -8
  26. package/layout/_partial/scripts/lazyload.ejs +0 -3
  27. package/layout/_partial/scripts.ejs +1 -0
  28. package/layout/_partial/sidebar/index_leftbar.ejs +12 -2
  29. package/layout/_partial/sidebar/logo.ejs +1 -1
  30. package/layout/_partial/sidebar/menu.ejs +21 -23
  31. package/layout/_partial/widgets/components/linklist.ejs +23 -21
  32. package/layout/_partial/widgets/linklist.ejs +7 -12
  33. package/layout/_partial/widgets/recent.ejs +52 -41
  34. package/layout/_partial/widgets/related.ejs +47 -36
  35. package/layout/_partial/widgets/tagtree.ejs +70 -54
  36. package/layout/_partial/widgets/toc.ejs +37 -62
  37. package/layout/_partial/widgets/tree.ejs +38 -51
  38. package/layout/_plugins/mermaid.ejs +3 -4
  39. package/layout/archive.ejs +11 -20
  40. package/layout/collection-preview.ejs +60 -0
  41. package/layout/index_topic.ejs +12 -6
  42. package/layout/index_wiki.ejs +49 -3
  43. package/layout/layout.ejs +4 -3
  44. package/package.json +1 -1
  45. package/scripts/events/index.js +0 -2
  46. package/scripts/generators/stellar-icons.js +1 -1
  47. package/scripts/lib/subtitle.js +5 -1
  48. package/scripts/tags/index.js +1 -0
  49. package/scripts/tags/lib/about.js +1 -1
  50. package/scripts/tags/lib/albums.js +1 -1
  51. package/scripts/tags/lib/banner.js +1 -1
  52. package/scripts/tags/lib/dropdown.js +64 -0
  53. package/scripts/tags/lib/friends.js +1 -1
  54. package/scripts/tags/lib/gallery.js +3 -3
  55. package/scripts/tags/lib/image.js +1 -1
  56. package/scripts/tags/lib/posters.js +1 -1
  57. package/scripts/tags/lib/sites.js +8 -4
  58. package/source/css/_common/base.styl +4 -4
  59. package/source/css/_common/blockquote.styl +2 -2
  60. package/source/css/_common/dropdown.styl +132 -0
  61. package/source/css/_common/html.styl +1 -1
  62. package/source/css/_common/loading.styl +5 -2
  63. package/source/css/_common/title.styl +8 -8
  64. package/source/css/_components/collection.styl +188 -0
  65. package/source/css/_components/layout.styl +1 -1
  66. package/source/css/_components/list.styl +44 -46
  67. package/source/css/_components/md.styl +3 -3
  68. package/source/css/_components/page-transition.styl +22 -0
  69. package/source/css/_components/pages/archives.styl +19 -18
  70. package/source/css/_components/pages/article-indent.styl +2 -2
  71. package/source/css/_components/pages/article-story.styl +9 -5
  72. package/source/css/_components/pages/collection-preview.styl +68 -0
  73. package/source/css/_components/pages/notebook.styl +11 -9
  74. package/source/css/_components/partial/article-footer.styl +7 -4
  75. package/source/css/_components/partial/cover.styl +280 -1
  76. package/source/css/_components/partial/related.styl +10 -6
  77. package/source/css/_components/sidebar/footer.styl +66 -12
  78. package/source/css/_components/sidebar/nav-area.styl +6 -1
  79. package/source/css/_components/sidebar/search.styl +6 -2
  80. package/source/css/_components/tag-plugins/chat.styl +1 -1
  81. package/source/css/_components/tag-plugins/folders.styl +4 -4
  82. package/source/css/_components/tag-plugins/folding.styl +3 -3
  83. package/source/css/_components/tag-plugins/hashtag.styl +2 -3
  84. package/source/css/_components/tag-plugins/mdrender.styl +1 -1
  85. package/source/css/_components/tag-plugins/note.styl +4 -4
  86. package/source/css/_components/tag-plugins/poetry.styl +5 -5
  87. package/source/css/_components/tag-plugins/quot.styl +1 -2
  88. package/source/css/_components/tag-plugins/quote.styl +1 -1
  89. package/source/css/_components/tag-plugins/tabs.styl +2 -2
  90. package/source/css/_components/tag-plugins/timeline.styl +4 -4
  91. package/source/css/_components/widgets/markdown.styl +1 -14
  92. package/source/css/_components/widgets/timeline.styl +2 -2
  93. package/source/css/_components/widgets/toc.styl +22 -20
  94. package/source/css/_components/widgets/widgets.styl +13 -1
  95. package/source/css/_components/wiki-card.styl +209 -0
  96. package/source/css/_custom.styl +22 -20
  97. package/source/css/_plugins/lazyload.styl +8 -5
  98. package/source/css/comments/utterances.styl +2 -2
  99. package/source/css/plugins/mermaid.styl +31 -12
  100. package/source/js/icons.js +1 -1
  101. package/source/js/main.js +62 -6
  102. package/source/js/plugins/adaptive-text.js +46 -2
  103. package/source/js/plugins/dropdown.js +435 -0
  104. package/source/js/plugins/galaxy.js +423 -0
  105. package/source/js/search/algolia-search.js +1 -1
  106. package/source/js/search/local-search.js +1 -1
  107. package/source/js/services/ghinfo.js +31 -3
  108. package/source/js/services/siteinfo.js +26 -0
  109. package/source/js/services/sites.js +6 -2
  110. package/source/js/services.js +5 -2
  111. package/source/js/tagtree.js +7 -7
  112. package/source/js/utils.js +1 -1
  113. package/layout/_partial/widgets/components/link.ejs +0 -21
  114. package/source/css/_components/sidebar/menu.styl +0 -77
  115. package/source/css/_components/widgets/components.styl +0 -68
  116. package/source/css/_components/widgets/list.styl +0 -46
  117. package/source/css/_components/widgets/related.styl +0 -40
@@ -1,7 +1,7 @@
1
1
  // 文字自适应颜色 DOM 插件
2
- // 扫描 [data-text-adaptive] 元素,解析背景来源(--cover-url → --bg-url →
2
+ // 扫描 [data-text-adaptive] 元素,解析背景来源(--adaptive-background → --cover-url → --bg-url →
3
3
  // background-image → background-color),调用 stellar.color 计算文字颜色,
4
- // 写入内联 --text-banner 与 --text-banner-theme。
4
+ // 写入内联 --text-banner 与 --text-banner-theme;Wiki 封面另写入主题色蒙版变量。
5
5
  // 属性值:
6
6
  // theme(默认) 背景图平均色 lighten/darken(两种变量同色)
7
7
  // contrast 黑白对比(两种变量同色)
@@ -19,6 +19,10 @@ function parseCssUrl(value) {
19
19
 
20
20
  function resolveBackground(el) {
21
21
  var style = window.getComputedStyle(el);
22
+ var adaptiveBackground = stellar.color.parse(style.getPropertyValue('--adaptive-background'));
23
+ if (adaptiveBackground) {
24
+ return { type: 'color', rgb: adaptiveBackground };
25
+ }
22
26
  var cover = parseCssUrl(style.getPropertyValue('--cover-url'));
23
27
  if (cover) {
24
28
  return { type: 'image', url: cover };
@@ -85,6 +89,39 @@ function setSplitTextColors(el, rgb) {
85
89
  }
86
90
  }
87
91
 
92
+ // Wiki 卡片的 Today 风格内容层使用封面平均色:保留色相,增强弱饱和度后压低明度,
93
+ // 让满不透明蒙版下的白色文字始终可读。变量写到 .wiki-card:蒙版用深色版本,
94
+ // hover 边框使用同源但提高 20 个明度点的版本。
95
+ function setWikiOverlayColor(el, rgb) {
96
+ var node = el.parentElement;
97
+ while (node && node.nodeType === 1) {
98
+ if ((' ' + node.className + ' ').indexOf(' wiki-card ') !== -1) {
99
+ var themed = stellar.color.enhanceSaturation(rgb, { minSaturation: 0.45, boostBelow: 0.35 });
100
+ var overlay = stellar.color.darken(themed, 0.26);
101
+ var border = stellar.color.lighten(themed, 0.46);
102
+ if (overlay) {
103
+ node.style.setProperty('--wiki-overlay-color', overlay);
104
+ }
105
+ if (border) {
106
+ node.style.setProperty('--wiki-border-color', border);
107
+ }
108
+ return;
109
+ }
110
+ node = node.parentElement;
111
+ }
112
+ }
113
+
114
+ // Wiki 卡片主题色确定后(包括提取失败时的 CSS 回退)通知索引页。
115
+ // 覆盖层会同时等待原图 load,避免先用默认主题色绘制、再切换为平均色。
116
+ function notifyWikiOverlayReady(el) {
117
+ if (!el || !el.dispatchEvent || !document.createEvent) {
118
+ return;
119
+ }
120
+ var event = document.createEvent('Event');
121
+ event.initEvent('wiki-overlay-ready', true, false);
122
+ el.dispatchEvent(event);
123
+ }
124
+
88
125
  function applyToElement(el) {
89
126
  // 用户显式覆盖优先:首次处理时元素已有内联 --text-banner 或内联 color 则跳过
90
127
  // (插件自身写入的变量不视为用户覆盖,主题切换重算时需重新应用)
@@ -103,6 +140,10 @@ function applyToElement(el) {
103
140
  } else {
104
141
  setTextColor(el, styleName, rgb);
105
142
  }
143
+ if (styleName === 'split' && el.parentElement && (' ' + el.parentElement.className + ' ').indexOf(' wiki-card-cover ') !== -1) {
144
+ setWikiOverlayColor(el, rgb);
145
+ notifyWikiOverlayReady(el);
146
+ }
106
147
  appliedElements.add(el);
107
148
  };
108
149
  if (bg.type === 'color') {
@@ -112,6 +153,9 @@ function applyToElement(el) {
112
153
  stellar.color.getAverageColor(bg.url, { background: backdrop }).then(function (rgb) {
113
154
  if (rgb) {
114
155
  apply(rgb);
156
+ } else if (styleName === 'split' && el.parentElement && (' ' + el.parentElement.className + ' ').indexOf(' wiki-card-cover ') !== -1) {
157
+ // 平均色提取失败时不写变量,保留 CSS 回退色;但仍须解除 Wiki 覆盖层等待。
158
+ notifyWikiOverlayReady(el);
115
159
  }
116
160
  });
117
161
  }
@@ -0,0 +1,435 @@
1
+ // 通用 dropdown 浮层:把打开的菜单挂到 body 下,避免被任意祖先容器裁剪。
2
+ (() => {
3
+ const selector = 'details.dropdown'
4
+ const gap = 8
5
+ const viewportPadding = 8
6
+ let layer = null
7
+ let active = null
8
+ let frame = null
9
+ let menuId = 0
10
+
11
+ function getLayer() {
12
+ if (layer && document.documentElement.contains(layer)) {
13
+ return layer
14
+ }
15
+ layer = document.createElement('div')
16
+ layer.className = 'dropdown-layer'
17
+ document.body.appendChild(layer)
18
+ return layer
19
+ }
20
+
21
+ function getDirectChild(parent, childSelector) {
22
+ for (const child of parent.children) {
23
+ if (child.matches(childSelector)) {
24
+ return child
25
+ }
26
+ }
27
+ return null
28
+ }
29
+
30
+ function getViewportSize() {
31
+ return {
32
+ width: document.documentElement.clientWidth || window.innerWidth,
33
+ height: document.documentElement.clientHeight || window.innerHeight
34
+ }
35
+ }
36
+
37
+ function getDirection(dropdown) {
38
+ const direction = dropdown.getAttribute('direction')
39
+ return direction === 'up' || direction === 'down' ? direction : 'auto'
40
+ }
41
+
42
+ function getAlign(dropdown) {
43
+ const align = dropdown.getAttribute('align')
44
+ return align === 'left' || align === 'right' ? align : 'auto'
45
+ }
46
+
47
+ function setMenuId(dropdown, menu) {
48
+ if (!menu.id) {
49
+ menuId += 1
50
+ menu.id = `stellar-dropdown-menu-${menuId}`
51
+ }
52
+ const trigger = getDirectChild(dropdown, '.dropdown-trigger')
53
+ if (trigger) {
54
+ trigger.setAttribute('aria-controls', menu.id)
55
+ }
56
+ }
57
+
58
+ function clearMenuPosition(menu) {
59
+ menu.style.removeProperty('top')
60
+ menu.style.removeProperty('left')
61
+ menu.style.removeProperty('max-height')
62
+ menu.style.removeProperty('visibility')
63
+ menu.style.removeProperty('opacity')
64
+ menu.removeAttribute('data-placement')
65
+ }
66
+
67
+ function hideMenuBeforeOpen(dropdown) {
68
+ const menu = getDirectChild(dropdown, '.dropdown-menu')
69
+ if (!menu || menu.classList.contains('dropdown-menu-portal')) {
70
+ return
71
+ }
72
+ menu.style.visibility = 'hidden'
73
+ menu.style.opacity = '0'
74
+ }
75
+
76
+ function isPointInRect(point, rect) {
77
+ return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom
78
+ }
79
+
80
+ function isPointInPolygon(point, polygon) {
81
+ let inside = false
82
+ for (let index = 0, previous = polygon.length - 1; index < polygon.length; previous = index++) {
83
+ const currentPoint = polygon[index]
84
+ const previousPoint = polygon[previous]
85
+ const intersects = currentPoint.y > point.y !== previousPoint.y > point.y &&
86
+ point.x < (previousPoint.x - currentPoint.x) * (point.y - currentPoint.y) /
87
+ (previousPoint.y - currentPoint.y) + currentPoint.x
88
+ if (intersects) {
89
+ inside = !inside
90
+ }
91
+ }
92
+ return inside
93
+ }
94
+
95
+ function updateBridge(state, triggerRect, menuRect, placement) {
96
+ const polygon = placement === 'up'
97
+ ? [
98
+ { x: triggerRect.left, y: triggerRect.top },
99
+ { x: menuRect.left, y: menuRect.bottom },
100
+ { x: menuRect.right, y: menuRect.bottom },
101
+ { x: triggerRect.right, y: triggerRect.top }
102
+ ]
103
+ : [
104
+ { x: triggerRect.left, y: triggerRect.bottom },
105
+ { x: triggerRect.right, y: triggerRect.bottom },
106
+ { x: menuRect.right, y: menuRect.top },
107
+ { x: menuRect.left, y: menuRect.top }
108
+ ]
109
+ const clipPath = `polygon(${polygon.map(point => `${Math.round(point.x)}px ${Math.round(point.y)}px`).join(', ')})`
110
+ state.bridge.__stellarDropdownPolygon = polygon
111
+ state.bridge.style.clipPath = clipPath
112
+ state.bridge.style.webkitClipPath = clipPath
113
+ }
114
+
115
+ function isPointerInsideState(state, event) {
116
+ const target = event.target
117
+ if (target && (state.dropdown.contains(target) || state.menu.contains(target) || state.bridge.contains(target))) {
118
+ return true
119
+ }
120
+ const point = { x: event.clientX, y: event.clientY }
121
+ if (isPointInRect(point, state.trigger.getBoundingClientRect()) || isPointInRect(point, state.menu.getBoundingClientRect())) {
122
+ return true
123
+ }
124
+ return isPointInPolygon(point, state.bridge.__stellarDropdownPolygon || [])
125
+ }
126
+
127
+ function closeState(state, focusTrigger) {
128
+ if (!state || active !== state) {
129
+ return
130
+ }
131
+ active = null
132
+ if (state.dropdown.open) {
133
+ state.dropdown.open = false
134
+ }
135
+ if (state.bridge.parentNode) {
136
+ state.bridge.remove()
137
+ }
138
+ state.menu.classList.remove('dropdown-menu-portal')
139
+ state.menu.classList.remove('dropdown-menu-visible')
140
+ state.menu.removeAttribute('data-portal')
141
+ clearMenuPosition(state.menu)
142
+ if (state.placeholder.parentNode) {
143
+ state.placeholder.parentNode.insertBefore(state.menu, state.placeholder)
144
+ state.placeholder.remove()
145
+ } else if (state.menu.parentNode) {
146
+ state.menu.remove()
147
+ }
148
+ state.dropdown.removeAttribute('data-dropdown-open')
149
+ if (focusTrigger && document.documentElement.contains(state.trigger)) {
150
+ state.trigger.focus()
151
+ }
152
+ }
153
+
154
+ function chooseVertical(dropdown, menuHeight, topSpace, bottomSpace) {
155
+ const direction = getDirection(dropdown)
156
+ if (direction === 'up') {
157
+ return 'up'
158
+ }
159
+ if (direction === 'down') {
160
+ return 'down'
161
+ }
162
+ if (menuHeight <= bottomSpace) {
163
+ return 'down'
164
+ }
165
+ if (menuHeight <= topSpace) {
166
+ return 'up'
167
+ }
168
+ return topSpace >= bottomSpace ? 'up' : 'down'
169
+ }
170
+
171
+ function chooseHorizontal(dropdown, rect, menuWidth, viewportWidth) {
172
+ const align = getAlign(dropdown)
173
+ const minLeft = viewportPadding
174
+ const maxLeft = Math.max(minLeft, viewportWidth - viewportPadding - menuWidth)
175
+ if (align === 'left') {
176
+ return Math.min(Math.max(rect.left, minLeft), maxLeft)
177
+ }
178
+ if (align === 'right') {
179
+ return Math.min(Math.max(rect.right - menuWidth, minLeft), maxLeft)
180
+ }
181
+ const leftAlignedLeft = rect.left
182
+ const rightAlignedLeft = rect.right - menuWidth
183
+ const leftFits = leftAlignedLeft >= minLeft && leftAlignedLeft <= maxLeft
184
+ const rightFits = rightAlignedLeft >= minLeft && rightAlignedLeft <= maxLeft
185
+ if (leftFits && rightFits) {
186
+ const rightSpace = viewportWidth - viewportPadding - rect.left
187
+ const leftSpace = rect.right - viewportPadding
188
+ return rightSpace >= leftSpace ? leftAlignedLeft : rightAlignedLeft
189
+ }
190
+ if (leftFits) {
191
+ return leftAlignedLeft
192
+ }
193
+ if (rightFits) {
194
+ return rightAlignedLeft
195
+ }
196
+ const preferLeft = viewportWidth - viewportPadding - rect.left >= rect.right - viewportPadding
197
+ const preferredLeft = preferLeft ? leftAlignedLeft : rightAlignedLeft
198
+ return Math.min(Math.max(preferredLeft, minLeft), maxLeft)
199
+ }
200
+
201
+ function positionActive() {
202
+ if (!active) {
203
+ return
204
+ }
205
+ const state = active
206
+ if (!document.documentElement.contains(state.trigger)) {
207
+ closeState(state, false)
208
+ return
209
+ }
210
+ const rect = state.trigger.getBoundingClientRect()
211
+ const viewport = getViewportSize()
212
+ if (rect.bottom <= 0 || rect.top >= viewport.height) {
213
+ closeState(state, false)
214
+ return
215
+ }
216
+
217
+ const topSpace = Math.max(0, rect.top - gap - viewportPadding)
218
+ const bottomSpace = Math.max(0, viewport.height - rect.bottom - gap - viewportPadding)
219
+ const menu = state.menu
220
+ menu.style.visibility = 'hidden'
221
+ menu.style.maxHeight = 'none'
222
+ menu.style.top = '0px'
223
+ menu.style.left = '0px'
224
+ const naturalRect = menu.getBoundingClientRect()
225
+ const vertical = chooseVertical(state.dropdown, naturalRect.height, topSpace, bottomSpace)
226
+ const availableHeight = vertical === 'up' ? topSpace : bottomSpace
227
+ menu.style.maxHeight = `${Math.max(1, Math.floor(availableHeight))}px`
228
+
229
+ const menuRect = menu.getBoundingClientRect()
230
+ const topLimit = viewportPadding
231
+ const bottomLimit = Math.max(topLimit, viewport.height - viewportPadding - menuRect.height)
232
+ const preferredTop = vertical === 'up'
233
+ ? rect.top - gap - menuRect.height
234
+ : rect.bottom + gap
235
+ const top = Math.min(Math.max(preferredTop, topLimit), bottomLimit)
236
+ const left = chooseHorizontal(state.dropdown, rect, menuRect.width, viewport.width)
237
+ menu.style.top = `${Math.round(top)}px`
238
+ menu.style.left = `${Math.round(left)}px`
239
+ menu.setAttribute('data-placement', vertical)
240
+ menu.style.removeProperty('visibility')
241
+ menu.style.removeProperty('opacity')
242
+ updateBridge(state, rect, menu.getBoundingClientRect(), vertical)
243
+ }
244
+
245
+ function schedulePosition() {
246
+ if (!active || frame !== null) {
247
+ return
248
+ }
249
+ frame = window.requestAnimationFrame(() => {
250
+ frame = null
251
+ positionActive()
252
+ })
253
+ }
254
+
255
+ function openDropdown(dropdown) {
256
+ const menu = getDirectChild(dropdown, '.dropdown-menu')
257
+ const trigger = getDirectChild(dropdown, '.dropdown-trigger')
258
+ if (!menu || !trigger) {
259
+ return
260
+ }
261
+ if (active && active.dropdown !== dropdown) {
262
+ closeState(active, false)
263
+ }
264
+ if (active && active.dropdown === dropdown) {
265
+ schedulePosition()
266
+ return
267
+ }
268
+
269
+ const placeholder = document.createComment('dropdown-menu')
270
+ const bridge = document.createElement('div')
271
+ bridge.className = 'dropdown-bridge'
272
+ if (!window.CSS || typeof window.CSS.supports !== 'function' ||
273
+ (!window.CSS.supports('clip-path', 'polygon(0 0, 1px 1px, 2px 2px)') &&
274
+ !window.CSS.supports('-webkit-clip-path', 'polygon(0 0, 1px 1px, 2px 2px)'))) {
275
+ bridge.style.pointerEvents = 'none'
276
+ }
277
+ menu.style.visibility = 'hidden'
278
+ menu.style.opacity = '0'
279
+ menu.parentNode.insertBefore(placeholder, menu)
280
+ const dropdownLayer = getLayer()
281
+ dropdownLayer.appendChild(bridge)
282
+ dropdownLayer.appendChild(menu)
283
+ setMenuId(dropdown, menu)
284
+ menu.classList.add('dropdown-menu-portal')
285
+ menu.setAttribute('data-portal', 'true')
286
+ dropdown.setAttribute('data-dropdown-open', 'true')
287
+ active = {
288
+ dropdown,
289
+ menu,
290
+ trigger,
291
+ placeholder,
292
+ bridge,
293
+ hoverMode: dropdown.__stellarDropdownHovering
294
+ }
295
+ positionActive()
296
+ if (!dropdown.open) {
297
+ dropdown.open = true
298
+ }
299
+ window.requestAnimationFrame(() => {
300
+ if (active && active.dropdown === dropdown) {
301
+ menu.classList.add('dropdown-menu-visible')
302
+ }
303
+ })
304
+ }
305
+
306
+ function bind(dropdown) {
307
+ if (dropdown.__stellarDropdownBound) {
308
+ return
309
+ }
310
+ dropdown.__stellarDropdownBound = true
311
+ dropdown.addEventListener('mouseenter', () => {
312
+ dropdown.__stellarDropdownHovering = true
313
+ if (active && active.dropdown === dropdown) {
314
+ active.hoverMode = true
315
+ }
316
+ if (!active || active.dropdown !== dropdown) {
317
+ openDropdown(dropdown)
318
+ }
319
+ })
320
+ const trigger = getDirectChild(dropdown, '.dropdown-trigger')
321
+ if (trigger) {
322
+ trigger.addEventListener('click', event => {
323
+ event.preventDefault()
324
+ if (!active || active.dropdown !== dropdown) {
325
+ openDropdown(dropdown)
326
+ }
327
+ })
328
+ trigger.addEventListener('keydown', event => {
329
+ if (event.key === 'Enter' || event.key === ' ') {
330
+ event.preventDefault()
331
+ if (active && active.dropdown === dropdown) {
332
+ closeState(active, false)
333
+ } else {
334
+ openDropdown(dropdown)
335
+ }
336
+ }
337
+ })
338
+ }
339
+ dropdown.addEventListener('mouseleave', () => {
340
+ dropdown.__stellarDropdownHovering = false
341
+ })
342
+ dropdown.addEventListener('toggle', () => {
343
+ if (dropdown.open) {
344
+ if (!active || active.dropdown !== dropdown) {
345
+ hideMenuBeforeOpen(dropdown)
346
+ openDropdown(dropdown)
347
+ } else {
348
+ schedulePosition()
349
+ }
350
+ } else if (active && active.dropdown === dropdown) {
351
+ closeState(active, false)
352
+ }
353
+ })
354
+ if (dropdown.open) {
355
+ openDropdown(dropdown)
356
+ }
357
+ dropdown.__stellarDropdownHovering = false
358
+ }
359
+
360
+ function bindTree(root) {
361
+ if (root.nodeType !== 1) {
362
+ return
363
+ }
364
+ if (root.matches(selector)) {
365
+ bind(root)
366
+ }
367
+ root.querySelectorAll(selector).forEach(bind)
368
+ }
369
+
370
+ function init() {
371
+ if (!document.body || !window.MutationObserver) {
372
+ return
373
+ }
374
+ bindTree(document.body)
375
+ const observer = new MutationObserver(records => {
376
+ records.forEach(record => {
377
+ record.addedNodes.forEach(bindTree)
378
+ record.removedNodes.forEach(node => {
379
+ if (active && node.nodeType === 1 && (node === active.dropdown || node.contains(active.dropdown))) {
380
+ closeState(active, false)
381
+ }
382
+ })
383
+ })
384
+ })
385
+ observer.observe(document.body, { childList: true, subtree: true })
386
+
387
+ document.addEventListener('click', event => {
388
+ if (!active) {
389
+ return
390
+ }
391
+ if (active.dropdown.contains(event.target) || active.menu.contains(event.target)) {
392
+ return
393
+ }
394
+ closeState(active, false)
395
+ }, true)
396
+
397
+ document.addEventListener('mousemove', event => {
398
+ if (!active || !active.hoverMode || isPointerInsideState(active, event)) {
399
+ return
400
+ }
401
+ closeState(active, false)
402
+ }, true)
403
+
404
+ document.addEventListener('keydown', event => {
405
+ if (!active) {
406
+ return
407
+ }
408
+ if (event.key === 'Escape') {
409
+ event.preventDefault()
410
+ closeState(active, true)
411
+ return
412
+ }
413
+ if (event.key === 'Tab' && document.activeElement === active.trigger && !event.shiftKey) {
414
+ const firstItem = active.menu.querySelector('a, button, [tabindex]:not([tabindex="-1"])')
415
+ if (firstItem) {
416
+ event.preventDefault()
417
+ firstItem.focus()
418
+ }
419
+ }
420
+ })
421
+
422
+ window.addEventListener('resize', schedulePosition)
423
+ window.addEventListener('scroll', schedulePosition, true)
424
+ if (window.visualViewport) {
425
+ window.visualViewport.addEventListener('resize', schedulePosition)
426
+ window.visualViewport.addEventListener('scroll', schedulePosition)
427
+ }
428
+ }
429
+
430
+ if (document.readyState === 'loading') {
431
+ document.addEventListener('DOMContentLoaded', init, { once: true })
432
+ } else {
433
+ init()
434
+ }
435
+ })()