RubyLouvre/anu

View on GitHub
packages/render/dom/event.js

Summary

Maintainability
D
2 days
Test Coverage
import { document, modern, contains } from './browser';
import { isFn, noop, toLowerCase } from 'react-core/util';
import { Renderer } from 'react-core/createRenderer';
import { enqueueDuplex } from './duplex';
export let rform = /textarea|input|select|option/i;
let globalEvents = {};
export let eventPropHooks = {}; //用于在事件回调里对事件对象进行
export let eventHooks = {}; //用于在元素上绑定特定的事件
//根据onXXX得到其全小写的事件名, onClick --> click, onClickCapture --> click,
// onMouseMove --> mousemove
let eventLowerCache = {
    onClick: 'click',
    onChange: 'change',
    onWheel: 'wheel'
};

export function eventAction(dom, name, val, lastProps, fiber) {
    let events = dom.__events || (dom.__events = {});
    events.vnode = fiber;
    let refName = toLowerCase(name.slice(2));
    if (val === false) {
        delete events[refName];
    } else {
        if (!lastProps[name]) {
            //添加全局监听事件
            let eventName = getBrowserName(name);
            let hook = eventHooks[eventName];
            if (hook) {
                hook(dom, eventName);
            }
            addGlobalEvent(eventName);
        }
        //onClick --> click, onClickCapture --> clickcapture
        events[refName] = val;
    }
}

let isTouch = 'ontouchstart' in document;

export function dispatchEvent(e, type, endpoint) {
    //__type__ 在injectTapEventPlugin里用到
    e = new SyntheticEvent(e);
    if (type) {
        e.type = type;
    }
    let bubble = e.type,
        terminal = endpoint || document,
        hook = eventPropHooks[e.type];
    if (hook && false === hook(e)) {
        return;
    }

    Renderer.batchedUpdates(function() {
        let paths = collectPaths(e.target, terminal, {});
        let captured = bubble + 'capture';
        triggerEventFlow(paths, captured, e);

        if (!e._stopPropagation) {
            triggerEventFlow(paths.reverse(), bubble, e);
        }
    }, e);
}

let nodeID = 1;
function collectPaths(begin, end, unique) {
    let paths = [];
    let node = begin;
    //先判定路径上有绑定事件没有
    while (node && node.nodeType == 1) {
        let checkChange = node;
        if (node.__events) {
            let vnode = node.__events.vnode;
            inner: while (vnode.return) {
                //ReactDOM.render有Unbatch, container,
                //ReactDOM.createPortal有AnuPortal
                if (vnode.tag === 5) {
                    node = vnode.stateNode;
                    if (node === end) {
                        return paths;
                    }
                    if (!node) {
                        break inner;
                    }
                    var uid = node.uniqueID || (node.uniqueID = ++nodeID);
                    if (node.__events && !unique[uid]) {
                        unique[uid] = 1;
                        paths.push({ node, events: node.__events });
                    }
                }
                vnode = vnode.return;
            }
        }
        if (node === checkChange) {
            node = node.parentNode;
        }
    }
    return paths;
}

function triggerEventFlow(paths, prop, e) {
    for (let i = paths.length; i--; ) {
        let path = paths[i];
        let fn = path.events[prop];
        if (isFn(fn)) {
            e.currentTarget = path.node;
            fn.call(void 666, e);
            if (e._stopPropagation) {
                break;
            }
        }
    }
}

export function addGlobalEvent(name, capture) {
    if (!globalEvents[name]) {
        globalEvents[name] = true;
        addEvent(document, name, dispatchEvent, capture);
    }
}

export function addEvent(el, type, fn, bool) {
    if (el.addEventListener) {
        el.addEventListener(type, fn, bool || false);
    } else if (el.attachEvent) {
        el.attachEvent('on' + type, fn);
    }
}

