From d1139b5cc75fa5ae6d965f7019a4ae7cf7a6ac13 Mon Sep 17 00:00:00 2001 From: huajiqaq Date: Fri, 2 Jan 2026 17:40:48 +0800 Subject: [PATCH 1/4] fix(core): prevent duplicate pull-to-refresh initialization, ensure MD theme triggers completion event --- .../pull-to-refresh/pull-to-refresh-class.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/components/pull-to-refresh/pull-to-refresh-class.js b/src/core/components/pull-to-refresh/pull-to-refresh-class.js index 1b37deb98..104f2f338 100644 --- a/src/core/components/pull-to-refresh/pull-to-refresh-class.js +++ b/src/core/components/pull-to-refresh/pull-to-refresh-class.js @@ -5,10 +5,12 @@ import { getDevice } from '../../shared/get-device.js'; class PullToRefresh extends Framework7Class { constructor(app, el) { + const $el = $(el); + if ($el[0].f7PullToRefresh) return $el[0].f7PullToRefresh; + super({}, [app]); const ptr = this; const device = getDevice(); - const $el = $(el); const $preloaderEl = $el.find('.ptr-preloader'); ptr.$el = $el; @@ -27,7 +29,8 @@ class PullToRefresh extends Framework7Class { ptr.done = function done() { const $transitionTarget = isMaterial ? $preloaderEl : $el; const onTranstionEnd = (e) => { - if ($(e.target).closest($preloaderEl).length) return; + // Material Design platform currently does not support bottom preloader animation. + if (!isMaterial && $(e.target).closest($preloaderEl).length) return; $el.removeClass('ptr-transitioning ptr-pull-up ptr-pull-down ptr-closing'); $el.trigger('ptr:done'); ptr.emit('local::done ptrDone', $el[0]); @@ -204,7 +207,7 @@ class PullToRefresh extends Framework7Class { ((!ptr.bottom && ptrScrollableEl.scrollTop > 0) || (ptr.bottom && ptrScrollableEl.scrollTop < - ptrScrollableEl.scrollHeight - ptrScrollableEl.offsetHeight)) + ptrScrollableEl.scrollHeight - ptrScrollableEl.offsetHeight)) ) { targetIsScrollable = true; } @@ -453,7 +456,7 @@ class PullToRefresh extends Framework7Class { ((!ptr.bottom && ptrScrollableEl.scrollTop > 0) || (ptr.bottom && ptrScrollableEl.scrollTop < - ptrScrollableEl.scrollHeight - ptrScrollableEl.offsetHeight)) + ptrScrollableEl.scrollHeight - ptrScrollableEl.offsetHeight)) ) { targetIsScrollable = true; } From 5375e624b1c389a6da7091320491ada816d15df6 Mon Sep 17 00:00:00 2001 From: huajiqaq Date: Fri, 2 Jan 2026 17:49:58 +0800 Subject: [PATCH 2/4] fix(core): add duplicate initialization guards for specific components, fix data-table-class reinit bug, add swiper init method, add toolbar scrollable width-zero warning --- .../components/data-table/data-table-class.js | 13 +++++-------- src/core/components/form/form.js | 6 ++++++ .../infinite-scroll/infinite-scroll.js | 2 ++ src/core/components/swiper/swiper.js | 19 ++++++++++++++----- src/core/components/toolbar/toolbar.js | 3 +++ 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/core/components/data-table/data-table-class.js b/src/core/components/data-table/data-table-class.js index 7f6d812a8..99d348df2 100644 --- a/src/core/components/data-table/data-table-class.js +++ b/src/core/components/data-table/data-table-class.js @@ -4,6 +4,11 @@ import Framework7Class from '../../shared/class.js'; class DataTable extends Framework7Class { constructor(app, params = {}) { + // El + const $el = $(params.el); + // Because temporarily does not support virtual lists, so if the element does not exist, we can return directly. + if ($el[0].f7DataTable) return $el[0].f7DataTable; + super(params, [app]); const table = this; @@ -15,19 +20,11 @@ class DataTable extends Framework7Class { table.params = extend(defaults, params); - // El - const $el = $(table.params.el); if ($el.length === 0) return undefined; table.$el = $el; table.el = $el[0]; - if (table.$el[0].f7DataTable) { - const instance = table.$el[0].f7DataTable; - table.destroy(); - return instance; - } - table.$el[0].f7DataTable = table; extend(table, { diff --git a/src/core/components/form/form.js b/src/core/components/form/form.js index f111ae7c3..5fb7081c7 100644 --- a/src/core/components/form/form.js +++ b/src/core/components/form/form.js @@ -68,6 +68,10 @@ const FormStorage = { const $formEl = $(formEl); const formId = $formEl.attr('id'); if (!formId) return; + // Check using data attribute + if ($formEl[0]._f7FormStorageInitialized) { + return; + } const initialData = app.form.getFormData(formId); if (initialData) { app.form.fillFromData($formEl, initialData); @@ -80,10 +84,12 @@ const FormStorage = { app.emit('formStoreData', $formEl[0], data); } $formEl.on('change submit', store); + $formEl[0]._f7FormStorageInitialized = true; // Set flag }, destroy(formEl) { const $formEl = $(formEl); $formEl.off('change submit'); + delete $formEl[0]._f7FormStorageInitialized; // Clear flag }, }; diff --git a/src/core/components/infinite-scroll/infinite-scroll.js b/src/core/components/infinite-scroll/infinite-scroll.js index 59a177f55..230afe69a 100644 --- a/src/core/components/infinite-scroll/infinite-scroll.js +++ b/src/core/components/infinite-scroll/infinite-scroll.js @@ -42,6 +42,8 @@ const InfiniteScroll = { app.infiniteScroll.handle(this, e); } $el.each((element) => { + // skip if already initialized + if (element.f7InfiniteScrollHandler) return; element.f7InfiniteScrollHandler = scrollHandler; element.addEventListener('scroll', element.f7InfiniteScrollHandler); }); diff --git a/src/core/components/swiper/swiper.js b/src/core/components/swiper/swiper.js index 5c72e8f43..f35d8b616 100644 --- a/src/core/components/swiper/swiper.js +++ b/src/core/components/swiper/swiper.js @@ -4,6 +4,7 @@ import Swiper from 'swiper/bundle'; import { register } from 'swiper/element/bundle'; import $ from '../../shared/dom7.js'; import ConstructorMethods from '../../shared/constructor-methods.js'; +import { extend } from '../../shared/utils.js'; register(); @@ -19,8 +20,14 @@ function initSwiper(swiperEl) { const app = this; const $swiperEl = $(swiperEl); if ($swiperEl.length === 0) return; - const isElement = $swiperEl[0].swiper && $swiperEl[0].swiper.isElement; + if (!$swiperEl[0].swiper) return; if ($swiperEl[0].swiper && !$swiperEl[0].swiper.isElement) return; + + // skip if already initialized + if ($swiperEl[0]._f7SwiperInitialized) return; + $swiperEl[0]._f7SwiperInitialized = true; + + const isElement = $swiperEl[0].swiper && $swiperEl[0].swiper.isElement; let initialSlide; let params = {}; let isTabs; @@ -113,11 +120,13 @@ export default { }, create() { const app = this; - app.swiper = ConstructorMethods({ - defaultSelector: '.swiper', + app.swiper = extend(ConstructorMethods({ + defaultSelector: ".swiper", constructor: Swiper, - domProp: 'swiper', - }); + domProp: "swiper" + }), { + init: initSwiper.bind(app) + }) }, on: { pageMounted(page) { diff --git a/src/core/components/toolbar/toolbar.js b/src/core/components/toolbar/toolbar.js index ca129d6a7..97c7691fe 100644 --- a/src/core/components/toolbar/toolbar.js +++ b/src/core/components/toolbar/toolbar.js @@ -45,6 +45,9 @@ const Toolbar = { let highlightTranslate; if ($tabbarEl.hasClass('tabbar-scrollable') && $activeLink && $activeLink[0]) { + if ($activeLink[0].offsetWidth == 0) { + console.warn("ToolBar: ToolBar's scrollable indicator width is 0 because the first active tab width measured as 0."); + }; highlightWidth = `${$activeLink[0].offsetWidth}px`; highlightTranslate = `${$activeLink[0].offsetLeft}px`; } else { From bc93d66dbdb7e8033d8d52b03578ac9f39c2fb3e Mon Sep 17 00:00:00 2001 From: huajiqaq Date: Fri, 2 Jan 2026 20:33:40 +0800 Subject: [PATCH 3/4] fix(page-content): ensure vue/react/svelte dynamic PTR/infinite loading setup after pageInit, remove undocumented vue event listener not in docs --- src/react/components/page-content.jsx | 6 ++++++ src/svelte/components/page-content.svelte | 6 ++++++ src/vue/components/page-content.vue | 16 ++++++---------- src/vue/components/page.vue | 12 ++++++------ 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/react/components/page-content.jsx b/src/react/components/page-content.jsx index 192be45db..e3d4edbfc 100644 --- a/src/react/components/page-content.jsx +++ b/src/react/components/page-content.jsx @@ -104,9 +104,15 @@ const PageContent = (props) => { f7.on('ptrPullEnd', onPtrPullEnd); f7.on('ptrRefresh', onPtrRefresh); f7.on('ptrDone', onPtrDone); + // PTR only initializes in pageInit callback. + // If page-content is added after page initialization, check if PTR exists. + // If not, create it manually. + f7.ptr.create(elRef.current); } if (infinite) { f7.on('infinite', onInfinite); + // Same logic applies to infinite scroll + f7.infiniteScroll.create(elRef.current); } }); }; diff --git a/src/svelte/components/page-content.svelte b/src/svelte/components/page-content.svelte index ba56d759d..85f2ed515 100644 --- a/src/svelte/components/page-content.svelte +++ b/src/svelte/components/page-content.svelte @@ -91,9 +91,15 @@ app.f7.on('ptrPullEnd', onPtrPullEnd); app.f7.on('ptrRefresh', onPtrRefresh); app.f7.on('ptrDone', onPtrDone); + // PTR only initializes in pageInit callback. + // If page-content is added after page initialization, check if PTR exists. + // If not, create it manually. + app.f7.ptr.create(ptrEl); } if (infinite) { app.f7.on('infinite', onInfinite); + // Same logic applies to infinite scroll + app.f7.infiniteScroll.create(ptrEl); } } function destroyPageContent() { diff --git a/src/vue/components/page-content.vue b/src/vue/components/page-content.vue index 5305a7327..fbe60ade7 100644 --- a/src/vue/components/page-content.vue +++ b/src/vue/components/page-content.vue @@ -71,11 +71,6 @@ export default { 'ptr:refresh', 'ptr:done', 'infinite', - 'ptrPullStart', - 'ptrPullMove', - 'ptrPullEnd', - 'ptrRefresh', - 'ptrDone', 'tab:hide', 'tab:show', ], @@ -85,27 +80,22 @@ export default { const onPtrPullStart = (el) => { if (elRef.value !== el) return; emit('ptr:pullstart'); - emit('ptrPullStart'); }; const onPtrPullMove = (el) => { if (elRef.value !== el) return; emit('ptr:pullmove'); - emit('ptrPullMove'); }; const onPtrPullEnd = (el) => { if (elRef.value !== el) return; emit('ptr:pullend'); - emit('ptrPullEnd'); }; const onPtrRefresh = (el, done) => { if (elRef.value !== el) return; emit('ptr:refresh', done); - emit('ptrRefresh', done); }; const onPtrDone = (el) => { if (elRef.value !== el) return; emit('ptr:done'); - emit('ptrDone'); }; const onInfinite = (el) => { if (elRef.value !== el) return; @@ -122,9 +112,15 @@ export default { f7.on('ptrPullEnd', onPtrPullEnd); f7.on('ptrRefresh', onPtrRefresh); f7.on('ptrDone', onPtrDone); + // PTR only initializes in pageInit callback. + // If page-content is added after page initialization, check if PTR exists. + // If not, create it manually. + f7.ptr.create(elRef.value); } if (props.infinite) { f7.on('infinite', onInfinite); + // Same logic applies to infinite scroll + f7.infiniteScroll.create(elRef.value); } }); }); diff --git a/src/vue/components/page.vue b/src/vue/components/page.vue index 642846148..1df9d5a39 100644 --- a/src/vue/components/page.vue +++ b/src/vue/components/page.vue @@ -376,12 +376,12 @@ export default { hideToolbarOnScroll: props.hideToolbarOnScroll, messagesContent: props.messagesContent || hasMessages, loginScreen: props.loginScreen, - onPtrPullStart, - onPtrPullMove, - onPtrPullEnd, - onPtrRefresh, - onPtrDone, - onInfinite, + "onPtr:pullstart": onPtrPullStart, + "onPtr:pullmove": onPtrPullMove, + "onPtr:pullend": onPtrPullEnd, + "onPtr:refresh": onPtrRefresh, + "onPtr:done": onPtrDone, + "onInfinite": onInfinite }, () => [slotsStatic && slotsStatic(), staticList], ), From 504366c97a99d81396170a27259a10da1d7899ee Mon Sep 17 00:00:00 2001 From: huajiqaq Date: Fri, 2 Jan 2026 20:33:50 +0800 Subject: [PATCH 4/4] fix(tabs): ensure vue/react/svelte dynamic swiper initialization after swipeable property is set post-pageInit --- src/react/components/tabs.jsx | 8 ++++++++ src/svelte/components/tabs.svelte | 21 ++++++++++++--------- src/vue/components/tabs.vue | 16 ++++++++++------ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/react/components/tabs.jsx b/src/react/components/tabs.jsx index b0620e373..7f814c2b6 100644 --- a/src/react/components/tabs.jsx +++ b/src/react/components/tabs.jsx @@ -3,6 +3,7 @@ import { useIsomorphicLayoutEffect } from '../shared/use-isomorphic-layout-effec import { classNames, getExtraAttrs } from '../shared/utils.js'; import { colorClasses } from '../shared/mixins.js'; import { TabsSwipeableContext } from '../shared/tabs-swipeable-context.js'; +import { f7ready, f7 } from '../shared/f7.js'; import { setRef } from '../shared/set-ref.js'; /* dts-imports import { SwiperOptions } from 'swiper'; @@ -28,6 +29,13 @@ const Tabs = (props) => { const elRef = useRef(null); useIsomorphicLayoutEffect(() => { + if (swipeable) { + f7ready(() => { + // It only initializes in pageInit callback + // We may need to manually call init() to update the instance + f7.swiper.init(elRef.current) + }); + } if (!swipeable || !swiperParams) return; if (!elRef.current) return; Object.assign(elRef.current, swiperParams); diff --git a/src/svelte/components/tabs.svelte b/src/svelte/components/tabs.svelte index 1a44e247a..38c0dd84e 100644 --- a/src/svelte/components/tabs.svelte +++ b/src/svelte/components/tabs.svelte @@ -1,6 +1,7 @@ diff --git a/src/vue/components/tabs.vue b/src/vue/components/tabs.vue index 8d8eb9545..e5bbcd495 100644 --- a/src/vue/components/tabs.vue +++ b/src/vue/components/tabs.vue @@ -4,12 +4,8 @@ - +
@@ -20,6 +16,7 @@ import { computed, ref, onMounted, provide } from 'vue'; import { classNames } from '../shared/utils.js'; import { colorClasses, colorProps } from '../shared/mixins.js'; +import { f7ready, f7 } from '../shared/f7.js'; export default { name: 'f7-tabs', @@ -37,6 +34,13 @@ export default { const elRef = ref(null); onMounted(() => { + if (props.swipeable) { + f7ready(() => { + // It only initializes in pageInit callback + // We may need to manually call init() to update the instance + f7.swiper.init(elRef.value) + }); + } if (!props.swipeable || !props.swiperParams) return; if (!elRef.value) return; Object.assign(elRef.value, props.swiperParams);