You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

342 lines
11 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 }
}
// copied from TNS FrameBase.replacePage
//
// it is not public but there is a comment in there indicating it is for
// HMR (probably their own core HMR though)
//
// NOTE this "worked" in TNS 5, but not anymore in TNS 6: updated version bellow
//
// eslint-disable-next-line no-unused-vars
const replacePage_tns5 = (frame, newPageElement, hotOptions) => {
const currentBackstackEntry = frame._currentEntry
frame.navigationType = 2
frame.performNavigation({
isBackNavigation: false,
entry: {
resolvedPage: newPageElement.nativeView,
//
// entry: currentBackstackEntry.entry,
entry: Object.assign(
currentBackstackEntry.entry,
getNavTransition(hotOptions)
),
navDepth: currentBackstackEntry.navDepth,
fragmentTag: currentBackstackEntry.fragmentTag,
frameId: currentBackstackEntry.frameId,
},
})
}
// Updated for TNS v6
//
// https://github.com/NativeScript/NativeScript/blob/6.1.1/tns-core-modules/ui/frame/frame-common.ts#L656
const replacePage = (frame, newPageElement) => {
const currentBackstackEntry = frame._currentEntry
const newPage = newPageElement.nativeView
const newBackstackEntry = {
entry: currentBackstackEntry.entry,
resolvedPage: newPage,
navDepth: currentBackstackEntry.navDepth,
fragmentTag: currentBackstackEntry.fragmentTag,
frameId: currentBackstackEntry.frameId,
}
const navigationContext = {
entry: newBackstackEntry,
isBackNavigation: false,
navigationType: 2 /* NavigationType replace */,
}
frame._navigationQueue.push(navigationContext)
frame._processNextNavigationEntry()
}
export const adapter = class ProxyAdapterNative extends ProxyAdapterDom {
constructor(instance) {
super(instance)
this.nativePageElement = null
this.originalNativeView = null
this.navigatedFromHandler = null
this.relayNativeNavigatedFrom = this.relayNativeNavigatedFrom.bind(this)
}
dispose() {
super.dispose()
this.releaseNativePageElement()
}
releaseNativePageElement() {
if (this.nativePageElement) {
// native cleaning will happen when navigating back from the page
this.nativePageElement = null
}
}
// svelte-native uses navigateFrom event + e.isBackNavigation to know
// when to $destroy the component -- but we don't want our proxy instance
// destroyed when we renavigate to the same page for navigation purposes!
interceptPageNavigation(pageElement) {
const originalNativeView = pageElement.nativeView
const { on } = originalNativeView
const ownOn = originalNativeView.hasOwnProperty('on')
// tricks svelte-native into giving us its handler
originalNativeView.on = function(type, handler) {
if (type === 'navigatedFrom') {
this.navigatedFromHandler = handler
if (ownOn) {
originalNativeView.on = on
} else {
delete originalNativeView.on
}
} else {
//some other handler wireup, we will just pass it on.
if (on) {
on(type, handler)
}
}
}
}
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.interceptPageNavigation(nativePageElement)
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()
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.
//
const nativeView = newPageElement.nativeView
frame.navigate(
Object.assign(
{},
{
create: () => nativeView,
clearHistory: true,
},
getNavTransition(hotOptions)
)
)
} else {
replacePage(frame, newPageElement, hotOptions)
}
} 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, relayNativeNavigatedFrom } = this
const oldNativeView = nativePageElement.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
// update event proxy
oldNativeView.off('navigatedFrom', relayNativeNavigatedFrom)
nativePageElement.nativeView.on('navigatedFrom', relayNativeNavigatedFrom)
return newPageElement
}
relayNativeNavigatedFrom({ isBackNavigation }) {
const { originalNativeView, navigatedFromHandler } = this
if (!isBackNavigation) {
return
}
if (originalNativeView) {
const { off } = originalNativeView
const ownOff = originalNativeView.hasOwnProperty('off')
originalNativeView.off = function() {
this.navigatedFromHandler = null
if (ownOff) {
originalNativeView.off = off
} else {
delete originalNativeView.off
}
}
}
if (navigatedFromHandler) {
return navigatedFromHandler.apply(this, arguments)
}
}
renderError(err /* , target, anchor */) {
// TODO fallback on TNS error handler for now... at least our error
// is more informative
throw err
}
}