let rcapture = /Capture$/;
export function getBrowserName(onStr) {
    let lower = eventLowerCache[onStr];
    if (lower) {
        return lower;
    }
    let camel = onStr.slice(2).replace(rcapture, '');
    lower = camel.toLowerCase();
    eventLowerCache[onStr] = lower;
    return lower;
}

/**
DOM通过event对象的relatedTarget属性提供了相关元素的信息。这个属性只对于mouseover和mouseout事件才包含值;
对于其他事件,这个属性的值是null。IE不支持realtedTarget属性,但提供了保存着同样信息的不同属性。
在mouseover事件触发时,IE的fromElement属性中保存了相关元素;
在mouseout事件出发时,IE的toElement属性中保存着相关元素。
但fromElement与toElement可能同时都有值
 */
function getRelatedTarget(e) {
    if (!e.timeStamp) {
        e.relatedTarget = e.type === 'mouseover' ? e.fromElement : e.toElement;
    }
    return e.relatedTarget;
}
function getTarget(e) {
    return e.target || e.srcElement;
}
String('load,error').replace(/\w+/g, function(name) {
    eventHooks[name] = function(dom, type) {
        let mark = '__' + type;

        if (!dom[mark]) {
            dom[mark] = true;
            addEvent(dom, type, dispatchEvent);
        }
    };
});

String('mouseenter,mouseleave').replace(/\w+/g, function(name) {
    eventHooks[name] = function(dom, type) {
        let mark = '__' + type;
        if (!dom[mark]) {
            dom[mark] = true;
            let mask = type === 'mouseenter' ? 'mouseover' : 'mouseout';
            addEvent(dom, mask, function(e) {
                let t = getRelatedTarget(e);
                if (!t || (t !== dom && !contains(dom, t))) {
                    let common = getLowestCommonAncestor(dom, t);
                    //由于不冒泡,因此paths长度为1
                    dispatchEvent(e, type, common);
                }
            });
        }
    };
});

let specialHandles = {};
export function createHandle(name, fn) {
    return (specialHandles[name] = function(e) {
        if (fn && fn(e) === false) {
            return;
        }
        dispatchEvent(e, name);
    });
}
function onCompositionStart(e) {
    e.target.__onComposition = true;
}

function onCompositionEnd(e) {
    e.target.__onComposition = false;
    //dispatchEvent(e, "change");
}
const input2change = /text|password|search|url|email/i;
//react中,text,textarea,password元素的change事件实质上是input事件
//https://segmentfault.com/a/1190000008023476
if (!document['__input']) {
    globalEvents.input = document['__input'] = true;
    addEvent(document, 'compositionstart', onCompositionStart);
    addEvent(document, 'compositionend', onCompositionEnd);
    addEvent(document, 'input', function(e) {
        var dom = getTarget(e);
        if (input2change.test(dom.type)) {
            if (!dom.__onComposition) {
                dispatchEvent(e, 'change');
            }
        }
        dispatchEvent(e);
    });
}

function getLowestCommonAncestor(instA, instB) {
    let depthA = 0;
    for (let tempA = instA; tempA; tempA = tempA.parentNode) {
        depthA++;
    }
    let depthB = 0;
    for (let tempB = instB; tempB; tempB = tempB.parentNode) {
        depthB++;
    }

    // If A is deeper, crawl up.
    while (depthA - depthB > 0) {
        instA = instA.parentNode;
        depthA--;
    }

    // If B is deeper, crawl up.
    while (depthB - depthA > 0) {
        instB = instB.parentNode;
        depthB--;
    }

    // Walk in lockstep until we find a match.
    let depth = depthA;
    while (depth--) {
        if (instA === instB) {
            return instA;
        }
        instA = instA.parentNode;
        instB = instB.parentNode;
    }
    return null;
}

eventPropHooks.change = function(e) {
    enqueueDuplex(e.target);
};

