import { defaultValue } from '../util'
import * as Util from '../util/EventUtil'
import { ProxyEvent } from './event/index'
/**
* 事件基类,所有可接收或者发送的对象继承于此
* @class Evented
* @moduleEX BaseModule
* @example
* * ```js
* map.on('click', function(e) {
* alert(e.latlng);
* } );
* ```
* Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function:
*
* ```js
* function onClick(e) { ... }
*
* map.on('click', onClick);
* map.off('click', onClick);
* ```
*/
export default class Evented {
constructor() {
return this._createEventedProxy()
}
/**
* @private
* @description: 创建代理对象
* @return {*}
*/
_createEventedProxy() {
this._decorateParents = []
this._decorateProperties = {}
const that = this
return new Proxy(this, {
set(target, key, value, receiver) {
if (that._decorateProperties[key]) {
const decorateProperty = that._decorateProperties[key]
const { type, createInstance } = decorateProperty
// 如果类型不一致,重新初始化构建对象
if (!type || !(value instanceof type)) {
value = createInstance(value)
}
// 如果属于事件基类且没有事件父级,设置事件父级
if (
value instanceof Evented &&
Array.isArray(value._decorateParents) &&
value._decorateParents.findIndex((v) => v.instance === that) === -1
) {
value._decorateParents.push({
instance: that,
key
})
}
}
// 处理设置值重复情况,不触发事件
if (target[key] === value) {
return true
}
// set eventData响应式刷新内容
const eventData = {
target,
key,
value,
oldValue: Reflect.get(target, key),
receiver
}
const result = Reflect.set(target, key, value, receiver)
// 私有变量不进行响应式更新
if (/^_[^_].*/.test(key)) {
return result
}
that.decorateEventFire('set', eventData)
return result // 返回设置结果
},
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
return result
}
})
}
decorateEventFire(eventName, eventData) {
this._decorateEventFire(this, this, [], eventName, eventData, eventData)
}
/**
* @private
* @description: 发送响应式事件
* @param {*} target 发送源
* @param {*} sourceTarget 触发源
* @param {*} firePath 事件路径
* @param {*} eventName 事件名称
* @param {*} eventData 事件主体
* @param {*} sourceEventData 源事件主体
* @return {*}
*/
_decorateEventFire(
target,
sourceTarget,
firePath,
eventName,
eventData,
sourceEventData
) {
const { key, value, oldValue } = eventData
const eventPath = firePath.concat()
eventPath.unshift(key)
const currentEventData = {
key,
value,
oldValue
}
// 响应式发送事件
const proxyEvent = new ProxyEvent({
eventName,
target,
eventData: currentEventData,
eventPath: eventPath.concat(),
sourceTarget,
sourceEventData
})
target.fire(eventName, proxyEvent)
// 向父节点传递
const parents = target._decorateParents || []
parents.forEach((detail) => {
const { instance, key } = detail
let currentKey = key
if (instance && instance.type === 'collection') {
currentKey = `${target.id}`
}
const parentEventData = {
key: currentKey,
value: instance[currentKey] || target,
oldValue: instance[currentKey] || target
}
this._decorateEventFire(
instance,
sourceTarget,
eventPath,
eventName,
parentEventData,
sourceEventData
)
})
}
/**
* @private
* @description: 响应式属性传递方法定义
* @param {String} key
* @param {any} value
* @param {Object} type
* @param {Function} createInstance
* @return {Object} 实体
*/
_decorate(key, value, type, createInstance) {
createInstance = defaultValue(createInstance, (v) => v)
let instance = value
if (!type || !(value instanceof type)) {
instance = createInstance(defaultValue(value, {}))
}
this._decorateProperties[key] = {
type,
createInstance
}
return instance
}
/**
* @function Evented.prototype.on
* @description 注册一个新的监听事件;<br/>
* 示例如下:<br/>
* <a href='#event1'>[1、注册一个事件]</a><br/>
* <a href='#event2'>[2、一次注册多个事件 - 同一个回调函数]</a><br/>
* <a href='#event3'>[3、一次注册多个事件 - 分别指回调应函数]</a><br/>
* <a href='#event4'>[4、当types为字符串时 - 指定上下文]</a><br/>
* <a href='#event5'>[5、当types为对象时 - 指定上下文]</a><br/>
* @param {String | Object} [types = null] 事件类型<br/>
* 当types为字符串时,可以定义单个或多个事件,单个事件:"click",多个事件:以空格分割:"click double-click";<br/>
* 当types为对象时,使用如下方式指定事件:{'click': onClickFun, 'mouse-move': onMouseMoveFun}
* @param {Function} [fn = null] 事件回调函数
* @param {Object} [context = null] 事件回调函数的this关键字将指向的对象
* @return {Object} 当前实例
*
* @example <caption><h5 id='event1'>注册一个事件</h5></caption>
* // 初始化一个点击事件回调函数
* const clickFunction = function (event) {
* console.log("点击事件:", event)
* }
* // 调用MapView或SceneView的on方法注册一个点击事件
* view.on('click', clickFunction)
*
* @example <caption><h5 id='event2'>一次注册多个事件 - 同一个回调函数</h5></caption>
* // 初始化一个事件回调函数
* const eventFunction = function (event) {
* console.log("事件:", event)
* }
*
* // 调用MapView或SceneView的on方法注册多个事件
* // 多个事件类型使用同一个回调函数
* view.on('click right-click-down', eventFunction)
*
* @example <caption><h5 id='event3'>一次注册多个事件 - 分别指回调应函数</h5></caption>
* // 初始化一个左键点击事件回调函数
* const clickFunction = function (event) {
* console.log("click事件:", event)
* }
*
* // 初始化一个右键按下事件回调函数
* const rightClickFunction = function (event) {
* console.log("right-click-down事件:", event)
* }
*
* // 调用MapView或SceneView的on方法注册多个事件
* // 每一个事件类型,使用单独的回调函数
* // 注意使用此种方式,一种类型的事件仅能指定一个回调函数
* view.on({
* "click": clickFunction,
* "right-click-down": rightClickFunction
* })
*
* @example <caption><h5 id='event4'>指定上下文 - types类型为字符串</h5></caption>
* // 初始化一个点击事件回调函数
* const clickFunction = function (event) {
* console.log("点击事件:", event)
* console.log("上下文对象:", this)
* }
* // 调用MapView或SceneView的on方法注册一个点击事件
* // 指定view为回调函数的上下文对象
* view.on('click', clickFunction, view)
*
* @example <caption><h5 id='event5'>指定上下文 - types类型为对象</h5></caption>
* // 初始化一个点击事件回调函数
* const clickFunction = function (event) {
* console.log("点击事件:", event)
* console.log("上下文对象:", this)
* }
* // 调用MapView或SceneView的on方法注册一个点击事件
* // 指定view为回调函数的上下文对象
* view.on({
* "click": clickFunction,
* "right-click-down": clickFunction
* }, view)
*/
on(types, fn, context) {
// types can be a map of types/handlers
if (typeof types === 'object') {
for (const type in types) {
// we don't process space-separated Evented here for performance;
// it's a hot path since Layer uses the on(obj) syntax
this._on(type, types[type], fn)
}
} else {
// types can be a string of space-separated words
types = Util.splitWords(types)
for (let i = 0, len = types.length; i < len; i++) {
this._on(types[i], fn, context)
}
}
return this
}
/**
* @function Evented.prototype.off
* @description 移除事件<br/>
* 示例如下:<br/>
* <a href='#off1'>[1、移除一个事件的指定回调函数]</a><br/>
* <a href='#off2'>[2、移除一个事件的所有回调函数]</a><br/>
* <a href='#off3'>[3、移除多个事件的同一个指定的回调函数]</a><br/>
* <a href='#off4'>[4、移除多个指定事件的回调函数]</a><br/>
* <a href='#off5'>[5、删除时指定上下文 - types类型为字符串]</a><br/>
* <a href='#off6'>[6、删除时指定上下文 - types类型为对象]</a><br/>
* @param {string} [types] 移除指定事件类型上绑定的回调函数<br/>
* 当类型为字符串时,可以移除单个或多个事件类型绑定的回调函数,单个事件:"click",多个事件:以空格分割:"click double-click";<br/>
* 当types为对象时,使用如下方式移除事件:{'click': onClickFun, 'mouse-move': onMouseMoveFun}
* @param {Function} [fn] 事件回调函数,当types为字符串,且不指定要删除的回调函数时,删除该事件上的所有回调函数
* @param {Object} [context] 事件回调函数的this关键字将指向的对象
* @return {Object} 当前实例
*
* @example <caption><h5 id='off1'>移除一个事件的指定回调函数</h5></caption>
* // 一个事件的回调函数
* const clickFunction = function (event) {
* console.log("点击事件:", event)
* }
* // 调用MapView或SceneView的off方法移除一个事件的回调函数
* view.off('click', clickFunction)
*
* @example <caption><h5 id='off2'>移除一个事件的所有回调函数</h5></caption>
* // 一个事件的回调函数1
* const clickFunction1 = function (event) {
* console.log("点击事件1:", event)
* }
*
* // 一个事件的回调函数2
* const clickFunction2 = function (event) {
* console.log("点击事件2:", event)
* }
*
* // 调用MapView或SceneView的off方法移除一个事件的所有回调函数
* // 不指定回调函数,则移除该事件上的所有绑定的回调函数
* view.off('click')
*
* @example <caption><h5 id='off3'>移除多个事件的同一个指定的回调函数</h5></caption>
* // 多个事件的同一个回调函数
* const eventFunction = function (event) {
* console.log("事件:", event)
* }
* // 调用MapView或SceneView的off方法移除多个事件的同一个指定的回调函数
* view.off('click double-click', eventFunction)
*
* @example <caption><h5 id='off4'>移除多个指定事件的回调函数</h5></caption>
* // 一个事件的回调函数
* const clickFunction = function (event) {
* console.log("click事件:", event)
* }
* // 调用MapView或SceneView的off方法移除多个指定事件的回调函数
* view.off({
* // 移除click事件上一个指定的函数
* "click": clickFunction,
* // 移除double-click上所有指定的函数
* "double-click": undefined
* })
*
* @example <caption><h5 id='off5'>删除时指定上下文 - types类型为字符串</h5></caption>
* // 一个事件的回调函数
* const clickFunction = function (event) {
* console.log("点击事件:", event)
* }
* // 调用MapView或SceneView的off方法移除一个事件的回调函数
* view.off('click', clickFunction, view)
* // 调用MapView或SceneView的off方法移除一个事件的所有回调函数
* view.off('click', undefined, view)
*
* @example <caption><h5 id='off6'>删除时指定上下文 - types类型为对象</h5></caption>
* // 一个事件的回调函数
* const clickFunction = function (event) {
* console.log("click事件:", event)
* }
* // 调用MapView或SceneView的off方法移除多个指定事件的回调函数
* view.off({
* // 移除click事件上一个指定的函数
* "click": clickFunction,
* // 移除double-click上所有指定的函数
* "double-click": undefined
* }, view)
*/
off(types, fn, context) {
if (!types) {
// clear all listeners if called without arguments
delete this._Evented
} else if (typeof types === 'object') {
for (const type in types) {
this._off(type, types[type], fn)
}
} else {
types = Util.splitWords(types)
for (let i = 0, len = types.length; i < len; i++) {
this._off(types[i], fn, context)
}
}
return this
}
// attach listener (without syntactic sugar now)
_on(type, fn, context) {
this._Evented = this._Evented || {}
/* get/init listeners for type */
let typeListeners = this._Evented[type]
if (!typeListeners) {
typeListeners = []
this._Evented[type] = typeListeners
}
if (context === this) {
// Less memory footprint.
context = undefined
}
const newListener = { fn, ctx: context }
const listeners = typeListeners
// check if fn already there
for (let i = 0, len = listeners.length; i < len; i++) {
if (listeners[i].fn === fn && listeners[i].ctx === context) {
return
}
}
listeners.push(newListener)
}
_off(type, fn, context) {
let listeners
let i
let len
if (!this._Evented) {
return
}
listeners = this._Evented[type]
if (!listeners) {
return
}
if (!fn) {
// Set all removed listeners to noop so they are not called if remove happens in fire
for (i = 0, len = listeners.length; i < len; i++) {
listeners[i].fn = Util.falseFn
}
// clear all listeners for a type if function isn't specified
delete this._Evented[type]
return
}
if (context === this) {
context = undefined
}
if (listeners) {
// find fn and remove it
for (i = 0, len = listeners.length; i < len; i++) {
const l = listeners[i]
if (l.ctx !== context) {
// eslint-disable-next-line no-continue
continue
}
if (l.fn === fn) {
// set the removed listener to noop so that's not called if remove happens in fire
l.fn = Util.falseFn
if (this._firingCount) {
/* copy array in case Evented are being fired */
// eslint-disable-next-line no-multi-assign
this._Evented[type] = listeners = listeners.slice()
}
listeners.splice(i, 1)
return
}
}
}
}
/**
* @function Evented.prototype.fire
* @private
* @description: 激发指定类型的事件
* @param {string} [type] 事件类型
* @param {Object} [data] 待传递的数据
* @param {Boolean} [propagate] 是否传播到父级
* @return {Object} 当前实例
*/
fire(type, data, propagate) {
if (!this.listens(type, propagate)) {
return this
}
const event = data
if (event) {
event.type = type
event.target = this
event.sourceTarget = (data && data.sourceTarget) || this
}
if (this._Evented) {
const listeners = this._Evented[type]
if (listeners) {
this._firingCount = this._firingCount + 1 || 1
for (let i = 0, len = listeners.length; i < len; i++) {
const l = listeners[i]
l.fn.call(l.ctx || this, event)
}
this._firingCount--
}
}
if (propagate) {
// propagate the event to parents (set with addEventParent)
this._propagateEvent(event)
}
return this
}
/**
* @function Evented.prototype.listens
* @private
* @description: 验证是否监听此事件
* @param {string} [type] 事件类型
* @param {Boolean} [propagate] 验证父级监听此事件
* @return {Boolean} 当前实例
*/
listens(type, propagate) {
const listeners = this._Evented && this._Evented[type]
if (listeners && listeners.length) {
return true
}
if (propagate) {
// also check parents for listeners if event propagates
for (const id in this._eventParents) {
if (this._eventParents[id].listens(type, propagate)) {
return true
}
}
}
return false
}
/**
* @function Evented.prototype.once
* @private
* @description: 事件监听,仅触发一次
* @param {string} [types] 将侦听器函数(fn)添加到对象的特定事件类型中。您还可以传递几个空格分隔的类型(例如“click dblclick”),也可以传入例如 {click: onClick, mousemove: onMouseMove}。
* @param {Function} [fn] 侦听器函数
* @param {Object} [context] 指定侦听器的上下文(this关键字将指向的对象)
* @return {Object} 当前实例
*/
once(types, fn, context) {
const self = this
if (typeof types === 'object') {
for (const type in types) {
this.once(type, types[type], fn)
}
return this
}
const handler = Util.bind(function () {
self.off(types, fn, context).off(types, handler, context)
}, this)
// add a listener that's executed once and removed after that
return this.on(types, fn, context).on(types, handler, context)
}
/**
* @private
* @function Evented.prototype.addEventParent
* @description: 添加事件父级-将接收传播事件的Evented
* @param {Evented} [obj] 事件父级对象
* @return {Object} 当前实例
*/
addEventParent(obj) {
this._eventParents = this._eventParents || {}
this._eventParents[Util.stamp(obj)] = obj
return this
}
/**
* @function Evented.prototype.removeEventParent
* @private
* @description: 移除事件父级-将接收传播事件的Evented
* @param {Evented} [obj] 事件父级对象
* @return {Object} 当前实例
*/
removeEventParent(obj) {
if (this._eventParents) {
delete this._eventParents[Util.stamp(obj)]
}
return this
}
_propagateEvent(e) {
for (const id in this._eventParents) {
this._eventParents[id].fire(
e.type,
Util.extend(
{
layer: e.target,
propagatedFrom: e.target
},
e
),
true
)
}
}
}
// aliases; we should ditch those eventually
// @method addEventListener(…): this
// Alias to [`on(…)`](#evented-on)
Evented.addEventListener = Evented.on
// @method removeEventListener(…): this
// Alias to [`off(…)`](#evented-off)
// @method clearAllEventListeners(…): this
// Alias to [`off()`](#evented-off)
// eslint-disable-next-line no-multi-assign
Evented.removeEventListener = Evented.clearAllEventListeners = Evented.off
// @method addOneTimeEventListener(…): this
// Alias to [`once(…)`](#evented-once)
Evented.addOneTimeEventListener = Evented.once
// @method fireEvent(…): this
// Alias to [`fire(…)`](#evented-fire)
Evented.fireEvent = Evented.fire
// @method hasEventListeners(…): Boolean
// Alias to [`listens(…)`](#evented-listens)
Evented.hasEventListeners = Evented.listens