Написание движка для интерактивного обучения
This commit is contained in:
125
resources/js/Components/Tour/composables/useFloating.js
Normal file
125
resources/js/Components/Tour/composables/useFloating.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import { ref, watch, onBeforeUnmount, unref, nextTick } from "vue";
|
||||
|
||||
import {
|
||||
computePosition,
|
||||
autoUpdate,
|
||||
offset,
|
||||
flip,
|
||||
shift
|
||||
} from "@floating-ui/dom";
|
||||
|
||||
export function useFloating(target, floating, options = {}) {
|
||||
const x = ref(0);
|
||||
const y = ref(0);
|
||||
|
||||
const strategy = ref("fixed");
|
||||
|
||||
const placement = ref(
|
||||
options.placement ?? "bottom"
|
||||
);
|
||||
|
||||
let cleanup = null;
|
||||
|
||||
function resolveTarget() {
|
||||
const value = unref(target);
|
||||
|
||||
if (!value)
|
||||
return null;
|
||||
|
||||
if (typeof value === "function")
|
||||
return value();
|
||||
|
||||
if (typeof value === "string")
|
||||
return document.querySelector(value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function resolveFloating() {
|
||||
return unref(floating);
|
||||
}
|
||||
|
||||
async function update() {
|
||||
const reference = resolveTarget();
|
||||
const popup = resolveFloating();
|
||||
|
||||
if (!reference || !popup)
|
||||
return;
|
||||
|
||||
const result = await computePosition(
|
||||
reference,
|
||||
popup,
|
||||
{
|
||||
strategy: strategy.value,
|
||||
placement: placement.value,
|
||||
middleware: [
|
||||
offset(
|
||||
options.offset ?? 12
|
||||
),
|
||||
flip(),
|
||||
shift({
|
||||
padding: 8
|
||||
})
|
||||
]
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
x.value = result.x;
|
||||
y.value = result.y;
|
||||
}
|
||||
|
||||
function start() {
|
||||
cleanup?.();
|
||||
const reference = resolveTarget();
|
||||
const popup = resolveFloating();
|
||||
|
||||
if (!reference || !popup)
|
||||
return;
|
||||
|
||||
cleanup = autoUpdate(
|
||||
reference,
|
||||
popup,
|
||||
update
|
||||
);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
watch(
|
||||
[
|
||||
() => resolveTarget(),
|
||||
() => resolveFloating()
|
||||
],
|
||||
() => {
|
||||
start()
|
||||
},
|
||||
{
|
||||
immediate:true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
floating,
|
||||
async value => {
|
||||
|
||||
if (!value)
|
||||
return
|
||||
|
||||
await nextTick()
|
||||
update()
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup?.();
|
||||
});
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
strategy,
|
||||
update
|
||||
};
|
||||
|
||||
}
|
||||
138
resources/js/Components/Tour/composables/useHighlight.js
Normal file
138
resources/js/Components/Tour/composables/useHighlight.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
watch,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
unref
|
||||
} from "vue";
|
||||
|
||||
|
||||
export function useHighlight(target, options = {}) {
|
||||
const rect = ref(null);
|
||||
const padding = options.padding ?? 8;
|
||||
|
||||
let resizeObserver = null;
|
||||
|
||||
function resolveTarget() {
|
||||
const value = unref(target);
|
||||
|
||||
if (!value)
|
||||
return null;
|
||||
|
||||
if (typeof value === "function")
|
||||
return value();
|
||||
|
||||
if (typeof value === "string")
|
||||
return document.querySelector(value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
function update() {
|
||||
const element = resolveTarget();
|
||||
|
||||
if (!element) {
|
||||
rect.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const box = element.getBoundingClientRect();
|
||||
|
||||
rect.value = {
|
||||
top: box.top - padding,
|
||||
left: box.left - padding,
|
||||
width: box.width + padding * 2,
|
||||
height: box.height + padding * 2,
|
||||
bottom:
|
||||
box.bottom + padding,
|
||||
right:
|
||||
box.right + padding
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function observe() {
|
||||
const element = resolveTarget();
|
||||
|
||||
if (!element)
|
||||
return;
|
||||
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
update();
|
||||
});
|
||||
|
||||
resizeObserver.observe(element);
|
||||
}
|
||||
|
||||
|
||||
const style = computed(() => {
|
||||
|
||||
if (!rect.value)
|
||||
return {};
|
||||
|
||||
return {
|
||||
top:
|
||||
`${rect.value.top}px`,
|
||||
left:
|
||||
`${rect.value.left}px`,
|
||||
width:
|
||||
`${rect.value.width}px`,
|
||||
height:
|
||||
`${rect.value.height}px`
|
||||
};
|
||||
});
|
||||
|
||||
watch(
|
||||
() => unref(target),
|
||||
() => {
|
||||
update();
|
||||
observe();
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
update
|
||||
);
|
||||
|
||||
window.addEventListener(
|
||||
"scroll",
|
||||
update,
|
||||
true
|
||||
);
|
||||
|
||||
update();
|
||||
observe();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect();
|
||||
|
||||
window.removeEventListener(
|
||||
"resize",
|
||||
update
|
||||
);
|
||||
|
||||
window.removeEventListener(
|
||||
"scroll",
|
||||
update,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
return {
|
||||
rect,
|
||||
style,
|
||||
update
|
||||
};
|
||||
|
||||
}
|
||||
61
resources/js/Components/Tour/composables/useWaitTarget.js
Normal file
61
resources/js/Components/Tour/composables/useWaitTarget.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { nextTick } from "vue"
|
||||
|
||||
export function useWaitTarget() {
|
||||
async function waitTarget(target, options = {}) {
|
||||
const {
|
||||
timeout = 5000,
|
||||
interval = 50,
|
||||
visible = false
|
||||
} = options
|
||||
|
||||
const start = Date.now()
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
await nextTick()
|
||||
let element = null
|
||||
|
||||
if (typeof target === "function") {
|
||||
element = target()
|
||||
}
|
||||
else {
|
||||
element = document.querySelector(target)
|
||||
}
|
||||
|
||||
if (element) {
|
||||
|
||||
if (visible) {
|
||||
const rect = element.getBoundingClientRect()
|
||||
|
||||
if (
|
||||
rect.width === 0 ||
|
||||
rect.height === 0
|
||||
) {
|
||||
await sleep(interval)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
await sleep(interval)
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`[Tour] Target not found:`,
|
||||
target
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
waitTarget
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user