createHandle('doubleclick');
createHandle('scroll');
createHandle('wheel');
globalEvents.wheel = true;
globalEvents.scroll = true;
globalEvents.doubleclick = true;

if (isTouch) {
    eventHooks.click = eventHooks.clickcapture = function(dom) {
        dom.onclick = dom.onclick || noop;
    };
}

eventPropHooks.click = function(e) {
    return !e.target.disabled;
};

const fixWheelType =
    document.onwheel !== void 666
        ? 'wheel'
        : 'onmousewheel' in document
            ? 'mousewheel'
            : 'DOMMouseScroll';
eventHooks.wheel = function(dom) {
    addEvent(dom, fixWheelType, specialHandles.wheel);
};

eventPropHooks.wheel = function(event) {
    event.deltaX =
        'deltaX' in event
            ? event.deltaX
            : // Fallback to `wheelDeltaX` for Webkit and normalize (right is positive).
            'wheelDeltaX' in event
                ? -event.wheelDeltaX
                : 0;
    event.deltaY =
        'deltaY' in event
            ? event.deltaY
            : // Fallback to `wheelDeltaY` for Webkit and normalize (down is positive).
            'wheelDeltaY' in event
                ? -event.wheelDeltaY
                : // Fallback to `wheelDelta` for IE<9 and normalize (down is positive).
                'wheelDelta' in event
                    ? -event.wheelDelta
                    : 0;
};

export let focusMap = {
    focus: 'focus',
    blur: 'blur'
};
let innerFocus;
function blurFocus(e) {
    let dom = getTarget(e);
    let type = focusMap[e.type];
    if (Renderer.inserting) {
        if (type === 'blur') {
            innerFocus = true;
            Renderer.inserting.focus();
            return;
        }
        //return放这里会导致浮层无法关闭
    }
    if (innerFocus) {
        innerFocus = false;
        return;
    }
    do {
        if (dom.nodeType === 1) {
            if (dom.__events && dom.__events[type]) {
                dispatchEvent(e, type);
                break;
            }
        } else {
            break;
        }
    } while ((dom = dom.parentNode));
}

'blur,focus'.replace(/\w+/g, function(type) {
    globalEvents[type] = true;
    if (modern) {
        let mark = '__' + type;
        if (!document[mark]) {
            document[mark] = true;
            addEvent(document, type, blurFocus, true);
        }
    } else {
        eventHooks[type] = function(dom, name) {
            addEvent(dom, focusMap[name], blurFocus);
        };
    }
});

eventHooks.scroll = function(dom, name) {
    addEvent(dom, name, specialHandles[name]);
};

eventHooks.doubleclick = function(dom, name) {
    addEvent(document, 'dblclick', specialHandles[name]);
};

export function SyntheticEvent(event) {
    if (event.nativeEvent) {
        return event;
    }
    for (let i in event) {
        if (!eventProto[i]) {
            this[i] = event[i];
        }
    }
    if (!this.target) {
        this.target = event.srcElement;
    }
    this.fixEvent();
    this.timeStamp = new Date() - 0;
    this.nativeEvent = event;
}

let eventProto = (SyntheticEvent.prototype = {
    fixEvent: noop, //留给以后扩展用
    fixHooks: noop,
    persist: noop,
    preventDefault: function() {
        let e = this.nativeEvent || {};
        e.returnValue = this.returnValue = false;
        if (e.preventDefault) {
            e.preventDefault();
            this.defaultPrevented = true;
        }
    },
    stopPropagation: function() {
        let e = this.nativeEvent || {};
        e.cancelBubble = this._stopPropagation = true;
        if (e.stopPropagation) {
            e.stopPropagation();
        }
    },
    stopImmediatePropagation: function() {
        this.stopPropagation();
        this.stopImmediate = true;
    },
    toString: function() {
        return '[object Event]';
    }
});

Renderer.eventSystem = {
    eventPropHooks,
    addEvent,
    dispatchEvent,
    SyntheticEvent
};