diff --git a/web/components/ComboBox.vue b/web/components/ComboBox.vue
new file mode 100644
index 0000000..520addc
--- /dev/null
+++ b/web/components/ComboBox.vue
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
diff --git a/web/components/SelectBox.vue b/web/components/SelectBox.vue
index 1ec0fa7..0f8146c 100644
--- a/web/components/SelectBox.vue
+++ b/web/components/SelectBox.vue
@@ -9,7 +9,7 @@
* 背景色用 --select-bg 变量留了口子:对话框里的控件要比对话框底色沉一档
* (--c-bg),页面上的控件要比页面底色浮一档(--c-surface),一个值满足不了。
*/
-import { computed, ref } from 'vue'
+import { computed, onBeforeUnmount, ref, watch } from 'vue'
import IconCheck from '~icons/ri/check-line'
import IconChevron from '~icons/ri/arrow-down-s-line'
@@ -31,15 +31,37 @@ const props = defineProps<{
const chosen = defineModel({ default: '' })
const root = ref(null)
+const menuEl = ref(null)
const open = ref(false)
/** 往上弹。下方视口不够放弹层、上方够时翻转,不把弹层怼出屏幕 */
const openUp = ref(false)
const active = ref(0)
+const menuStyle = ref>({})
const current = computed(() => props.options.find((o) => o.value === chosen.value) ?? null)
const MENU_ROOM = 220
+/**
+ * 弹层是 position: fixed,按视口摆放。absolute 的弹层会计入祖先滚动容器的
+ * 可滚动区域——在对话框里打开就把对话框撑出滚动条。fixed 逃出所有 overflow
+ * 裁剪,又因为还在 dialog 的 DOM 子树里,照样画在 top layer 之上。
+ * 代价是位置不跟随滚动,所以下面在滚动和缩放时直接收起。
+ */
+function place() {
+ const rect = root.value?.getBoundingClientRect()
+ if (!rect) return
+ const up = window.innerHeight - rect.bottom < MENU_ROOM && rect.top > MENU_ROOM
+ openUp.value = up
+ menuStyle.value = {
+ left: `${String(rect.left)}px`,
+ minWidth: `${String(rect.width)}px`,
+ ...(up
+ ? { bottom: `${String(window.innerHeight - rect.top + 4)}px` }
+ : { top: `${String(rect.bottom + 4)}px` }),
+ }
+}
+
function toggleOpen() {
if (open.value) {
open.value = false
@@ -49,11 +71,31 @@ function toggleOpen() {
0,
props.options.findIndex((o) => o.value === chosen.value),
)
- const rect = root.value?.getBoundingClientRect()
- openUp.value = !!rect && window.innerHeight - rect.bottom < MENU_ROOM && rect.top > MENU_ROOM
+ place()
open.value = true
}
+/** 页面或对话框一滚动,锚点就不准了,直接收起。弹层内部的滚动不算 */
+function closeOnMove(event?: Event) {
+ if (event?.target instanceof Node && menuEl.value?.contains(event.target)) return
+ open.value = false
+}
+
+watch(open, (isOpen) => {
+ if (isOpen) {
+ window.addEventListener('scroll', closeOnMove, true)
+ window.addEventListener('resize', closeOnMove)
+ } else {
+ window.removeEventListener('scroll', closeOnMove, true)
+ window.removeEventListener('resize', closeOnMove)
+ }
+})
+
+onBeforeUnmount(() => {
+ window.removeEventListener('scroll', closeOnMove, true)
+ window.removeEventListener('resize', closeOnMove)
+})
+
function pick(value: string) {
chosen.value = value
open.value = false
@@ -119,7 +161,7 @@ function groupAt(index: number): string | null {
-
+
- 没有可选项
- {{ groupAt(i) }}
@@ -143,9 +185,10 @@ function groupAt(index: number): string | null {
diff --git a/web/views/RuleForm.vue b/web/views/RuleForm.vue
index 97c9beb..604e3cf 100644
--- a/web/views/RuleForm.vue
+++ b/web/views/RuleForm.vue
@@ -9,6 +9,7 @@
*/
import { computed, ref, watch } from 'vue'
import { buildRule, isStructuredEditable, parseRule } from '@shared/rules.ts'
+import ComboBox from '../components/ComboBox.vue'
import IconClose from '~icons/ri/close-line'
const props = defineProps<{
@@ -143,45 +144,36 @@ function submit() {