blob: 8e7b5721e4f30edecf2bb91e313dc1c44decde36 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Node from './Node'
import {
getDoc,
getTaskCenter,
linkParent,
nextElement,
previousElement,
insertIndex,
moveIndex,
removeIndex
} from './operation'
import { uniqueId, isEmpty } from '../shared/utils'
import { getWeexElement, setElement } from './WeexElement'
const DEFAULT_TAG_NAME = 'div'
const BUBBLE_EVENTS = [
'click', 'longpress', 'touchstart', 'touchmove', 'touchend',
'panstart', 'panmove', 'panend', 'horizontalpan', 'verticalpan', 'swipe'
]
function registerNode (docId, node) {
const doc = getDoc(docId)
doc.nodeMap[node.nodeId] = node
}
export default class Element extends Node {
constructor (type = DEFAULT_TAG_NAME, props, isExtended) {
super()
const WeexElement = getWeexElement(type)
if (WeexElement && !isExtended) {
return new WeexElement(type, props, true)
}
props = props || {}
this.nodeType = 1
this.nodeId = uniqueId()
this.ref = this.nodeId
this.type = type
this.attr = props.attr || {}
this.style = props.style || {}
this.classStyle = props.classStyle || {}
this.event = {}
this.children = []
this.pureChildren = []
}
/**
* Append a child node.
* @param {object} node
* @return {undefined | number} the signal sent by native
*/
appendChild (node) {
if (node.parentNode && node.parentNode !== this) {
return
}
/* istanbul ignore else */
if (!node.parentNode) {
linkParent(node, this)
insertIndex(node, this.children, this.children.length, true)
if (this.docId) {
registerNode(this.docId, node)
}
if (node.nodeType === 1) {
insertIndex(node, this.pureChildren, this.pureChildren.length)
const taskCenter = getTaskCenter(this.docId)
if (taskCenter) {
return taskCenter.send(
'dom',
{ action: 'addElement' },
[this.ref, node.toJSON(), -1]
)
}
}
}
else {
moveIndex(node, this.children, this.children.length, true)
if (node.nodeType === 1) {
const index = moveIndex(node, this.pureChildren, this.pureChildren.length)
const taskCenter = getTaskCenter(this.docId)
if (taskCenter && index >= 0) {
return taskCenter.send(
'dom',
{ action: 'moveElement' },
[node.ref, this.ref, index]
)
}
}
}
}
/**
* Insert a node before specified node.
* @param {object} node
* @param {object} before
* @return {undefined | number} the signal sent by native
*/
insertBefore (node, before) {
if (node.parentNode && node.parentNode !== this) {
return
}
if (node === before || (node.nextSibling && node.nextSibling === before)) {
return
}
if (!node.parentNode) {
linkParent(node, this)
insertIndex(node, this.children, this.children.indexOf(before), true)
if (this.docId) {
registerNode(this.docId, node)
}
if (node.nodeType === 1) {
const pureBefore = nextElement(before)
const index = insertIndex(
node,
this.pureChildren,
pureBefore
? this.pureChildren.indexOf(pureBefore)
: this.pureChildren.length
)
const taskCenter = getTaskCenter(this.docId)
if (taskCenter) {
return taskCenter.send(
'dom',
{ action: 'addElement' },
[this.ref, node.toJSON(), index]
)
}
}
}
else {
moveIndex(node, this.children, this.children.indexOf(before), true)
if (node.nodeType === 1) {
const pureBefore = nextElement(before)
/* istanbul ignore next */
const index = moveIndex(
node,
this.pureChildren,
pureBefore
? this.pureChildren.indexOf(pureBefore)
: this.pureChildren.length
)
const taskCenter = getTaskCenter(this.docId)
if (taskCenter && index >= 0) {
return taskCenter.send(
'dom',
{ action: 'moveElement' },
[node.ref, this.ref, index]
)
}
}
}
}
/**
* Insert a node after specified node.
* @param {object} node
* @param {object} after
* @return {undefined | number} the signal sent by native
*/
insertAfter (node, after) {
if (node.parentNode && node.parentNode !== this) {
return
}
if (node === after || (node.previousSibling && node.previousSibling === after)) {
return
}
if (!node.parentNode) {
linkParent(node, this)
insertIndex(node, this.children, this.children.indexOf(after) + 1, true)
/* istanbul ignore else */
if (this.docId) {
registerNode(this.docId, node)
}
if (node.nodeType === 1) {
const index = insertIndex(
node,
this.pureChildren,
this.pureChildren.indexOf(previousElement(after)) + 1
)
const taskCenter = getTaskCenter(this.docId)
/* istanbul ignore else */
if (taskCenter) {
return taskCenter.send(
'dom',
{ action: 'addElement' },
[this.ref, node.toJSON(), index]
)
}
}
}
else {
moveIndex(node, this.children, this.children.indexOf(after) + 1, true)
if (node.nodeType === 1) {
const index = moveIndex(
node,
this.pureChildren,
this.pureChildren.indexOf(previousElement(after)) + 1
)
const taskCenter = getTaskCenter(this.docId)
if (taskCenter && index >= 0) {
return taskCenter.send(
'dom',
{ action: 'moveElement' },
[node.ref, this.ref, index]
)
}
}
}
}
/**
* Remove a child node, and decide whether it should be destroyed.
* @param {object} node
* @param {boolean} preserved
*/
removeChild (node, preserved) {
if (node.parentNode) {
removeIndex(node, this.children, true)
if (node.nodeType === 1) {
removeIndex(node, this.pureChildren)
const taskCenter = getTaskCenter(this.docId)
if (taskCenter) {
taskCenter.send(
'dom',
{ action: 'removeElement' },
[node.ref]
)
}
}
}
if (!preserved) {
node.destroy()
}
}
/**
* Clear all child nodes.
*/
clear () {
const taskCenter = getTaskCenter(this.docId)
/* istanbul ignore else */
if (taskCenter) {
this.pureChildren.forEach(node => {
taskCenter.send(
'dom',
{ action: 'removeElement' },
[node.ref]
)
})
}
this.children.forEach(node => {
node.destroy()
})
this.children.length = 0
this.pureChildren.length = 0
}
/**
* Set an attribute, and decide whether the task should be send to native.
* @param {string} key
* @param {string | number} value
* @param {boolean} silent
*/
setAttr (key, value, silent) {
if (this.attr[key] === value && silent !== false) {
return
}
this.attr[key] = value
const taskCenter = getTaskCenter(this.docId)
if (!silent && taskCenter) {
const result = {}
result[key] = value
taskCenter.send(
'dom',
{ action: 'updateAttrs' },
[this.ref, result]
)
}
}
/**
* Set batched attributes.
* @param {object} batchedAttrs
* @param {boolean} silent
*/
setAttrs (batchedAttrs, silent) {
if (isEmpty(batchedAttrs)) return
const mutations = {}
for (const key in batchedAttrs) {
if (this.attr[key] !== batchedAttrs[key]) {
this.attr[key] = batchedAttrs[key]
mutations[key] = batchedAttrs[key]
}
}
if (!isEmpty(mutations)) {
const taskCenter = getTaskCenter(this.docId)
if (!silent && taskCenter) {
taskCenter.send(
'dom',
{ action: 'updateAttrs' },
[this.ref, mutations]
)
}
}
}
/**
* Set a style property, and decide whether the task should be send to native.
* @param {string} key
* @param {string | number} value
* @param {boolean} silent
*/
setStyle (key, value, silent) {
if (this.style[key] === value && silent !== false) {
return
}
this.style[key] = value
const taskCenter = getTaskCenter(this.docId)
if (!silent && taskCenter) {
const result = {}
result[key] = value
taskCenter.send(
'dom',
{ action: 'updateStyle' },
[this.ref, result]
)
}
}
/**
* Set batched style properties.
* @param {object} batchedStyles
* @param {boolean} silent
*/
setStyles (batchedStyles, silent) {
if (isEmpty(batchedStyles)) return
const mutations = {}
for (const key in batchedStyles) {
if (this.style[key] !== batchedStyles[key]) {
this.style[key] = batchedStyles[key]
mutations[key] = batchedStyles[key]
}
}
if (!isEmpty(mutations)) {
const taskCenter = getTaskCenter(this.docId)
if (!silent && taskCenter) {
taskCenter.send(
'dom',
{ action: 'updateStyle' },
[this.ref, mutations]
)
}
}
}
/**
* Set style properties from class.
* @param {object} classStyle
*/
setClassStyle (classStyle) {
// reset previous class style to empty string
for (const key in this.classStyle) {
this.classStyle[key] = ''
}
Object.assign(this.classStyle, classStyle)
const taskCenter = getTaskCenter(this.docId)
if (taskCenter) {
taskCenter.send(
'dom',
{ action: 'updateStyle' },
[this.ref, this.toStyle()]
)
}
}
/**
* Add an event handler.
* @param {string} event type
* @param {function} event handler
*/
addEvent (type, handler, params) {
if (!this.event) {
this.event = {}
}
if (!this.event[type]) {
this.event[type] = { handler, params }
const taskCenter = getTaskCenter(this.docId)
if (taskCenter) {
taskCenter.send(
'dom',
{ action: 'addEvent' },
[this.ref, type]
)
}
}
}
/**
* Remove an event handler.
* @param {string} event type
*/
removeEvent (type) {
if (this.event && this.event[type]) {
delete this.event[type]
const taskCenter = getTaskCenter(this.docId)
if (taskCenter) {
taskCenter.send(
'dom',
{ action: 'removeEvent' },
[this.ref, type]
)
}
}
}
/**
* Fire an event manually.
* @param {string} type type
* @param {function} event handler
* @param {boolean} isBubble whether or not event bubble
* @param {boolean} options
* @return {} anything returned by handler function
*/
fireEvent (type, event, isBubble, options) {
let result = null
let isStopPropagation = false
const eventDesc = this.event[type]
if (eventDesc && event) {
const handler = eventDesc.handler
event.stopPropagation = () => {
isStopPropagation = true
}
if (options && options.params) {
result = handler.call(this, ...options.params, event)
}
else {
result = handler.call(this, event)
}
}
if (!isStopPropagation
&& isBubble
&& (BUBBLE_EVENTS.indexOf(type) !== -1)
&& this.parentNode
&& this.parentNode.fireEvent) {
event.currentTarget = this.parentNode
this.parentNode.fireEvent(type, event, isBubble) // no options
}
return result
}
/**
* Get all styles of current element.
* @return {object} style
*/
toStyle () {
return Object.assign({}, this.classStyle, this.style)
}
/**
* Convert current element to JSON like object.
* @return {object} element
*/
toJSON () {
const result = {
ref: this.ref.toString(),
type: this.type,
attr: this.attr,
style: this.toStyle()
}
const event = []
for (const type in this.event) {
const { params } = this.event[type]
if (!params) {
event.push(type)
}
else {
event.push({ type, params })
}
}
if (event.length) {
result.event = event
}
if (this.pureChildren.length) {
result.children = this.pureChildren.map((child) => child.toJSON())
}
return result
}
/**
* Convert to HTML element tag string.
* @return {stirng} html
*/
toString () {
return '<' + this.type +
' attr=' + JSON.stringify(this.attr) +
' style=' + JSON.stringify(this.toStyle()) + '>' +
this.pureChildren.map((child) => child.toString()).join('') +
'</' + this.type + '>'
}
}
setElement(Element)