|
@@ -1,6 +1,28 @@
|
|
|
-import { unref } from "vue";
|
|
|
|
|
|
|
+import { nextTick, unref } from "vue";
|
|
|
|
|
|
|
|
export const useScroll = () => {
|
|
export const useScroll = () => {
|
|
|
|
|
+ const getElement = (value) => {
|
|
|
|
|
+ const instance = unref(value);
|
|
|
|
|
+ return instance?.$el ?? instance ?? null;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const getScrollableParent = (targetElement) => {
|
|
|
|
|
+ let parent = targetElement?.parentElement;
|
|
|
|
|
+
|
|
|
|
|
+ while (parent) {
|
|
|
|
|
+ const { overflowY } = window.getComputedStyle(parent);
|
|
|
|
|
+ if (
|
|
|
|
|
+ ["auto", "scroll"].includes(overflowY) &&
|
|
|
|
|
+ parent.scrollHeight > parent.clientHeight
|
|
|
|
|
+ ) {
|
|
|
|
|
+ return parent;
|
|
|
|
|
+ }
|
|
|
|
|
+ parent = parent.parentElement;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return document.scrollingElement || document.documentElement;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Scrolls to a target element within a scroll container.
|
|
* Scrolls to a target element within a scroll container.
|
|
|
* The function is agnostic to whether the container is a QScrollArea or a native DOM element.
|
|
* The function is agnostic to whether the container is a QScrollArea or a native DOM element.
|
|
@@ -17,10 +39,9 @@ export const useScroll = () => {
|
|
|
|
|
|
|
|
const targetInstance = unref(targetRef);
|
|
const targetInstance = unref(targetRef);
|
|
|
const containerInstance = unref(containerRef);
|
|
const containerInstance = unref(containerRef);
|
|
|
-
|
|
|
|
|
- const targetElement = targetInstance?.$el ?? targetInstance;
|
|
|
|
|
-
|
|
|
|
|
- const containerElement = containerInstance?.$el ?? containerInstance;
|
|
|
|
|
|
|
+ const targetElement = getElement(targetInstance);
|
|
|
|
|
+ const containerElement =
|
|
|
|
|
+ getElement(containerInstance) || getScrollableParent(targetElement);
|
|
|
|
|
|
|
|
if (!targetElement || !containerElement) {
|
|
if (!targetElement || !containerElement) {
|
|
|
console.warn("useScroll: Target or container element not found");
|
|
console.warn("useScroll: Target or container element not found");
|
|
@@ -28,30 +49,12 @@ export const useScroll = () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- let currentElement = targetElement;
|
|
|
|
|
- let offsetTop = 0;
|
|
|
|
|
-
|
|
|
|
|
- while (currentElement && currentElement !== containerElement) {
|
|
|
|
|
- offsetTop += currentElement.offsetTop;
|
|
|
|
|
- if (
|
|
|
|
|
- containerElement.contains(currentElement.offsetParent) ||
|
|
|
|
|
- containerElement === document.body
|
|
|
|
|
- ) {
|
|
|
|
|
- currentElement = currentElement.offsetParent;
|
|
|
|
|
- } else {
|
|
|
|
|
- currentElement = null;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (
|
|
|
|
|
- containerElement !== document.body &&
|
|
|
|
|
- !containerElement.contains(targetElement)
|
|
|
|
|
- ) {
|
|
|
|
|
- console.warn("useScroll: Target is not a child of the container");
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const targetPosition = Math.max(0, offsetTop - offset);
|
|
|
|
|
|
|
+ const targetRect = targetElement.getBoundingClientRect();
|
|
|
|
|
+ const containerRect = containerElement.getBoundingClientRect();
|
|
|
|
|
+ const targetPosition = Math.max(
|
|
|
|
|
+ 0,
|
|
|
|
|
+ targetRect.top - containerRect.top + containerElement.scrollTop - offset,
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
if (typeof containerInstance.setScrollPosition === "function") {
|
|
if (typeof containerInstance.setScrollPosition === "function") {
|
|
|
containerInstance.setScrollPosition(
|
|
containerInstance.setScrollPosition(
|
|
@@ -60,9 +63,7 @@ export const useScroll = () => {
|
|
|
duration,
|
|
duration,
|
|
|
);
|
|
);
|
|
|
} else {
|
|
} else {
|
|
|
- const scrollable =
|
|
|
|
|
- containerElement === document.body ? window : containerElement;
|
|
|
|
|
- scrollable.scrollTo({
|
|
|
|
|
|
|
+ containerElement.scrollTo({
|
|
|
top: targetPosition,
|
|
top: targetPosition,
|
|
|
behavior: "smooth",
|
|
behavior: "smooth",
|
|
|
});
|
|
});
|
|
@@ -75,7 +76,44 @@ export const useScroll = () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const scrollToFirstError = async (formRef, containerRef, options = {}) => {
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ const forms = unref(formRef);
|
|
|
|
|
+ const formList = Array.isArray(forms) ? forms : [forms];
|
|
|
|
|
+
|
|
|
|
|
+ for (const form of formList) {
|
|
|
|
|
+ const components = form?.getValidationComponents?.() ?? [];
|
|
|
|
|
+ const firstError = components.find((component) => component.hasError);
|
|
|
|
|
+
|
|
|
|
|
+ if (firstError) {
|
|
|
|
|
+ firstError.focus?.();
|
|
|
|
|
+ const scrollFn = options.scrollFn || scrollToComponent;
|
|
|
|
|
+ scrollFn(firstError, unref(containerRef), options);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return false;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const validateAndScroll = async (formRef, containerRef, options = {}) => {
|
|
|
|
|
+ const forms = unref(formRef);
|
|
|
|
|
+ const formList = (Array.isArray(forms) ? forms : [forms]).filter(Boolean);
|
|
|
|
|
+ const results = await Promise.all(
|
|
|
|
|
+ formList.map((form) => form.validate?.(true) ?? true),
|
|
|
|
|
+ );
|
|
|
|
|
+ const valid = results.every(Boolean);
|
|
|
|
|
+
|
|
|
|
|
+ if (!valid) {
|
|
|
|
|
+ await scrollToFirstError(formRef, containerRef, options);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return valid;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
scrollToComponent,
|
|
scrollToComponent,
|
|
|
|
|
+ scrollToFirstError,
|
|
|
|
|
+ validateAndScroll,
|
|
|
};
|
|
};
|
|
|
};
|
|
};
|