react-nativescript
Version:
React renderer for NativeScript
389 lines • 13.7 kB
JavaScript
import { getViewClass, getViewMeta, normalizeElementName, } from './registry';
import { ELEMENT_REF } from './runtimeHelpers';
import { debug } from '../shared';
import { ObservableArray } from '@nativescript/core';
import { unsetValue } from '@nativescript/core/ui/core/properties';
/*
* I had some difficulty importing this as:
* import set from 'set-value';
* I believe that turning on `"esModuleInterop": true` in tsconfig.json would allow us to use the default import.
* But maybe this is just a problem in the Webpack domain.
* ... And later as:
* import set = require('set-value');
*/
import { default as set } from "set-value";
import { warn } from '../../shared/Logger';
let nodeId = 0;
export class NSVNode {
constructor(nodeType) {
this.parentNode = null;
this.childNodes = [];
this.nextSibling = null;
this.prevSibling = null;
this.nodeType = nodeType;
this.nodeId = nodeId++;
}
get text() {
return this._text;
}
set text(t) {
this._text = t;
}
get firstChild() {
return this.childNodes.length ? this.childNodes[0] : null;
}
get lastChild() {
return this.childNodes.length
? this.childNodes[this.childNodes.length - 1]
: null;
}
toString() {
return this.toString();
}
}
export class NSVElement extends NSVNode {
constructor(tagName) {
super("element" /* NSVNodeTypes.ELEMENT */);
this._tagName = normalizeElementName(tagName);
const viewClass = getViewClass(tagName);
this._nativeView = new viewClass();
this._nativeView[ELEMENT_REF] = this;
}
get tagName() {
return this._tagName;
}
get nativeView() {
return this._nativeView;
}
get style() {
return this.nativeView.style;
}
set style(inlineStyle) {
this.nativeView.style = inlineStyle;
}
get text() {
return this.nativeView.text;
}
set text(t) {
super.text = t;
this.nativeView.text = t;
}
get meta() {
if (this._meta) {
return this._meta;
}
return (this._meta = getViewMeta(this.tagName));
}
get eventListeners() {
if (!this._eventListeners) {
this._eventListeners = new Map();
}
return this._eventListeners;
}
addEventListener(event, handler, options = {}) {
const { capture, once } = options;
if (capture) {
debug('Bubble propagation is not supported');
return;
}
if (once) {
const oldHandler = handler;
const self = this;
handler = (...args) => {
const res = oldHandler.call(null, ...args);
if (res !== null) {
self.removeEventListener(event, handler);
}
};
}
this.nativeView.addEventListener(event, handler);
this.eventListeners.set(event, handler);
}
removeEventListener(event, handler) {
this.eventListeners.delete(event);
this.nativeView.removeEventListener(event, handler);
}
dispatchEvent(event) {
this.nativeView.notify({ eventName: event, object: this.nativeView });
}
getAttribute(name) {
return this.nativeView[name];
}
setAttribute(name, value) {
if (name === "nodeRole" && typeof value === "string") {
this.nodeRole = value;
return;
}
/**
* The 'ios' and 'android' properties (e.g. on ActionItem)
* are readonly, so we need to assign one level lower.
*/
if (name === "ios" && value) {
Object.keys(value).forEach((key) => {
set(this.nativeView.ios, key, value);
});
return;
}
if (name === "android" && value) {
Object.keys(value).forEach((key) => {
set(this.nativeView.android, key, value);
});
return;
}
set(this.nativeView, name, value);
}
removeAttribute(name) {
if (name === "nodeRole") {
this.nodeRole = void 0;
return;
}
// potential issue: unsetValue is an empty object
// not all properties/attributes may know/check for this
set(this.nativeView, name, unsetValue);
// originally we deleted the property, but in case of built-in properties
// this would break them. For example, deleting the padding property
// will prevent us from changing the padding once we deleted it
// that's not the expected behaviour.
// unset(this.nativeView, name)
}
insertBefore(el, anchor) {
if (!anchor) {
return this.appendChild(el);
}
const refIndex = this.childNodes.findIndex((node) => node.nodeId === anchor.nodeId);
if (refIndex === -1) {
return this.appendChild(el);
}
if (el.parentNode) {
el.parentNode.removeChild(el);
}
this.childNodes.splice(refIndex, 0, el);
el.parentNode = this;
// find index to use for the native view, since non-visual nodes
// (comment/text don't exist in the native view hierarchy)
// todo: potentially refactor based on my benchmark:
// https://www.measurethat.net/Benchmarks/Show/7450/0/filter-findindex
const trueIndex = this.childNodes
.filter((node) => node.nodeType === "element" /* NSVNodeTypes.ELEMENT */)
.findIndex((node) => node.nodeId === el.nodeId);
this.addChild(el, trueIndex);
}
appendChild(el) {
this.childNodes.push(el);
el.parentNode = this;
this.addChild(el);
}
removeChild(el) {
const index = this.childNodes.findIndex((node) => node.nodeId === el.nodeId);
if (index > -1) {
this.childNodes.splice(index, 1);
el.parentNode = null;
if (el.nodeType === "element" /* NSVNodeTypes.ELEMENT */) {
removeChild(el, this); // Removing a child span takes us down here
}
else if (el.nodeType === "text" /* NSVNodeTypes.TEXT */) {
this.updateText();
}
}
}
// abstracted from appendChild, and insertBefore to avoid code duplication
addChild(el, atIndex) {
if (el.nodeType === "element" /* NSVNodeTypes.ELEMENT */) {
addChild(el, this, atIndex);
}
else if (el.nodeType === "text" /* NSVNodeTypes.TEXT */) {
this.updateText();
}
}
updateText() {
this.setAttribute('text', this.childNodes
.filter((node) => node.nodeType === "text" /* NSVNodeTypes.TEXT */)
.reduce((text, currentNode) => {
return text + currentNode.text;
}, ''));
}
toString() {
return "NSVElement:" + this.nativeView.toString();
}
}
export class NSVComment extends NSVNode {
constructor(text) {
super("comment" /* NSVNodeTypes.COMMENT */);
this.text = text;
}
toString() {
return "NSVComment:" + `"` + this.text + `"`;
}
}
export class NSVText extends NSVNode {
constructor(text) {
super("text" /* NSVNodeTypes.TEXT */);
this.text = text;
}
toString() {
return "NSVText:" + `"` + this.text + `"`;
}
}
export class NSVRoot extends NSVNode {
constructor() {
super("root" /* NSVNodeTypes.ROOT */);
}
setBaseRef(el) {
// console.log(`NSVRoot->appendChild(${el.nodeType})`)
if (el instanceof NSVElement) {
this.baseRef = el;
}
// no-op
}
toString() {
if (this.baseRef) {
return "NSVRoot:" + this.baseRef.toString();
}
else {
return "NSVRoot:" + "null";
}
}
}
function addChild(child, parent, atIndex) {
if (__TEST__)
return;
// debug(
// `...addChild( ${child.tagName}(${child.nodeId}), ${parent.tagName}(${
// parent.nodeId
// }), ${atIndex} )`
// )
if (child.meta.viewFlags & 1 /* NSVViewFlags.SKIP_ADD_TO_DOM */) {
// debug('SKIP_ADD_TO_DOM')
return;
}
const parentView = parent.nativeView;
const childView = child.nativeView;
if (parent.meta.viewFlags & 8 /* NSVViewFlags.NO_CHILDREN */) {
// debug('NO_CHILDREN')
return;
}
if (parent.meta.nodeOps) {
return parent.meta.nodeOps.insert(child, parent, atIndex);
}
const nodeRole = child.nodeRole;
if (nodeRole) {
return addChildByNodeRole(nodeRole, childView, parentView, atIndex);
}
if (parent.meta.viewFlags & 4 /* NSVViewFlags.LAYOUT_VIEW */) {
if (atIndex) {
parentView.insertChild(childView, atIndex);
}
else {
parentView.addChild(childView);
}
}
else if (parent.meta.viewFlags & 2 /* NSVViewFlags.CONTENT_VIEW */) {
parentView.content = childView;
}
else {
parentView._addChildFromBuilder(childView.constructor.name, childView);
}
}
function removeChild(child, parent) {
if (__TEST__)
return;
// debug(
// `...removeChild( ${child.tagName}(${child.nodeId}), ${parent.tagName}(${
// parent.nodeId
// }) )`
// )
if (child.meta.viewFlags & 1 /* NSVViewFlags.SKIP_ADD_TO_DOM */) {
// debug('SKIP_ADD_TO_DOM')
return;
}
if (parent.meta.viewFlags & 8 /* NSVViewFlags.NO_CHILDREN */) {
// debug('NO_CHILDREN')
return;
}
if (parent.meta.nodeOps) {
return parent.meta.nodeOps.remove(child, parent);
}
const parentView = parent.nativeView;
const childView = child.nativeView;
const nodeRole = child.nodeRole;
if (nodeRole) {
return removeChildByNodeRole(nodeRole, childView, parentView);
}
if (parent.meta.viewFlags & 4 /* NSVViewFlags.LAYOUT_VIEW */) {
parentView.removeChild(childView);
}
else if (parent.meta.viewFlags & 2 /* NSVViewFlags.CONTENT_VIEW */) {
parentView.content = null;
}
else {
// Removing a child span takes us down here
parentView._removeView(childView);
}
}
function addChildByNodeRole(nodeRole, childView, parentView, atIndex) {
const childrenSetter = parentView[nodeRole];
if (typeof childrenSetter !== "undefined" && typeof childrenSetter.length !== "undefined") {
// Treat as if it's an array.
const childrenSetterLength = parentView[nodeRole].length;
const atSafeIndex = typeof atIndex === "undefined" ? childrenSetterLength : atIndex;
if (childrenSetter instanceof ObservableArray) {
parentView[nodeRole].splice(atSafeIndex, 0, childView);
}
else if (Array.isArray(childrenSetter)) {
parentView[nodeRole] = [...parentView[nodeRole]].splice(atSafeIndex, 0, childView);
}
else {
if (__DEV__) {
warn(`parentView "${parentView.constructor.name}" had a value for nodeRole "${nodeRole}" ` +
`that had a "length" property yet did not conform to Array or ObservableArray. Cannot add child. ` +
`Please explicitly implement nodeOps.insert() for the parentView.`);
}
}
}
else {
/*
* Treat it as if it's simply a setter.
* This assumes (quite fairly) that the plugin author is not delegating to us the responsibility
* of initialising an array for childrenSetter.
*/
parentView[nodeRole] = childView;
}
}
function removeChildByNodeRole(nodeRole, childView, parentView) {
const childrenSetter = parentView[nodeRole];
if (typeof childrenSetter !== "undefined" && typeof childrenSetter.indexOf === "function") {
// Treat as if it's an array.
const childIndex = parentView[nodeRole].indexOf(childView);
if (childrenSetter instanceof ObservableArray) {
parentView[nodeRole].splice(childIndex, 1);
}
else if (Array.isArray(childrenSetter)) {
parentView[nodeRole] = [...parentView[nodeRole]].splice(childIndex, 1);
}
else {
if (__DEV__) {
warn(`parentView "${parentView.constructor.name}" had a value for nodeRole "${nodeRole}" ` +
`that had an "indexOf" property yet did not conform to Array or ObservableArray. Cannot add childView "${childView.constructor.name}". ` +
`Please explicitly implement nodeOps.remove() for the parentView.`);
}
}
}
else {
/*
* Treat it as if it's simply a setter.
* We can't use unsetValue here, because the childrenSetter is not necessarily a Property (which indeed is the case for FormattedString.spans).
* TODO: If there's a way to determine whether the childrenSetter is a Property, I'd be very happy to run that first check and use unsetValue.
*/
const defaultValueForChildrenSetter = parentView.__proto__[nodeRole];
try {
parentView[nodeRole] = defaultValueForChildrenSetter;
}
catch (e) {
if (__DEV__) {
warn(`parentView "${parentView.constructor.name}" failed to remove childView "${childView.constructor.name}", given nodeRole "${nodeRole}" ` +
`Please explicitly implement nodeOps.remove() for the parentView.`);
}
}
}
}
//# sourceMappingURL=nodes.js.map