250 lines
8.5 KiB
JavaScript
250 lines
8.5 KiB
JavaScript
/* global document */
|
|
|
|
import { adapter as ProxyAdapterDom } from '../proxy-adapter-dom'
|
|
|
|
import { patchShowModal, getModalData } from './patch-page-show-modal'
|
|
|
|
patchShowModal()
|
|
|
|
// Svelte Native support
|
|
// =====================
|
|
//
|
|
// Rerendering Svelte Native page proves challenging...
|
|
//
|
|
// In NativeScript, pages are the top level component. They are normally
|
|
// introduced into NativeScript's runtime by its `navigate` function. This
|
|
// is how Svelte Natives handles it: it renders the Page component to a
|
|
// dummy fragment, and "navigate" to the page element thus created.
|
|
//
|
|
// As long as modifications only impact child components of the page, then
|
|
// we can keep the existing page and replace its content for HMR.
|
|
//
|
|
// However, if the page component itself is modified (including its system
|
|
// title bar), things get hairy...
|
|
//
|
|
// Apparently, the sole way of introducing a new page in a NS application is
|
|
// to navigate to it (no way to just replace it in its parent "element", for
|
|
// example). This is how it is done in NS's own "core" HMR.
|
|
//
|
|
// NOTE The last paragraph has not really been confirmed with NS6.
|
|
//
|
|
// Unfortunately the API they're using to do that is not public... Its various
|
|
// parts remain exposed though (but documented as private), so this exploratory
|
|
// work now relies on it. It might be fragile...
|
|
//
|
|
// The problem is that there is no public API that can navigate to a page and
|
|
// replace (like location.replace) the current history entry. Actually there
|
|
// is an active issue at NS asking for that. Incidentally, members of
|
|
// NativeScript-Vue have commented on the issue to weight in for it -- they
|
|
// probably face some similar challenge.
|
|
//
|
|
// https://github.com/NativeScript/NativeScript/issues/6283
|
|
|
|
const getNavTransition = ({ transition }) => {
|
|
if (typeof transition === 'string') {
|
|
transition = { name: transition }
|
|
}
|
|
return transition ? { animated: true, transition } : { animated: false }
|
|
}
|
|
|
|
export const adapter = class ProxyAdapterNative extends ProxyAdapterDom {
|
|
constructor(instance) {
|
|
super(instance)
|
|
|
|
this.nativePageElement = null
|
|
}
|
|
|
|
dispose() {
|
|
super.dispose()
|
|
this.releaseNativePageElement()
|
|
}
|
|
|
|
releaseNativePageElement() {
|
|
if (this.nativePageElement) {
|
|
// native cleaning will happen when navigating back from the page
|
|
this.nativePageElement = null
|
|
}
|
|
}
|
|
|
|
afterMount(target, anchor) {
|
|
// nativePageElement needs to be updated each time (only for page
|
|
// components, native component that are not pages follow normal flow)
|
|
//
|
|
// TODO quid of components that are initially a page, but then have the
|
|
// <page> tag removed while running? or the opposite?
|
|
//
|
|
// insertionPoint needs to be updated _only when the target changes_ --
|
|
// i.e. when the component is mount, i.e. (in svelte3) when the component
|
|
// is _created_, and svelte3 doesn't allow it to move afterward -- that
|
|
// is, insertionPoint only needs to be created once when the component is
|
|
// first mounted.
|
|
//
|
|
// TODO is it really true that components' elements cannot move in the
|
|
// DOM? what about keyed list?
|
|
//
|
|
|
|
const isNativePage =
|
|
(target.tagName === 'fragment' || target.tagName === 'frame') &&
|
|
target.firstChild &&
|
|
target.firstChild.tagName == 'page'
|
|
if (isNativePage) {
|
|
const nativePageElement = target.firstChild
|
|
this.nativePageElement = nativePageElement
|
|
} else {
|
|
// try to protect against components changing from page to no-page
|
|
// or vice versa -- see DEBUG 1 above. NOT TESTED so prolly not working
|
|
this.nativePageElement = null
|
|
super.afterMount(target, anchor)
|
|
}
|
|
}
|
|
|
|
rerender() {
|
|
const { nativePageElement } = this
|
|
if (nativePageElement) {
|
|
this.rerenderNative()
|
|
} else {
|
|
super.rerender()
|
|
}
|
|
}
|
|
|
|
rerenderNative() {
|
|
const { nativePageElement: oldPageElement } = this
|
|
const nativeView = oldPageElement.nativeView
|
|
const frame = nativeView.frame
|
|
if (frame) {
|
|
return this.rerenderPage(frame, nativeView)
|
|
}
|
|
const modalParent = nativeView._modalParent // FIXME private API
|
|
if (modalParent) {
|
|
return this.rerenderModal(modalParent, nativeView)
|
|
}
|
|
// wtf? hopefully a race condition with a destroyed component, so
|
|
// we have nothing more to do here
|
|
//
|
|
// for once, it happens when hot reloading dev deps, like this file
|
|
//
|
|
}
|
|
|
|
rerenderPage(frame, previousPageView) {
|
|
const isCurrentPage = frame.currentPage === previousPageView
|
|
if (isCurrentPage) {
|
|
const {
|
|
instance: { hotOptions },
|
|
} = this
|
|
const newPageElement = this.createPage()
|
|
if (!newPageElement) {
|
|
throw new Error('Failed to create updated page')
|
|
}
|
|
const isFirstPage = !frame.canGoBack()
|
|
const nativeView = newPageElement.nativeView
|
|
const navigationEntry = Object.assign(
|
|
{},
|
|
{
|
|
create: () => nativeView,
|
|
clearHistory: true,
|
|
},
|
|
getNavTransition(hotOptions)
|
|
)
|
|
|
|
if (isFirstPage) {
|
|
// NOTE not so sure of bellow with the new NS6 method for replace
|
|
//
|
|
// The "replacePage" strategy does not work on the first page
|
|
// of the stack.
|
|
//
|
|
// Resulting bug:
|
|
// - launch
|
|
// - change first page => HMR
|
|
// - navigate to other page
|
|
// - back
|
|
// => actual: back to OS
|
|
// => expected: back to page 1
|
|
//
|
|
// Fortunately, we can overwrite history in this case.
|
|
//
|
|
frame.navigate(navigationEntry)
|
|
} else {
|
|
frame.replacePage(navigationEntry)
|
|
}
|
|
} else {
|
|
const backEntry = frame.backStack.find(
|
|
({ resolvedPage: page }) => page === previousPageView
|
|
)
|
|
if (!backEntry) {
|
|
// well... looks like we didn't make it to history after all
|
|
return
|
|
}
|
|
// replace existing nativeView
|
|
const newPageElement = this.createPage()
|
|
if (newPageElement) {
|
|
backEntry.resolvedPage = newPageElement.nativeView
|
|
} else {
|
|
throw new Error('Failed to create updated page')
|
|
}
|
|
}
|
|
}
|
|
|
|
// modalParent is the page on which showModal(...) was called
|
|
// oldPageElement is the modal content, that we're actually trying to reload
|
|
rerenderModal(modalParent, modalView) {
|
|
const modalData = getModalData(modalView)
|
|
|
|
modalData.closeCallback = () => {
|
|
const nativePageElement = this.createPage()
|
|
if (!nativePageElement) {
|
|
throw new Error('Failed to created updated modal page')
|
|
}
|
|
const { nativeView } = nativePageElement
|
|
const { originalOptions } = modalData
|
|
// Options will get monkey patched again, the only work left for us
|
|
// is to try to reduce visual disturbances.
|
|
//
|
|
// FIXME Even that proves too much unfortunately... Apparently TNS
|
|
// does not respect the `animated` option in this context:
|
|
// https://docs.nativescript.org/api-reference/interfaces/_ui_core_view_base_.showmodaloptions#animated
|
|
//
|
|
const options = Object.assign({}, originalOptions, { animated: false })
|
|
modalParent.showModal(nativeView, options)
|
|
}
|
|
|
|
modalView.closeModal()
|
|
}
|
|
|
|
createPage() {
|
|
const {
|
|
instance: { refreshComponent },
|
|
} = this
|
|
const { nativePageElement: oldNativePageElement } = this
|
|
const oldNativeView = oldNativePageElement.nativeView
|
|
// rerender
|
|
const target = document.createElement('fragment')
|
|
|
|
// not using conservative for now, since there's nothing in place here to
|
|
// leverage it (yet?) -- and it might be easier to miss breakages in native
|
|
// only code paths
|
|
refreshComponent(target, null)
|
|
|
|
// this.nativePageElement is updated in afterMount, triggered by proxy / hooks
|
|
const newPageElement = this.nativePageElement
|
|
|
|
// svelte-native uses navigateFrom event + e.isBackNavigation to know when to $destroy the component.
|
|
// To keep that behaviour after refresh, we move event handler from old native view to the new one using
|
|
// __navigateFromHandler property that svelte-native provides us with.
|
|
const navigateFromHandler = oldNativeView.__navigateFromHandler
|
|
if (navigateFromHandler) {
|
|
oldNativeView.off('navigatedFrom', navigateFromHandler)
|
|
newPageElement.nativeView.on('navigatedFrom', navigateFromHandler)
|
|
newPageElement.nativeView.__navigateFromHandler = navigateFromHandler
|
|
delete oldNativeView.__navigateFromHandler
|
|
}
|
|
|
|
return newPageElement
|
|
}
|
|
|
|
renderError(err /* , target, anchor */) {
|
|
// TODO fallback on TNS error handler for now... at least our error
|
|
// is more informative
|
|
throw err
|
|
}
|
|
}
|