|
|
@@ -1,87 +1,169 @@
|
|
|
-import { unref } from "vue";
|
|
|
-
|
|
|
-export const useScroll = () => {
|
|
|
- /**
|
|
|
- * 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.
|
|
|
- *
|
|
|
- * @param {Ref<Component|HTMLElement>} targetRef - Ref to the target component or DOM element.
|
|
|
- * @param {Ref<Component|HTMLElement>} containerRef - Ref to the container (QScrollArea or a scrollable DOM element like body).
|
|
|
- * @param {Object} options - Scroll options.
|
|
|
- * @param {number} options.offset - Offset from the top in pixels (default: 50).
|
|
|
- * @param {number} options.duration - Animation duration for QScrollArea (default: 150). Note: duration is not supported for native scrolling.
|
|
|
- * @returns {boolean} - Whether the scroll was successful.
|
|
|
- */
|
|
|
- const scrollToComponent = (targetRef, containerRef, options = {}) => {
|
|
|
- const { offset = 150, duration = 150 } = options;
|
|
|
-
|
|
|
- const targetInstance = unref(targetRef);
|
|
|
- const providedContainer = unref(containerRef);
|
|
|
-
|
|
|
- const targetElement = targetInstance?.$el ?? targetInstance;
|
|
|
- const detectedContainer = targetElement?.closest?.(
|
|
|
- ".q-scrollarea__container, .scroll, .scroll-y",
|
|
|
- );
|
|
|
- const containerInstance = providedContainer ?? detectedContainer ?? document.body;
|
|
|
- const containerElement = containerInstance?.$el ?? containerInstance;
|
|
|
-
|
|
|
- if (!targetElement || !containerElement) {
|
|
|
- console.warn("useScroll: Target or container element not found");
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- 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;
|
|
|
- }
|
|
|
+import { nextTick, unref } from "vue";
|
|
|
+
|
|
|
+ export const useScroll = () => {
|
|
|
+ const resolveElement = (value) => {
|
|
|
+ const instance = unref(value);
|
|
|
+
|
|
|
+ return instance?.$el ?? instance ?? null;
|
|
|
+ };
|
|
|
+
|
|
|
+ const resolveContainer = (targetElement, containerRef) => {
|
|
|
+ const containerInstance = unref(containerRef);
|
|
|
+
|
|
|
+ if (typeof containerInstance?.setScrollPosition === "function") {
|
|
|
+ return {
|
|
|
+ instance: containerInstance,
|
|
|
+ element:
|
|
|
+ containerInstance.getScrollTarget?.() ??
|
|
|
+ containerInstance.$el?.querySelector?.(
|
|
|
+ ".q-scrollarea__container",
|
|
|
+ ) ??
|
|
|
+ containerInstance.$el,
|
|
|
+ isQuasar: true,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const providedElement = resolveElement(containerRef);
|
|
|
+
|
|
|
+ const detectedElement = targetElement?.closest?.(
|
|
|
+ ".q-scrollarea__container, .scroll, .scroll-y",
|
|
|
+ );
|
|
|
+
|
|
|
+ const element =
|
|
|
+ providedElement ??
|
|
|
+ detectedElement ??
|
|
|
+ document.scrollingElement ??
|
|
|
+ document.documentElement;
|
|
|
+
|
|
|
+ return {
|
|
|
+ instance: element,
|
|
|
+ element,
|
|
|
+ isQuasar: false,
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ const calculatePosition = (targetElement, containerElement, offset) => {
|
|
|
+ const targetRect = targetElement.getBoundingClientRect();
|
|
|
+
|
|
|
+ const isDocumentContainer =
|
|
|
+ containerElement === document.body ||
|
|
|
+ containerElement === document.documentElement ||
|
|
|
+ containerElement === document.scrollingElement;
|
|
|
+
|
|
|
+ if (isDocumentContainer) {
|
|
|
+ return Math.max(
|
|
|
+ 0,
|
|
|
+ targetRect.top + window.scrollY - offset,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const containerRect = containerElement.getBoundingClientRect();
|
|
|
+
|
|
|
+ return Math.max(
|
|
|
+ 0,
|
|
|
+ targetRect.top -
|
|
|
+ containerRect.top +
|
|
|
+ containerElement.scrollTop -
|
|
|
+ offset,
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const scrollToComponent = (targetRef, containerRef, options = {}) => {
|
|
|
+ const { offset = 150, duration = 150, behavior = "smooth" } = options;
|
|
|
+
|
|
|
+ const targetElement = resolveElement(targetRef);
|
|
|
+
|
|
|
+ if (!targetElement) {
|
|
|
+ console.warn("useScroll: Target element not found");
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
+ const container = resolveContainer(targetElement, containerRef);
|
|
|
+
|
|
|
+ const containerElement = container.element;
|
|
|
+
|
|
|
+ if (!containerElement) {
|
|
|
+ console.warn("useScroll: Container element not found");
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const isDocumentContainer =
|
|
|
+ containerElement === document.body ||
|
|
|
+ containerElement === document.documentElement ||
|
|
|
+ containerElement === document.scrollingElement;
|
|
|
+
|
|
|
if (
|
|
|
- containerElement !== document.body &&
|
|
|
+ !isDocumentContainer &&
|
|
|
!containerElement.contains(targetElement)
|
|
|
) {
|
|
|
- console.warn("useScroll: Target is not a child of the container");
|
|
|
+ console.warn("useScroll: Target is not inside the container");
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- const targetPosition = Math.max(0, offsetTop - offset);
|
|
|
-
|
|
|
- if (typeof containerInstance.setScrollPosition === "function") {
|
|
|
- containerInstance.setScrollPosition(
|
|
|
- "vertical",
|
|
|
- targetPosition,
|
|
|
- duration,
|
|
|
+ try {
|
|
|
+ const targetPosition = calculatePosition(
|
|
|
+ targetElement,
|
|
|
+ containerElement,
|
|
|
+ offset,
|
|
|
);
|
|
|
- } else {
|
|
|
- const scrollable =
|
|
|
- containerElement === document.body ||
|
|
|
- containerElement === document.documentElement
|
|
|
- ? window
|
|
|
- : containerElement;
|
|
|
- scrollable.scrollTo({
|
|
|
- top: targetPosition,
|
|
|
- behavior: "smooth",
|
|
|
- });
|
|
|
+
|
|
|
+ if (container.isQuasar) {
|
|
|
+ container.instance.setScrollPosition(
|
|
|
+ "vertical",
|
|
|
+ targetPosition,
|
|
|
+ duration,
|
|
|
+ );
|
|
|
+ } else if (isDocumentContainer) {
|
|
|
+ window.scrollTo({
|
|
|
+ top: targetPosition,
|
|
|
+ behavior,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ containerElement.scrollTo({
|
|
|
+ top: targetPosition,
|
|
|
+ behavior,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error("useScroll: Error while scrolling", error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const scrollToFirstError = async (
|
|
|
+ formRef,
|
|
|
+ containerRef,
|
|
|
+ options = {},
|
|
|
+ ) => {
|
|
|
+ await nextTick();
|
|
|
+
|
|
|
+ const forms = unref(formRef);
|
|
|
+
|
|
|
+ const formInstances = Array.isArray(forms) ? forms : [forms];
|
|
|
+
|
|
|
+ for (const formInstance of formInstances) {
|
|
|
+ const formElement = resolveElement(formInstance);
|
|
|
+
|
|
|
+ const errorElement =
|
|
|
+ formElement?.matches?.(".q-field--error")
|
|
|
+ ? formElement
|
|
|
+ : formElement?.querySelector?.(".q-field--error");
|
|
|
+
|
|
|
+ if (errorElement) {
|
|
|
+ const scrollFn = options.scrollFn ?? scrollToComponent;
|
|
|
+
|
|
|
+ return scrollFn(errorElement, containerRef, options);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- return true;
|
|
|
- } catch (error) {
|
|
|
- console.error("useScroll: Error while scrolling", error);
|
|
|
return false;
|
|
|
- }
|
|
|
- };
|
|
|
+ };
|
|
|
|
|
|
- return {
|
|
|
- scrollToComponent,
|
|
|
+ return {
|
|
|
+ scrollToComponent,
|
|
|
+ scrollToFirstError,
|
|
|
+ };
|
|
|
};
|
|
|
-};
|