File size: 8,725 Bytes
bc20498 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
/* 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
}
}
|