diff --git a/packages/devextreme/js/__internal/events/core/m_emitter.feedback.ts b/packages/devextreme/js/__internal/events/core/m_emitter.feedback.ts new file mode 100644 index 000000000000..bf19246fa872 --- /dev/null +++ b/packages/devextreme/js/__internal/events/core/m_emitter.feedback.ts @@ -0,0 +1,181 @@ +import Class from '@js/core/class'; +import devices from '@js/core/devices'; +import { ensureDefined, noop } from '@js/core/utils/common'; +import { contains } from '@js/core/utils/dom'; +import Emitter from '@js/events/core/emitter'; +import registerEmitter from '@js/events/core/emitter_registrator'; +import pointerEvents from '@js/events/pointer'; +import { isMouseEvent } from '@js/events/utils/index'; + +const ACTIVE_EVENT_NAME = 'dxactive'; +const INACTIVE_EVENT_NAME = 'dxinactive'; + +const ACTIVE_TIMEOUT = 30; +const INACTIVE_TIMEOUT = 400; + +const FeedbackEvent = Class.inherit({ + + ctor(timeout, fire) { + this._timeout = timeout; + this._fire = fire; + }, + + start() { + const that = this; + + this._schedule(() => { + that.force(); + }); + }, + + _schedule(fn) { + this.stop(); + this._timer = setTimeout(fn, this._timeout); + }, + + stop() { + clearTimeout(this._timer); + }, + + force() { + if (this._fired) { + return; + } + + this.stop(); + this._fire(); + this._fired = true; + }, + + fired() { + return this._fired; + }, + +}); + +let activeFeedback; + +const FeedbackEmitter = Emitter.inherit({ + + ctor() { + this.callBase.apply(this, arguments); + + this._active = new FeedbackEvent(0, noop); + this._inactive = new FeedbackEvent(0, noop); + }, + + /* eslint-disable default-case */ + configure(data, eventName) { + switch (eventName) { + case ACTIVE_EVENT_NAME: + data.activeTimeout = data.timeout; + break; + case INACTIVE_EVENT_NAME: + data.inactiveTimeout = data.timeout; + break; + } + + this.callBase(data); + }, + + start(e) { + if (activeFeedback) { + const activeChildExists = contains(this.getElement().get(0), activeFeedback.getElement().get(0)); + const childJustActivated = !activeFeedback._active.fired(); + + if (activeChildExists && childJustActivated) { + this._cancel(); + return; + } + + activeFeedback._inactive.force(); + } + activeFeedback = this; + + this._initEvents(e); + this._active.start(); + }, + + _initEvents(e) { + const that = this; + + const eventTarget = this._getEmitterTarget(e); + + const mouseEvent = isMouseEvent(e); + const isSimulator = devices.isSimulator(); + const deferFeedback = isSimulator || !mouseEvent; + + const activeTimeout = ensureDefined(this.activeTimeout, ACTIVE_TIMEOUT); + const inactiveTimeout = ensureDefined(this.inactiveTimeout, INACTIVE_TIMEOUT); + + this._active = new FeedbackEvent(deferFeedback ? activeTimeout : 0, () => { + that._fireEvent(ACTIVE_EVENT_NAME, e, { target: eventTarget }); + }); + this._inactive = new FeedbackEvent(deferFeedback ? inactiveTimeout : 0, () => { + that._fireEvent(INACTIVE_EVENT_NAME, e, { target: eventTarget }); + activeFeedback = null; + }); + }, + + cancel(e) { + this.end(e); + }, + + end(e) { + const skipTimers = e.type !== pointerEvents.up; + + if (skipTimers) { + this._active.stop(); + } else { + this._active.force(); + } + + this._inactive.start(); + + if (skipTimers) { + this._inactive.force(); + } + }, + + dispose() { + this._active.stop(); + this._inactive.stop(); + + if (activeFeedback === this) { + activeFeedback = null; + } + + this.callBase(); + }, + + lockInactive() { + this._active.force(); + this._inactive.stop(); + activeFeedback = null; + this._cancel(); + + return this._inactive.force.bind(this._inactive); + }, + +}); +// @ts-expect-error +FeedbackEmitter.lock = function (deferred) { + const lockInactive = activeFeedback ? activeFeedback.lockInactive() : noop; + + deferred.done(lockInactive); +}; + +registerEmitter({ + emitter: FeedbackEmitter, + events: [ + ACTIVE_EVENT_NAME, + INACTIVE_EVENT_NAME, + ], +}); + +// @ts-expect-error +export const { lock } = FeedbackEmitter; +export { + ACTIVE_EVENT_NAME as active, + INACTIVE_EVENT_NAME as inactive, +}; diff --git a/packages/devextreme/js/__internal/events/core/m_emitter.ts b/packages/devextreme/js/__internal/events/core/m_emitter.ts new file mode 100644 index 000000000000..65d31a3b7aa6 --- /dev/null +++ b/packages/devextreme/js/__internal/events/core/m_emitter.ts @@ -0,0 +1,106 @@ +import Class from '@js/core/class'; +import $ from '@js/core/renderer'; +import Callbacks from '@js/core/utils/callbacks'; +import { noop } from '@js/core/utils/common'; +import { extend } from '@js/core/utils/extend'; +import { fireEvent, hasTouches, isDxMouseWheelEvent } from '@js/events/utils/index'; + +const Emitter = Class.inherit({ + + ctor(element) { + this._$element = $(element); + + this._cancelCallback = Callbacks(); + this._acceptCallback = Callbacks(); + }, + + getElement() { + return this._$element; + }, + + validate(e) { + return !isDxMouseWheelEvent(e); + }, + + validatePointers(e) { + return hasTouches(e) === 1; + }, + + allowInterruptionByMouseWheel() { + return true; + }, + + configure(data) { + extend(this, data); + }, + + addCancelCallback(callback) { + this._cancelCallback.add(callback); + }, + + removeCancelCallback() { + this._cancelCallback.empty(); + }, + + _cancel(e) { + this._cancelCallback.fire(this, e); + }, + + addAcceptCallback(callback) { + this._acceptCallback.add(callback); + }, + + removeAcceptCallback() { + this._acceptCallback.empty(); + }, + + _accept(e) { + this._acceptCallback.fire(this, e); + }, + + _requestAccept(e) { + this._acceptRequestEvent = e; + }, + + _forgetAccept() { + this._accept(this._acceptRequestEvent); + this._acceptRequestEvent = null; + }, + + start: noop, + move: noop, + end: noop, + + cancel: noop, + reset() { + if (this._acceptRequestEvent) { + this._accept(this._acceptRequestEvent); + } + }, + + _fireEvent(eventName, e, params) { + const eventData = extend({ + type: eventName, + originalEvent: e, + target: this._getEmitterTarget(e), + delegateTarget: this.getElement().get(0), + }, params); + + e = fireEvent(eventData); + + if (e.cancel) { + this._cancel(e); + } + + return e; + }, + + _getEmitterTarget(e) { + return (this.delegateSelector ? $(e.target).closest(this.delegateSelector) : this.getElement()).get(0); + }, + + dispose: noop, + +}); + +export default Emitter; diff --git a/packages/devextreme/js/__internal/events/core/m_emitter_registrator.ts b/packages/devextreme/js/__internal/events/core/m_emitter_registrator.ts new file mode 100644 index 000000000000..77540f77ce3c --- /dev/null +++ b/packages/devextreme/js/__internal/events/core/m_emitter_registrator.ts @@ -0,0 +1,303 @@ +import Class from '@js/core/class'; +import domAdapter from '@js/core/dom_adapter'; +import { data as elementData } from '@js/core/element_data'; +import $ from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import readyCallbacks from '@js/core/utils/ready_callbacks'; +import registerEvent from '@js/events/core/event_registrator'; +import eventsEngine from '@js/events/core/events_engine'; +import { name as wheelEventName } from '@js/events/core/wheel'; +import pointerEvents from '@js/events/pointer'; +import { addNamespace, isMouseEvent } from '@js/events/utils/index'; + +const MANAGER_EVENT = 'dxEventManager'; +const EMITTER_DATA = 'dxEmitter'; + +const EventManager = Class.inherit({ + + ctor() { + this._attachHandlers(); + this.reset(); + + this._proxiedCancelHandler = this._cancelHandler.bind(this); + this._proxiedAcceptHandler = this._acceptHandler.bind(this); + }, + + _attachHandlers() { + readyCallbacks.add(() => { + const document = domAdapter.getDocument(); + // @ts-expect-error + eventsEngine.subscribeGlobal(document, addNamespace(pointerEvents.down, MANAGER_EVENT), this._pointerDownHandler.bind(this)); + // @ts-expect-error + eventsEngine.subscribeGlobal(document, addNamespace(pointerEvents.move, MANAGER_EVENT), this._pointerMoveHandler.bind(this)); + // @ts-expect-error + eventsEngine.subscribeGlobal(document, addNamespace([pointerEvents.up, pointerEvents.cancel].join(' '), MANAGER_EVENT), this._pointerUpHandler.bind(this)); + // @ts-expect-error + eventsEngine.subscribeGlobal(document, addNamespace(wheelEventName, MANAGER_EVENT), this._mouseWheelHandler.bind(this)); + }); + }, + + _eachEmitter(callback) { + const activeEmitters = this._activeEmitters || []; + let i = 0; + + while (activeEmitters.length > i) { + const emitter = activeEmitters[i]; + if (callback(emitter) === false) { + break; + } + + if (activeEmitters[i] === emitter) { + i++; + } + } + }, + + _applyToEmitters(method, arg) { + this._eachEmitter((emitter) => { + emitter[method].call(emitter, arg); + }); + }, + + reset() { + this._eachEmitter(this._proxiedCancelHandler); + this._activeEmitters = []; + }, + + resetEmitter(emitter) { + this._proxiedCancelHandler(emitter); + }, + + _pointerDownHandler(e) { + if (isMouseEvent(e) && e.which > 1) { + return; + } + + this._updateEmitters(e); + }, + + _updateEmitters(e) { + if (!this._isSetChanged(e)) { + return; + } + + this._cleanEmitters(e); + this._fetchEmitters(e); + }, + + _isSetChanged(e) { + const currentSet = this._closestEmitter(e); + const previousSet = this._emittersSet || []; + + let setChanged = currentSet.length !== previousSet.length; + + each(currentSet, (index, emitter) => { + setChanged = setChanged || previousSet[index] !== emitter; + return !setChanged; + }); + + this._emittersSet = currentSet; + + return setChanged; + }, + + _closestEmitter(e) { + const that = this; + + const result: any = []; + let $element = $(e.target); + + function handleEmitter(_, emitter) { + if (!!emitter && emitter.validatePointers(e) && emitter.validate(e)) { + emitter.addCancelCallback(that._proxiedCancelHandler); + emitter.addAcceptCallback(that._proxiedAcceptHandler); + result.push(emitter); + } + } + + while ($element.length) { + const emitters = elementData($element.get(0), EMITTER_DATA) || []; + each(emitters, handleEmitter); + $element = $element.parent(); + } + + return result; + }, + + _acceptHandler(acceptedEmitter, e) { + this._eachEmitter((emitter) => { + if (emitter !== acceptedEmitter) { + this._cancelEmitter(emitter, e); + } + }); + }, + + _cancelHandler(canceledEmitter, e) { + this._cancelEmitter(canceledEmitter, e); + }, + + _cancelEmitter(emitter, e) { + const activeEmitters = this._activeEmitters; + + if (e) { + emitter.cancel(e); + } else { + emitter.reset(); + } + + emitter.removeCancelCallback(); + emitter.removeAcceptCallback(); + + const emitterIndex = activeEmitters.indexOf(emitter); + if (emitterIndex > -1) { + activeEmitters.splice(emitterIndex, 1); + } + }, + + _cleanEmitters(e) { + this._applyToEmitters('end', e); + this.reset(e); + }, + + _fetchEmitters(e) { + this._activeEmitters = this._emittersSet.slice(); + this._applyToEmitters('start', e); + }, + + _pointerMoveHandler(e) { + this._applyToEmitters('move', e); + }, + + _pointerUpHandler(e) { + this._updateEmitters(e); + }, + + _mouseWheelHandler(e) { + if (!this._allowInterruptionByMouseWheel()) { + return; + } + + e.pointers = [null]; + this._pointerDownHandler(e); + + this._adjustWheelEvent(e); + + this._pointerMoveHandler(e); + e.pointers = []; + this._pointerUpHandler(e); + }, + + _allowInterruptionByMouseWheel() { + let allowInterruption = true; + this._eachEmitter((emitter) => { + allowInterruption = emitter.allowInterruptionByMouseWheel() && allowInterruption; + return allowInterruption; + }); + return allowInterruption; + }, + + _adjustWheelEvent(e) { + let closestGestureEmitter: any = null; + // @ts-expect-error + this._eachEmitter((emitter) => { + if (!emitter.gesture) { + return; + } + + const direction = emitter.getDirection(e); + if (direction !== 'horizontal' && !e.shiftKey || direction !== 'vertical' && e.shiftKey) { + closestGestureEmitter = emitter; + return false; + } + }); + + if (!closestGestureEmitter) { + return; + } + + const direction = closestGestureEmitter.getDirection(e); + const verticalGestureDirection = direction === 'both' && !e.shiftKey || direction === 'vertical'; + const prop = verticalGestureDirection ? 'pageY' : 'pageX'; + + e[prop] += e.delta; + }, + + isActive(element) { + let result = false; + this._eachEmitter((emitter) => { + result = result || emitter.getElement().is(element); + }); + return result; + }, +}); + +const eventManager = new EventManager(); + +const EMITTER_SUBSCRIPTION_DATA = 'dxEmitterSubscription'; + +const registerEmitter = function (emitterConfig) { + const EmitterClass = emitterConfig.emitter; + const emitterName = emitterConfig.events[0]; + const emitterEvents = emitterConfig.events; + + each(emitterEvents, (_, eventName) => { + registerEvent(eventName, { + + noBubble: !emitterConfig.bubble, + + setup(element) { + const subscriptions = elementData(element, EMITTER_SUBSCRIPTION_DATA) || {}; + + const emitters = elementData(element, EMITTER_DATA) || {}; + const emitter = emitters[emitterName] || new EmitterClass(element); + + subscriptions[eventName] = true; + emitters[emitterName] = emitter; + + elementData(element, EMITTER_DATA, emitters); + elementData(element, EMITTER_SUBSCRIPTION_DATA, subscriptions); + }, + + add(element, handleObj) { + const emitters = elementData(element, EMITTER_DATA); + const emitter = emitters[emitterName]; + + emitter.configure(extend({ + delegateSelector: handleObj.selector, + }, handleObj.data), handleObj.type); + }, + + teardown(element) { + const subscriptions = elementData(element, EMITTER_SUBSCRIPTION_DATA); + + const emitters = elementData(element, EMITTER_DATA); + const emitter = emitters[emitterName]; + + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete subscriptions[eventName]; + + let disposeEmitter = true; + each(emitterEvents, (_, eventName) => { + disposeEmitter = disposeEmitter && !subscriptions[eventName]; + return disposeEmitter; + }); + + if (disposeEmitter) { + // @ts-expect-error + if (eventManager.isActive(element)) { + // @ts-expect-error + eventManager.resetEmitter(emitter); + } + + emitter && emitter.dispose(); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete emitters[emitterName]; + } + }, + + }); + }); +}; + +export default registerEmitter; diff --git a/packages/devextreme/js/__internal/events/core/m_event_registrator.ts b/packages/devextreme/js/__internal/events/core/m_event_registrator.ts new file mode 100644 index 000000000000..1300b04f4577 --- /dev/null +++ b/packages/devextreme/js/__internal/events/core/m_event_registrator.ts @@ -0,0 +1,35 @@ +import { each } from '@js/core/utils/iterator'; +import callbacks from '@js/events/core/event_registrator_callbacks'; + +const registerEvent = function (name, eventObject) { + const strategy: any = {}; + + if ('noBubble' in eventObject) { + strategy.noBubble = eventObject.noBubble; + } + + if ('bindType' in eventObject) { + strategy.bindType = eventObject.bindType; + } + + if ('delegateType' in eventObject) { + strategy.delegateType = eventObject.delegateType; + } + + each(['setup', 'teardown', 'add', 'remove', 'trigger', 'handle', '_default', 'dispose'], (_, methodName) => { + if (!eventObject[methodName]) { + return; + } + + strategy[methodName] = function () { + const args: any = [].slice.call(arguments); + args.unshift(this); + return eventObject[methodName].apply(eventObject, args); + }; + }); + + callbacks.fire(name, strategy); +}; +registerEvent.callbacks = callbacks; + +export default registerEvent; diff --git a/packages/devextreme/js/__internal/events/core/m_events_engine.ts b/packages/devextreme/js/__internal/events/core/m_events_engine.ts new file mode 100644 index 000000000000..75b3058cd4db --- /dev/null +++ b/packages/devextreme/js/__internal/events/core/m_events_engine.ts @@ -0,0 +1,701 @@ +import domAdapter from '@js/core/dom_adapter'; +import errors from '@js/core/errors'; +import callOnce from '@js/core/utils/call_once'; +import Callbacks from '@js/core/utils/callbacks'; +import injector from '@js/core/utils/dependency_injector'; +import { extend } from '@js/core/utils/extend'; +import { + isFunction, isObject, isString, isWindow, +} from '@js/core/utils/type'; +import { getWindow, hasWindow } from '@js/core/utils/window'; +import registerEventCallbacks from '@js/events/core/event_registrator_callbacks'; +import hookTouchProps from '@js/events/core/hook_touch_props'; +import { getEventTarget } from '@js/events/utils/event_target'; + +const window = getWindow(); + +/* eslint-disable spellcheck/spell-checker */ +const EMPTY_EVENT_NAME = 'dxEmptyEventType'; +const NATIVE_EVENTS_TO_SUBSCRIBE = { + mouseenter: 'mouseover', + mouseleave: 'mouseout', + pointerenter: 'pointerover', + pointerleave: 'pointerout', +}; +const NATIVE_EVENTS_TO_TRIGGER = { + focusin: 'focus', + focusout: 'blur', +}; +const NO_BUBBLE_EVENTS = ['blur', 'focus', 'load']; + +const forcePassiveFalseEventNames = ['touchmove', 'wheel', 'mousewheel', 'touchstart']; + +const EVENT_PROPERTIES = [ + 'target', + 'relatedTarget', + 'delegateTarget', + 'altKey', + 'bubbles', + 'cancelable', + 'changedTouches', + 'ctrlKey', + 'detail', + 'eventPhase', + 'metaKey', + 'shiftKey', + 'view', + 'char', + 'code', + 'charCode', + 'key', + 'keyCode', + 'button', + 'buttons', + 'offsetX', + 'offsetY', + 'pointerId', + 'pointerType', + 'targetTouches', + 'toElement', + 'touches', +]; + +function matchesSafe(target, selector) { + // @ts-expect-error + return !isWindow(target) && target.nodeName !== '#document' && domAdapter.elementMatches(target, selector); +} +const elementDataMap = new WeakMap(); +let guid = 0; +let skipEvent; + +const special = (function () { + const specialData = {}; + + registerEventCallbacks.add((eventName, eventObject) => { + specialData[eventName] = eventObject; + }); + + return { + getField(eventName, field) { + return specialData[eventName] && specialData[eventName][field]; + }, + callMethod(eventName, methodName, context, args) { + return specialData[eventName] && specialData[eventName][methodName] && specialData[eventName][methodName].apply(context, args); + }, + }; +}()); + +const eventsEngine = injector({ + on: getHandler(normalizeOnArguments(iterate((element, eventName, selector, data, handler) => { + const handlersController = getHandlersController(element, eventName); + handlersController.addHandler(handler, selector, data); + }))), + + one: getHandler(normalizeOnArguments((element, eventName, selector, data, handler) => { + const oneTimeHandler = function () { + eventsEngine.off(element, eventName, selector, oneTimeHandler); + handler.apply(this, arguments); + }; + + eventsEngine.on(element, eventName, selector, data, oneTimeHandler); + })), + + off: getHandler(normalizeOffArguments(iterate((element, eventName, selector, handler) => { + const handlersController = getHandlersController(element, eventName); + handlersController.removeHandler(handler, selector); + }))), + + trigger: getHandler(normalizeTriggerArguments((element, event, extraParameters) => { + const eventName = event.type; + const handlersController = getHandlersController(element, event.type); + + special.callMethod(eventName, 'trigger', element, [event, extraParameters]); + handlersController.callHandlers(event, extraParameters); + + const noBubble = special.getField(eventName, 'noBubble') + || event.isPropagationStopped() + || NO_BUBBLE_EVENTS.includes(eventName); + + if (!noBubble) { + const parents: any = []; + const getParents = function (element) { + const parent = element.parentNode + ?? (isObject(element.host) ? element.host : null); + if (parent) { + parents.push(parent); + getParents(parent); + } + }; + getParents(element); + parents.push(window); + + let i = 0; + + while (parents[i] && !event.isPropagationStopped()) { + const parentDataByEvent = getHandlersController(parents[i], event.type); + parentDataByEvent.callHandlers(extend(event, { currentTarget: parents[i] }), extraParameters); + i++; + } + } + + if (element.nodeType || isWindow(element)) { + special.callMethod(eventName, '_default', element, [event, extraParameters]); + callNativeMethod(eventName, element); + } + })), + + triggerHandler: getHandler(normalizeTriggerArguments((element, event, extraParameters) => { + const handlersController = getHandlersController(element, event.type); + handlersController.callHandlers(event, extraParameters); + })), +}); + +function applyForEach(args, method) { + const element = args[0]; + + if (!element) { + return; + } + + if (domAdapter.isNode(element) || isWindow(element)) { + method.apply(eventsEngine, args); + } else if (!isString(element) && 'length' in element) { + const itemArgs = Array.prototype.slice.call(args, 0); + + Array.prototype.forEach.call(element, (itemElement) => { + itemArgs[0] = itemElement; + applyForEach(itemArgs, method); + }); + } else { + throw errors.Error('E0025'); + } +} + +function getHandler(method) { + return function () { + applyForEach(arguments, method); + }; +} + +function detectPassiveEventHandlersSupport() { + let isSupported = false; + + try { + const options = Object.defineProperty({}, 'passive', { + get() { + isSupported = true; + return true; + }, + }); + + // @ts-expect-error + window.addEventListener('test', null, options); + // eslint-disable-next-line no-empty + } catch (e) { } + + return isSupported; +} + +const passiveEventHandlersSupported = callOnce(detectPassiveEventHandlersSupport); + +const contains = (container, element) => { + if (isWindow(container)) { + return contains(container.document, element); + } + + return container.contains + ? container.contains(element) + : !!(element.compareDocumentPosition(container) & element.DOCUMENT_POSITION_CONTAINS); +}; + +function getHandlersController(element, eventName) { + let elementData = elementDataMap.get(element); + + eventName = eventName || ''; + + const eventNameParts = eventName.split('.'); + const namespaces = eventNameParts.slice(1); + const eventNameIsDefined = !!eventNameParts[0]; + + eventName = eventNameParts[0] || EMPTY_EVENT_NAME; + + if (!elementData) { + elementData = {}; + elementDataMap.set(element, elementData); + } + + if (!elementData[eventName]) { + elementData[eventName] = { + handleObjects: [], + nativeHandler: null, + }; + } + + const eventData = elementData[eventName]; + + return { + addHandler(handler, selector, data) { + const callHandler = function (e, extraParameters) { + const handlerArgs = [e]; + const target = e.currentTarget; + const { relatedTarget } = e; + let secondaryTargetIsInside; + let result; + + if (eventName in NATIVE_EVENTS_TO_SUBSCRIBE) { + secondaryTargetIsInside = relatedTarget && target && (relatedTarget === target || contains(target, relatedTarget)); + } + + if (extraParameters !== undefined) { + handlerArgs.push(extraParameters); + } + + special.callMethod(eventName, 'handle', element, [e, data]); + + if (!secondaryTargetIsInside) { + result = handler.apply(target, handlerArgs); + } + + if (result === false) { + e.preventDefault(); + e.stopPropagation(); + } + }; + + const wrappedHandler = function (e, extraParameters) { + if (skipEvent && e.type === skipEvent) { + return; + } + + e.data = data; + e.delegateTarget = element; + + if (selector) { + let currentTarget = e.target; + + while (currentTarget && currentTarget !== element) { + if (matchesSafe(currentTarget, selector)) { + e.currentTarget = currentTarget; + callHandler(e, extraParameters); + } + currentTarget = currentTarget.parentNode; + } + } else { + e.currentTarget = e.delegateTarget || e.target; + + const isTargetInShadowDOM = Boolean(e.target?.shadowRoot); + if (isTargetInShadowDOM) { + const target = getEventTarget(e); + e.target = target; + } + + callHandler(e, extraParameters); + } + }; + + const handleObject = { + handler, + wrappedHandler, + selector, + type: eventName, + data, + namespace: namespaces.join('.'), + namespaces, + guid: ++guid, + }; + + eventData.handleObjects.push(handleObject); + + const firstHandlerForTheType = eventData.handleObjects.length === 1; + let shouldAddNativeListener = firstHandlerForTheType && eventNameIsDefined; + let nativeListenerOptions; + + if (shouldAddNativeListener) { + shouldAddNativeListener = !special.callMethod(eventName, 'setup', element, [data, namespaces, handler]); + } + + if (shouldAddNativeListener) { + eventData.nativeHandler = getNativeHandler(eventName); + + if (passiveEventHandlersSupported() && forcePassiveFalseEventNames.includes(eventName)) { + nativeListenerOptions = { + passive: false, + }; + } + + // @ts-expect-error + eventData.removeListener = domAdapter.listen(element, NATIVE_EVENTS_TO_SUBSCRIBE[eventName] || eventName, eventData.nativeHandler, nativeListenerOptions); + } + + special.callMethod(eventName, 'add', element, [handleObject]); + }, + + removeHandler(handler, selector) { + const removeByEventName = function (eventName) { + const eventData = elementData[eventName]; + + if (!eventData.handleObjects.length) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete elementData[eventName]; + return; + } + let removedHandler; + + eventData.handleObjects = eventData.handleObjects.filter((handleObject) => { + const skip = namespaces.length && !isSubset(handleObject.namespaces, namespaces) + || handler && handleObject.handler !== handler + || selector && handleObject.selector !== selector; + + if (!skip) { + removedHandler = handleObject.handler; + special.callMethod(eventName, 'remove', element, [handleObject]); + } + + return skip; + }); + + const lastHandlerForTheType = !eventData.handleObjects.length; + const shouldRemoveNativeListener = lastHandlerForTheType && eventName !== EMPTY_EVENT_NAME; + + if (shouldRemoveNativeListener) { + special.callMethod(eventName, 'teardown', element, [namespaces, removedHandler]); + if (eventData.nativeHandler) { + eventData.removeListener(); + } + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete elementData[eventName]; + } + }; + + if (eventNameIsDefined) { + removeByEventName(eventName); + } else { + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const name in elementData) { + removeByEventName(name); + } + } + + const elementDataIsEmpty = Object.keys(elementData).length === 0; + + if (elementDataIsEmpty) { + elementDataMap.delete(element); + } + }, + + callHandlers(event, extraParameters) { + let forceStop = false; + + const handleCallback = function (handleObject) { + if (forceStop) { + return; + } + + if (!namespaces.length || isSubset(handleObject.namespaces, namespaces)) { + handleObject.wrappedHandler(event, extraParameters); + forceStop = event.isImmediatePropagationStopped(); + } + }; + + eventData.handleObjects.forEach(handleCallback); + if (namespaces.length && elementData[EMPTY_EVENT_NAME]) { + elementData[EMPTY_EVENT_NAME].handleObjects.forEach(handleCallback); + } + }, + }; +} + +function getNativeHandler(subscribeName) { + return function (event, extraParameters) { + const handlersController = getHandlersController(this, subscribeName); + event = eventsEngine.Event(event); + handlersController.callHandlers(event, extraParameters); + }; +} + +function isSubset(original, checked) { + for (let i = 0; i < checked.length; i++) { + if (original.indexOf(checked[i]) < 0) return false; + } + return true; +} + +function normalizeOnArguments(callback) { + return function (element, eventName, selector, data, handler) { + if (!handler) { + handler = data; + data = undefined; + } + if (typeof selector !== 'string') { + data = selector; + selector = undefined; + } + + if (!handler && typeof eventName === 'string') { + handler = data || selector; + selector = undefined; + data = undefined; + } + + callback(element, eventName, selector, data, handler); + }; +} + +function normalizeOffArguments(callback) { + return function (element, eventName, selector, handler) { + if (typeof selector === 'function') { + handler = selector; + selector = undefined; + } + + callback(element, eventName, selector, handler); + }; +} + +function normalizeTriggerArguments(callback) { + return function (element, src, extraParameters) { + if (typeof src === 'string') { + src = { + type: src, + }; + } + + if (!src.target) { + src.target = element; + } + + src.currentTarget = element; + + if (!src.delegateTarget) { + src.delegateTarget = element; + } + + if (!src.type && src.originalEvent) { + src.type = src.originalEvent.type; + } + + callback(element, src instanceof eventsEngine.Event ? src : eventsEngine.Event(src), extraParameters); + }; +} + +function normalizeEventArguments(callback) { + eventsEngine.Event = function (src, config) { + if (!(this instanceof eventsEngine.Event)) { + return new eventsEngine.Event(src, config); + } + + if (!src) { + src = {}; + } + + if (typeof src === 'string') { + src = { + type: src, + }; + } + + if (!config) { + config = {}; + } + + callback.call(this, src, config); + }; + Object.assign(eventsEngine.Event.prototype, { + _propagationStopped: false, + _immediatePropagationStopped: false, + _defaultPrevented: false, + isPropagationStopped() { + return !!(this._propagationStopped || this.originalEvent && this.originalEvent.propagationStopped); + }, + stopPropagation() { + this._propagationStopped = true; + this.originalEvent && this.originalEvent.stopPropagation(); + }, + isImmediatePropagationStopped() { + return this._immediatePropagationStopped; + }, + stopImmediatePropagation() { + this.stopPropagation(); + this._immediatePropagationStopped = true; + this.originalEvent && this.originalEvent.stopImmediatePropagation(); + }, + isDefaultPrevented() { + return !!(this._defaultPrevented || this.originalEvent && this.originalEvent.defaultPrevented); + }, + preventDefault() { + this._defaultPrevented = true; + this.originalEvent && this.originalEvent.preventDefault(); + }, + + }); + return eventsEngine.Event; +} + +function iterate(callback) { + const iterateEventNames = function (element, eventName) { + if (eventName && eventName.indexOf(' ') > -1) { + const args = Array.prototype.slice.call(arguments, 0); + eventName.split(' ').forEach(function (eventName) { + args[1] = eventName; + callback.apply(this, args); + }); + } else { + callback.apply(this, arguments); + } + }; + + return function (element, eventName) { + if (typeof eventName === 'object') { + const args: any = Array.prototype.slice.call(arguments, 0); + + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const name in eventName) { + args[1] = name; + args[args.length - 1] = eventName[name]; + iterateEventNames.apply(this, args); + } + } else { + // @ts-expect-error + iterateEventNames.apply(this, arguments); + } + }; +} + +function callNativeMethod(eventName, element) { + const nativeMethodName = NATIVE_EVENTS_TO_TRIGGER[eventName] || eventName; + + const isLinkClickEvent = function (eventName, element) { + return eventName === 'click' && element.localName === 'a'; + }; + + if (isLinkClickEvent(eventName, element)) return; + + if (isFunction(element[nativeMethodName])) { + skipEvent = eventName; + element[nativeMethodName](); + skipEvent = undefined; + } +} + +function calculateWhich(event) { + const setForMouseEvent = function (event) { + const mouseEventRegex = /^(?:mouse|pointer|contextmenu|drag|drop)|click/; + return !event.which && event.button !== undefined && mouseEventRegex.test(event.type); + }; + + const setForKeyEvent = function (event) { + return event.which == null && event.type.indexOf('key') === 0; + }; + + if (setForKeyEvent(event)) { + return event.charCode != null ? event.charCode : event.keyCode; + } + + if (setForMouseEvent(event)) { + const whichByButton = { + 1: 1, 2: 3, 3: 1, 4: 2, + }; + return whichByButton[event.button]; + } + + return event.which; +} + +function initEvent(EventClass) { + if (EventClass) { + eventsEngine.Event = EventClass; + eventsEngine.Event.prototype = EventClass.prototype; + } +} + +initEvent(normalizeEventArguments(function (src, config) { + const srcIsEvent = src instanceof eventsEngine.Event + // @ts-expect-error + || (hasWindow() && src instanceof window.Event) + || (src.view?.Event && src instanceof src.view.Event); + + if (srcIsEvent) { + this.originalEvent = src; + this.type = src.type; + this.currentTarget = undefined; + if (Object.prototype.hasOwnProperty.call(src, 'isTrusted')) { + this.isTrusted = src.isTrusted; + } + this.timeStamp = src.timeStamp || Date.now(); + } else { + Object.assign(this, src); + } + + addProperty('which', calculateWhich, this); + + if (src.type.indexOf('touch') === 0) { + delete config.pageX; + delete config.pageY; + } + + Object.assign(this, config); + + this.guid = ++guid; +})); + +function addProperty(propName, hook, eventInstance) { + Object.defineProperty(eventInstance || eventsEngine.Event.prototype, propName, { + enumerable: true, + configurable: true, + + get() { + return this.originalEvent && hook(this.originalEvent); + }, + + set(value) { + Object.defineProperty(this, propName, { + enumerable: true, + configurable: true, + writable: true, + value, + }); + }, + }); +} +// @ts-expect-error +EVENT_PROPERTIES.forEach((prop) => addProperty(prop, (event) => event[prop])); +hookTouchProps(addProperty); + +const beforeSetStrategy = Callbacks(); +const afterSetStrategy = Callbacks(); + +eventsEngine.set = function (engine) { + beforeSetStrategy.fire(); + eventsEngine.inject(engine); + initEvent(engine.Event); + afterSetStrategy.fire(); +}; + +eventsEngine.subscribeGlobal = function () { + applyForEach(arguments, normalizeOnArguments(function () { + const args = arguments; + + eventsEngine.on.apply(this, args); + + beforeSetStrategy.add(function () { + const offArgs = Array.prototype.slice.call(args, 0); + offArgs.splice(3, 1); + eventsEngine.off.apply(this, offArgs); + }); + + afterSetStrategy.add(function () { + eventsEngine.on.apply(this, args); + }); + })); +}; + +eventsEngine.forcePassiveFalseEventNames = forcePassiveFalseEventNames; +eventsEngine.passiveEventHandlersSupported = passiveEventHandlersSupported; + +/// #DEBUG +eventsEngine.elementDataMap = elementDataMap; +eventsEngine.detectPassiveEventHandlersSupport = detectPassiveEventHandlersSupport; + +/// #ENDDEBUG + +export default eventsEngine; diff --git a/packages/devextreme/js/__internal/events/core/m_hook_touch_props.ts b/packages/devextreme/js/__internal/events/core/m_hook_touch_props.ts new file mode 100644 index 000000000000..8099d17c4fbd --- /dev/null +++ b/packages/devextreme/js/__internal/events/core/m_hook_touch_props.ts @@ -0,0 +1,19 @@ +const touchPropsToHook = ['pageX', 'pageY', 'screenX', 'screenY', 'clientX', 'clientY']; +const touchPropHook = function (name, event) { + if (event[name] && !event.touches || !event.touches) { + return event[name]; + } + + const touches = event.touches.length ? event.touches : event.changedTouches; + if (!touches.length) { + return; + } + + return touches[0][name]; +}; + +export default function (callback) { + touchPropsToHook.forEach((name) => { + callback(name, (event) => touchPropHook(name, event)); + }, this); +} diff --git a/packages/devextreme/js/__internal/events/core/m_keyboard_processor.ts b/packages/devextreme/js/__internal/events/core/m_keyboard_processor.ts new file mode 100644 index 000000000000..bc3194beeda1 --- /dev/null +++ b/packages/devextreme/js/__internal/events/core/m_keyboard_processor.ts @@ -0,0 +1,80 @@ +import Class from '@js/core/class'; +import $ from '@js/core/renderer'; +import eventsEngine from '@js/events/core/events_engine'; +import { addNamespace, normalizeKeyName } from '@js/events/utils/index'; + +const COMPOSITION_START_EVENT = 'compositionstart'; +const COMPOSITION_END_EVENT = 'compositionend'; +const KEYDOWN_EVENT = 'keydown'; +const NAMESPACE = 'KeyboardProcessor'; + +const createKeyDownOptions = (e) => ({ + keyName: normalizeKeyName(e), + key: e.key, + code: e.code, + ctrl: e.ctrlKey, + location: e.location, + metaKey: e.metaKey, + shift: e.shiftKey, + alt: e.altKey, + which: e.which, + originalEvent: e, +}); + +const KeyboardProcessor = Class.inherit({ + _keydown: addNamespace(KEYDOWN_EVENT, NAMESPACE), + _compositionStart: addNamespace(COMPOSITION_START_EVENT, NAMESPACE), + _compositionEnd: addNamespace(COMPOSITION_END_EVENT, NAMESPACE), + + ctor(options) { + options = options || {}; + if (options.element) { + this._element = $(options.element); + } + if (options.focusTarget) { + this._focusTarget = options.focusTarget; + } + this._handler = options.handler; + + if (this._element) { + this._processFunction = (e) => { + const focusTargets = $(this._focusTarget).toArray(); + const isNotFocusTarget = this._focusTarget && this._focusTarget !== e.target && !focusTargets.includes(e.target); + const shouldSkipProcessing = this._isComposingJustFinished && e.which === 229 || this._isComposing || isNotFocusTarget; + + this._isComposingJustFinished = false; + if (!shouldSkipProcessing) { + this.process(e); + } + }; + this._toggleProcessingWithContext = this.toggleProcessing.bind(this); + + eventsEngine.on(this._element, this._keydown, this._processFunction); + eventsEngine.on(this._element, this._compositionStart, this._toggleProcessingWithContext); + eventsEngine.on(this._element, this._compositionEnd, this._toggleProcessingWithContext); + } + }, + + dispose() { + if (this._element) { + eventsEngine.off(this._element, this._keydown, this._processFunction); + eventsEngine.off(this._element, this._compositionStart, this._toggleProcessingWithContext); + eventsEngine.off(this._element, this._compositionEnd, this._toggleProcessingWithContext); + } + this._element = undefined; + this._handler = undefined; + }, + + process(e) { + this._handler(createKeyDownOptions(e)); + }, + + toggleProcessing({ type }) { + this._isComposing = type === COMPOSITION_START_EVENT; + this._isComposingJustFinished = !this._isComposing; + }, +}); +// @ts-expect-error +KeyboardProcessor.createKeyDownOptions = createKeyDownOptions; + +export default KeyboardProcessor; diff --git a/packages/devextreme/js/__internal/events/core/m_wheel.ts b/packages/devextreme/js/__internal/events/core/m_wheel.ts new file mode 100644 index 000000000000..f4f94e27f02a --- /dev/null +++ b/packages/devextreme/js/__internal/events/core/m_wheel.ts @@ -0,0 +1,55 @@ +import $ from '@js/core/renderer'; +import registerEvent from '@js/events/core/event_registrator'; +import eventsEngine from '@js/events/core/events_engine'; +import { addNamespace, fireEvent } from '@js/events/utils/index'; + +const EVENT_NAME = 'dxmousewheel'; +const EVENT_NAMESPACE = 'dxWheel'; +const NATIVE_EVENT_NAME = 'wheel'; + +const PIXEL_MODE = 0; +const DELTA_MUTLIPLIER = 30; + +const wheel = { + setup(element) { + const $element = $(element); + eventsEngine.on($element, addNamespace(NATIVE_EVENT_NAME, EVENT_NAMESPACE), wheel._wheelHandler.bind(wheel)); + }, + + teardown(element) { + eventsEngine.off(element, `.${EVENT_NAMESPACE}`); + }, + + _wheelHandler(e) { + const { + deltaMode, deltaY, deltaX, deltaZ, + } = e.originalEvent; + + fireEvent({ + type: EVENT_NAME, + originalEvent: e, + // @ts-expect-error + delta: this._normalizeDelta(deltaY, deltaMode), + deltaX, + deltaY, + deltaZ, + deltaMode, + pointerType: 'mouse', + }); + + e.stopPropagation(); + }, + + _normalizeDelta(delta, deltaMode = PIXEL_MODE) { + if (deltaMode === PIXEL_MODE) { + return -delta; + } + // Use multiplier to get rough delta value in px for the LINE or PAGE mode + // https://bugzilla.mozilla.org/show_bug.cgi?id=1392460 + return -DELTA_MUTLIPLIER * delta; + }, +}; + +registerEvent(EVENT_NAME, wheel); + +export { EVENT_NAME as name }; diff --git a/packages/devextreme/js/__internal/events/gesture/m_emitter.gesture.scroll.ts b/packages/devextreme/js/__internal/events/gesture/m_emitter.gesture.scroll.ts new file mode 100644 index 000000000000..b8ba6593c113 --- /dev/null +++ b/packages/devextreme/js/__internal/events/gesture/m_emitter.gesture.scroll.ts @@ -0,0 +1,341 @@ +import { cancelAnimationFrame, requestAnimationFrame } from '@js/animation/frame'; +import Class from '@js/core/class'; +import devices from '@js/core/devices'; +import registerEmitter from '@js/events/core/emitter_registrator'; +import eventsEngine from '@js/events/core/events_engine'; +import GestureEmitter from '@js/events/gesture/emitter.gesture'; +import { + addNamespace, eventData, eventDelta, isDxMouseWheelEvent, isMouseEvent, +} from '@js/events/utils/index'; + +const { abstract } = Class; + +const realDevice = devices.real(); + +const SCROLL_EVENT = 'scroll'; +const SCROLL_INIT_EVENT = 'dxscrollinit'; +const SCROLL_START_EVENT = 'dxscrollstart'; +const SCROLL_MOVE_EVENT = 'dxscroll'; +const SCROLL_END_EVENT = 'dxscrollend'; +const SCROLL_STOP_EVENT = 'dxscrollstop'; +const SCROLL_CANCEL_EVENT = 'dxscrollcancel'; + +const Locker = Class.inherit((function () { + const NAMESPACED_SCROLL_EVENT = addNamespace(SCROLL_EVENT, 'dxScrollEmitter'); + + return { + + ctor(element) { + this._element = element; + + this._locked = false; + + this._proxiedScroll = (e) => { + if (!this._disposed) { + this._scroll(e); + } + }; + eventsEngine.on(this._element, NAMESPACED_SCROLL_EVENT, this._proxiedScroll); + }, + + _scroll: abstract, + + check(e, callback) { + if (this._locked) { + callback(); + } + }, + + dispose() { + this._disposed = true; + eventsEngine.off(this._element, NAMESPACED_SCROLL_EVENT, this._proxiedScroll); + }, + + }; +})()); + +const TimeoutLocker = Locker.inherit((function () { + return { + + ctor(element, timeout) { + this.callBase(element); + + this._timeout = timeout; + }, + + _scroll() { + this._prepare(); + this._forget(); + }, + + _prepare() { + if (this._timer) { + this._clearTimer(); + } + this._locked = true; + }, + + _clearTimer() { + clearTimeout(this._timer); + this._locked = false; + this._timer = null; + }, + + _forget() { + const that = this; + + this._timer = setTimeout(() => { + that._clearTimer(); + }, this._timeout); + }, + + dispose() { + this.callBase(); + + this._clearTimer(); + }, + + }; +})()); + +const WheelLocker = TimeoutLocker.inherit((function () { + const WHEEL_UNLOCK_TIMEOUT = 400; + + return { + + ctor(element) { + this.callBase(element, WHEEL_UNLOCK_TIMEOUT); + + this._lastWheelDirection = null; + }, + + check(e, callback) { + this._checkDirectionChanged(e); + + this.callBase(e, callback); + }, + + _checkDirectionChanged(e) { + if (!isDxMouseWheelEvent(e)) { + this._lastWheelDirection = null; + return; + } + + const direction = e.shiftKey || false; + const directionChange = this._lastWheelDirection !== null && direction !== this._lastWheelDirection; + this._lastWheelDirection = direction; + + this._locked = this._locked && !directionChange; + }, + + }; +})()); + +let PointerLocker = TimeoutLocker.inherit((function () { + const POINTER_UNLOCK_TIMEOUT = 400; + + return { + + ctor(element) { + this.callBase(element, POINTER_UNLOCK_TIMEOUT); + }, + + }; +})()); + +(function () { + const { ios: isIos, android: isAndroid } = realDevice; + + if (!(isIos || isAndroid)) { + return; + } + + PointerLocker = Locker.inherit((function () { + return { + + _scroll() { + this._locked = true; + + const that = this; + cancelAnimationFrame(this._scrollFrame); + this._scrollFrame = requestAnimationFrame(() => { + that._locked = false; + }); + }, + + check(e, callback) { + cancelAnimationFrame(this._scrollFrame); + cancelAnimationFrame(this._checkFrame); + + const that = this; + const { callBase } = this; + this._checkFrame = requestAnimationFrame(() => { + callBase.call(that, e, callback); + + that._locked = false; + }); + }, + + dispose() { + this.callBase(); + + cancelAnimationFrame(this._scrollFrame); + cancelAnimationFrame(this._checkFrame); + }, + + }; + })()); +}()); + +const ScrollEmitter = GestureEmitter.inherit((function () { + const INERTIA_TIMEOUT = 100; + const VELOCITY_CALC_TIMEOUT = 200; + const FRAME_DURATION = Math.round(1000 / 60); + + return { + + ctor(element) { + this.callBase.apply(this, arguments); + this.direction = 'both'; + + this._pointerLocker = new PointerLocker(element); + this._wheelLocker = new WheelLocker(element); + }, + + validate() { + return true; + }, + + configure(data) { + if (data.scrollTarget) { + this._pointerLocker.dispose(); + this._wheelLocker.dispose(); + this._pointerLocker = new PointerLocker(data.scrollTarget); + this._wheelLocker = new WheelLocker(data.scrollTarget); + } + + this.callBase(data); + }, + + _init(e) { + this._wheelLocker.check(e, () => { + if (isDxMouseWheelEvent(e)) { + this._accept(e); + } + }); + + this._pointerLocker.check(e, () => { + const skipCheck = this.isNative && isMouseEvent(e); + if (!isDxMouseWheelEvent(e) && !skipCheck) { + this._accept(e); + } + }); + + this._fireEvent(SCROLL_INIT_EVENT, e); + + this._prevEventData = eventData(e); + }, + + move(e) { + this.callBase.apply(this, arguments); + + e.isScrollingEvent = this.isNative || e.isScrollingEvent; + }, + + _start(e) { + this._savedEventData = eventData(e); + + this._fireEvent(SCROLL_START_EVENT, e); + + this._prevEventData = eventData(e); + }, + + _move(e) { + const currentEventData: any = eventData(e); + + this._fireEvent(SCROLL_MOVE_EVENT, e, { + delta: eventDelta(this._prevEventData, currentEventData), + }); + + const delta = eventDelta(this._savedEventData, currentEventData); + if (delta.time > VELOCITY_CALC_TIMEOUT) { + this._savedEventData = this._prevEventData; + } + + this._prevEventData = eventData(e); + }, + + _end(e) { + // @ts-expect-error + const endEventDelta = eventDelta(this._prevEventData, eventData(e)); + let velocity = { x: 0, y: 0 }; + + if (!isDxMouseWheelEvent(e) && endEventDelta.time < INERTIA_TIMEOUT) { + const delta = eventDelta(this._savedEventData, this._prevEventData); + const velocityMultiplier = FRAME_DURATION / delta.time; + + velocity = { x: delta.x * velocityMultiplier, y: delta.y * velocityMultiplier }; + } + + this._fireEvent(SCROLL_END_EVENT, e, { + velocity, + }); + }, + + _stop(e) { + this._fireEvent(SCROLL_STOP_EVENT, e); + }, + + cancel(e) { + this.callBase.apply(this, arguments); + + this._fireEvent(SCROLL_CANCEL_EVENT, e); + }, + + dispose() { + this.callBase.apply(this, arguments); + + this._pointerLocker.dispose(); + this._wheelLocker.dispose(); + }, + + _clearSelection() { + if (this.isNative) { + return; + } + + return this.callBase.apply(this, arguments); + }, + + _toggleGestureCover() { + if (this.isNative) { + return; + } + + return this.callBase.apply(this, arguments); + }, + + }; +})()); + +registerEmitter({ + emitter: ScrollEmitter, + events: [ + SCROLL_INIT_EVENT, + SCROLL_START_EVENT, + SCROLL_MOVE_EVENT, + SCROLL_END_EVENT, + SCROLL_STOP_EVENT, + SCROLL_CANCEL_EVENT, + ], +}); + +export default { + init: SCROLL_INIT_EVENT, + start: SCROLL_START_EVENT, + move: SCROLL_MOVE_EVENT, + end: SCROLL_END_EVENT, + stop: SCROLL_STOP_EVENT, + cancel: SCROLL_CANCEL_EVENT, + scroll: SCROLL_EVENT, +}; diff --git a/packages/devextreme/js/__internal/events/gesture/m_emitter.gesture.ts b/packages/devextreme/js/__internal/events/gesture/m_emitter.gesture.ts new file mode 100644 index 000000000000..b74fd5ff811b --- /dev/null +++ b/packages/devextreme/js/__internal/events/gesture/m_emitter.gesture.ts @@ -0,0 +1,244 @@ +import devices from '@js/core/devices'; +import $ from '@js/core/renderer'; +import callOnce from '@js/core/utils/call_once'; +import { noop } from '@js/core/utils/common'; +import { clearSelection, resetActiveElement } from '@js/core/utils/dom'; +import { sign } from '@js/core/utils/math'; +import readyCallbacks from '@js/core/utils/ready_callbacks'; +import { styleProp } from '@js/core/utils/style'; +import { isDefined } from '@js/core/utils/type'; +import Emitter from '@js/events/core/emitter'; +import eventsEngine from '@js/events/core/events_engine'; +import { + createEvent, eventData, eventDelta, isDxMouseWheelEvent, isTouchEvent, needSkipEvent, +} from '@js/events/utils/index'; + +const ready = readyCallbacks.add; +const { abs } = Math; + +const SLEEP = 0; +const INITED = 1; +const STARTED = 2; + +let TOUCH_BOUNDARY = 10; +const IMMEDIATE_TOUCH_BOUNDARY = 0; +const IMMEDIATE_TIMEOUT = 180; + +const supportPointerEvents = function () { + return styleProp('pointer-events'); +}; + +const setGestureCover = callOnce(() => { + const GESTURE_COVER_CLASS = 'dx-gesture-cover'; + + const isDesktop = devices.real().deviceType === 'desktop'; + + if (!supportPointerEvents() || !isDesktop) { + return noop; + } + + const $cover = $('
') + .addClass(GESTURE_COVER_CLASS) + .css('pointerEvents', 'none'); + // @ts-expect-error + eventsEngine.subscribeGlobal($cover, 'dxmousewheel', (e) => { + e.preventDefault(); + }); + + ready(() => { + // @ts-expect-error + $cover.appendTo('body'); + }); + + return function (toggle, cursor) { + $cover.css('pointerEvents', toggle ? 'all' : 'none'); + toggle && $cover.css('cursor', cursor); + }; +}); + +const gestureCover = function (toggle, cursor) { + const gestureCoverStrategy = setGestureCover(); + gestureCoverStrategy(toggle, cursor); +}; + +const GestureEmitter = Emitter.inherit({ + + gesture: true, + + configure(data) { + this.getElement().css('msTouchAction', data.immediate ? 'pinch-zoom' : ''); + + this.callBase(data); + }, + + allowInterruptionByMouseWheel() { + return this._stage !== STARTED; + }, + + getDirection() { + return this.direction; + }, + + _cancel() { + this.callBase.apply(this, arguments); + + this._toggleGestureCover(false); + this._stage = SLEEP; + }, + + start(e) { + if (e._needSkipEvent || needSkipEvent(e)) { + this._cancel(e); + return; + } + + this._startEvent = createEvent(e); + this._startEventData = eventData(e); + + this._stage = INITED; + this._init(e); + + this._setupImmediateTimer(); + }, + + _setupImmediateTimer() { + clearTimeout(this._immediateTimer); + this._immediateAccepted = false; + + if (!this.immediate) { + return; + } + + if (this.immediateTimeout === 0) { + this._immediateAccepted = true; + return; + } + + this._immediateTimer = setTimeout(() => { + this._immediateAccepted = true; + }, this.immediateTimeout ?? IMMEDIATE_TIMEOUT); + }, + + move(e) { + if (this._stage === INITED && this._directionConfirmed(e)) { + this._stage = STARTED; + + this._resetActiveElement(); + this._toggleGestureCover(true); + this._clearSelection(e); + + this._adjustStartEvent(e); + this._start(this._startEvent); + + if (this._stage === SLEEP) { + return; + } + + this._requestAccept(e); + this._move(e); + this._forgetAccept(); + } else if (this._stage === STARTED) { + this._clearSelection(e); + this._move(e); + } + }, + + _directionConfirmed(e) { + const touchBoundary = this._getTouchBoundary(e); + // @ts-expect-error + const delta = eventDelta(this._startEventData, eventData(e)); + const deltaX = abs(delta.x); + const deltaY = abs(delta.y); + + const horizontalMove = this._validateMove(touchBoundary, deltaX, deltaY); + const verticalMove = this._validateMove(touchBoundary, deltaY, deltaX); + + const direction = this.getDirection(e); + const bothAccepted = direction === 'both' && (horizontalMove || verticalMove); + const horizontalAccepted = direction === 'horizontal' && horizontalMove; + const verticalAccepted = direction === 'vertical' && verticalMove; + + return bothAccepted || horizontalAccepted || verticalAccepted || this._immediateAccepted; + }, + + _validateMove(touchBoundary, mainAxis, crossAxis) { + return mainAxis && mainAxis >= touchBoundary && (this.immediate ? mainAxis >= crossAxis : true); + }, + + _getTouchBoundary(e) { + return this.immediate || isDxMouseWheelEvent(e) ? IMMEDIATE_TOUCH_BOUNDARY : TOUCH_BOUNDARY; + }, + + _adjustStartEvent(e) { + const touchBoundary = this._getTouchBoundary(e); + // @ts-expect-error + const delta = eventDelta(this._startEventData, eventData(e)); + + this._startEvent.pageX += sign(delta.x) * touchBoundary; + this._startEvent.pageY += sign(delta.y) * touchBoundary; + }, + + _resetActiveElement() { + if (devices.real().platform === 'ios' && this.getElement().find(':focus').length) { + resetActiveElement(); + } + }, + + _toggleGestureCover(toggle) { + this._toggleGestureCoverImpl(toggle); + }, + + _toggleGestureCoverImpl(toggle) { + const isStarted = this._stage === STARTED; + + if (isStarted) { + gestureCover(toggle, this.getElement().css('cursor')); + } + }, + + _clearSelection(e) { + if (isDxMouseWheelEvent(e) || isTouchEvent(e)) { + return; + } + + clearSelection(); + }, + + end(e) { + this._toggleGestureCover(false); + + if (this._stage === STARTED) { + this._end(e); + } else if (this._stage === INITED) { + this._stop(e); + } + + this._stage = SLEEP; + }, + + dispose() { + clearTimeout(this._immediateTimer); + this.callBase.apply(this, arguments); + this._toggleGestureCover(false); + }, + + _init: noop, + _start: noop, + _move: noop, + _stop: noop, + _end: noop, + +}); +// @ts-expect-error +GestureEmitter.initialTouchBoundary = TOUCH_BOUNDARY; +// @ts-expect-error +GestureEmitter.touchBoundary = function (newBoundary) { + if (isDefined(newBoundary)) { + TOUCH_BOUNDARY = newBoundary; + return; + } + + return TOUCH_BOUNDARY; +}; + +export default GestureEmitter; diff --git a/packages/devextreme/js/__internal/events/gesture/m_swipeable.ts b/packages/devextreme/js/__internal/events/gesture/m_swipeable.ts new file mode 100644 index 000000000000..4ba1540119cd --- /dev/null +++ b/packages/devextreme/js/__internal/events/gesture/m_swipeable.ts @@ -0,0 +1,110 @@ +import DOMComponent from '@js/core/dom_component'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import { name } from '@js/core/utils/public_component'; +import eventsEngine from '@js/events/core/events_engine'; +import { + end as swipeEventEnd, + start as swipeEventStart, + swipe as swipeEventSwipe, +} from '@js/events/swipe'; +import { addNamespace } from '@js/events/utils/index'; + +const DX_SWIPEABLE = 'dxSwipeable'; +const SWIPEABLE_CLASS = 'dx-swipeable'; + +const ACTION_TO_EVENT_MAP = { + onStart: swipeEventStart, + onUpdated: swipeEventSwipe, + onEnd: swipeEventEnd, + onCancel: 'dxswipecancel', +}; + +const IMMEDIATE_TIMEOUT = 180; +// @ts-expect-error +const Swipeable = DOMComponent.inherit({ + + _getDefaultOptions() { + return extend(this.callBase(), { + elastic: true, + immediate: false, + immediateTimeout: IMMEDIATE_TIMEOUT, + direction: 'horizontal', + itemSizeFunc: null, + onStart: null, + onUpdated: null, + onEnd: null, + onCancel: null, + }); + }, + + _render() { + this.callBase(); + + this.$element().addClass(SWIPEABLE_CLASS); + this._attachEventHandlers(); + }, + + _attachEventHandlers() { + this._detachEventHandlers(); + + if (this.option('disabled')) { + return; + } + + const { NAME } = this; + + this._createEventData(); + + each(ACTION_TO_EVENT_MAP, (actionName, eventName) => { + const action = this._createActionByOption(actionName, { context: this }); + + eventName = addNamespace(eventName, NAME); + + eventsEngine.on(this.$element(), eventName, this._eventData, (e) => action({ event: e })); + }); + }, + + _createEventData() { + this._eventData = { + elastic: this.option('elastic'), + itemSizeFunc: this.option('itemSizeFunc'), + direction: this.option('direction'), + immediate: this.option('immediate'), + immediateTimeout: this.option('immediateTimeout'), + }; + }, + + _detachEventHandlers() { + eventsEngine.off(this.$element(), `.${DX_SWIPEABLE}`); + }, + + _optionChanged(args) { + switch (args.name) { + case 'disabled': + case 'onStart': + case 'onUpdated': + case 'onEnd': + case 'onCancel': + case 'elastic': + case 'immediate': + case 'itemSizeFunc': + case 'direction': + this._detachEventHandlers(); + this._attachEventHandlers(); + break; + case 'rtlEnabled': + break; + default: + this.callBase(args); + } + }, + + _useTemplates() { + return false; + }, +}); + +name(Swipeable, DX_SWIPEABLE); + +export default Swipeable; diff --git a/packages/devextreme/js/__internal/events/m_click.ts b/packages/devextreme/js/__internal/events/m_click.ts new file mode 100644 index 000000000000..adbe5737fb8c --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_click.ts @@ -0,0 +1,122 @@ +import { cancelAnimationFrame, requestAnimationFrame } from '@js/animation/frame'; +import devices from '@js/core/devices'; +import domAdapter from '@js/core/dom_adapter'; +import $ from '@js/core/renderer'; +import { resetActiveElement } from '@js/core/utils/dom'; +import Emitter from '@js/events/core/emitter'; +import registerEmitter from '@js/events/core/emitter_registrator'; +import eventsEngine from '@js/events/core/events_engine'; +import pointerEvents from '@js/events/pointer'; +import { subscribeNodesDisposing, unsubscribeNodesDisposing } from '@js/events/utils/event_nodes_disposing'; +import { getEventTarget } from '@js/events/utils/event_target'; +import { addNamespace, fireEvent } from '@js/events/utils/index'; + +const CLICK_EVENT_NAME = 'dxclick'; + +const misc = { requestAnimationFrame, cancelAnimationFrame }; + +let prevented: boolean | null = null; +let lastFiredEvent = null; + +const onNodeRemove = () => { + lastFiredEvent = null; +}; + +const clickHandler = function (e) { + const { originalEvent } = e; + const eventAlreadyFired = lastFiredEvent === originalEvent || originalEvent && originalEvent.DXCLICK_FIRED; + const leftButton = !e.which || e.which === 1; + + if (leftButton && !prevented && !eventAlreadyFired) { + if (originalEvent) { + originalEvent.DXCLICK_FIRED = true; + } + + unsubscribeNodesDisposing(lastFiredEvent, onNodeRemove); + lastFiredEvent = originalEvent; + subscribeNodesDisposing(lastFiredEvent, onNodeRemove); + + fireEvent({ + type: CLICK_EVENT_NAME, + originalEvent: e, + }); + } +}; + +const ClickEmitter = Emitter.inherit({ + + ctor(element) { + this.callBase(element); + eventsEngine.on(this.getElement(), 'click', clickHandler); + }, + + start() { + prevented = null; + }, + + cancel() { + prevented = true; + }, + + dispose() { + eventsEngine.off(this.getElement(), 'click', clickHandler); + }, +}); + +// NOTE: fixes native click blur on slow devices +(function () { + const desktopDevice = devices.real().generic; + + if (!desktopDevice) { + let startTarget = null; + let blurPrevented = false; + + const isInput = function (element) { + return $(element).is('input, textarea, select, button ,:focus, :focus *'); + }; + + const pointerDownHandler = function (e) { + startTarget = e.target; + blurPrevented = e.isDefaultPrevented(); + }; + + const getTarget = function (e) { + const target = getEventTarget(e); + return $(target); + }; + + const clickHandler = function (e) { + const $target = getTarget(e); + + if (!blurPrevented && startTarget && !$target.is(startTarget) && !$(startTarget).is('label') && isInput($target)) { + resetActiveElement(); + } + + startTarget = null; + blurPrevented = false; + }; + + const NATIVE_CLICK_FIXER_NAMESPACE = 'NATIVE_CLICK_FIXER'; + const document = domAdapter.getDocument(); + // @ts-expect-error + eventsEngine.subscribeGlobal(document, addNamespace(pointerEvents.down, NATIVE_CLICK_FIXER_NAMESPACE), pointerDownHandler); + // @ts-expect-error + eventsEngine.subscribeGlobal(document, addNamespace('click', NATIVE_CLICK_FIXER_NAMESPACE), clickHandler); + } +}()); + +registerEmitter({ + emitter: ClickEmitter, + bubble: true, + events: [ + CLICK_EVENT_NAME, + ], +}); + +export { CLICK_EVENT_NAME as name }; + +/// #DEBUG +export { + misc, +}; +/// #ENDDEBUG diff --git a/packages/devextreme/js/__internal/events/m_contextmenu.ts b/packages/devextreme/js/__internal/events/m_contextmenu.ts new file mode 100644 index 000000000000..958534cfd4d8 --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_contextmenu.ts @@ -0,0 +1,56 @@ +import Class from '@js/core/class'; +import devices from '@js/core/devices'; +import $ from '@js/core/renderer'; +import { touch } from '@js/core/utils/support'; +import registerEvent from '@js/events/core/event_registrator'; +import eventsEngine from '@js/events/core/events_engine'; +import holdEvent from '@js/events/hold'; +import { addNamespace, fireEvent, isMouseEvent } from '@js/events/utils/index'; + +const CONTEXTMENU_NAMESPACE = 'dxContexMenu'; + +const CONTEXTMENU_NAMESPACED_EVENT_NAME = addNamespace('contextmenu', CONTEXTMENU_NAMESPACE); +const HOLD_NAMESPACED_EVENT_NAME = addNamespace(holdEvent.name, CONTEXTMENU_NAMESPACE); + +const CONTEXTMENU_EVENT_NAME = 'dxcontextmenu'; + +const ContextMenu = Class.inherit({ + + setup(element) { + const $element = $(element); + + eventsEngine.on($element, CONTEXTMENU_NAMESPACED_EVENT_NAME, this._contextMenuHandler.bind(this)); + + if (touch || devices.isSimulator()) { + eventsEngine.on($element, HOLD_NAMESPACED_EVENT_NAME, this._holdHandler.bind(this)); + } + }, + + _holdHandler(e) { + if (isMouseEvent(e) && !devices.isSimulator()) { + return; + } + + this._fireContextMenu(e); + }, + + _contextMenuHandler(e) { + this._fireContextMenu(e); + }, + + _fireContextMenu(e) { + return fireEvent({ + type: CONTEXTMENU_EVENT_NAME, + originalEvent: e, + }); + }, + + teardown(element) { + eventsEngine.off(element, `.${CONTEXTMENU_NAMESPACE}`); + }, + +}); + +registerEvent(CONTEXTMENU_EVENT_NAME, new ContextMenu()); + +export const name = CONTEXTMENU_EVENT_NAME; diff --git a/packages/devextreme/js/__internal/events/dblclick.ts b/packages/devextreme/js/__internal/events/m_dblclick.ts similarity index 100% rename from packages/devextreme/js/__internal/events/dblclick.ts rename to packages/devextreme/js/__internal/events/m_dblclick.ts diff --git a/packages/devextreme/js/__internal/events/m_drag.ts b/packages/devextreme/js/__internal/events/m_drag.ts new file mode 100644 index 000000000000..952dd4b7f624 --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_drag.ts @@ -0,0 +1,311 @@ +import { data as elementData, removeData } from '@js/core/element_data'; +import $ from '@js/core/renderer'; +import { wrapToArray } from '@js/core/utils/array'; +import { contains } from '@js/core/utils/dom'; +import * as iteratorUtils from '@js/core/utils/iterator'; +import registerEmitter from '@js/events/core/emitter_registrator'; +import registerEvent from '@js/events/core/event_registrator'; +import GestureEmitter from '@js/events/gesture/emitter.gesture'; +import { eventData as eData, fireEvent } from '@js/events/utils/index'; + +const DRAG_START_EVENT = 'dxdragstart'; +const DRAG_EVENT = 'dxdrag'; +const DRAG_END_EVENT = 'dxdragend'; + +const DRAG_ENTER_EVENT = 'dxdragenter'; +const DRAG_LEAVE_EVENT = 'dxdragleave'; +const DROP_EVENT = 'dxdrop'; + +const DX_DRAG_EVENTS_COUNT_KEY = 'dxDragEventsCount'; + +const knownDropTargets: Element[] = []; +const knownDropTargetSelectors: any[] = []; +const knownDropTargetConfigs: any[] = []; + +const dropTargetRegistration = { + + setup(element, data) { + const knownDropTarget = knownDropTargets.includes(element); + if (!knownDropTarget) { + knownDropTargets.push(element); + knownDropTargetSelectors.push([]); + knownDropTargetConfigs.push(data || {}); + } + }, + + add(element, handleObj) { + const index = knownDropTargets.indexOf(element); + this.updateEventsCounter(element, handleObj.type, 1); + + const { selector } = handleObj; + if (!knownDropTargetSelectors[index].includes(selector)) { + knownDropTargetSelectors[index].push(selector); + } + }, + + updateEventsCounter(element, event, value) { + if ([DRAG_ENTER_EVENT, DRAG_LEAVE_EVENT, DROP_EVENT].includes(event)) { + const eventsCount = elementData(element, DX_DRAG_EVENTS_COUNT_KEY) || 0; + elementData(element, DX_DRAG_EVENTS_COUNT_KEY, Math.max(0, eventsCount + value)); + } + }, + + remove(element, handleObj) { + this.updateEventsCounter(element, handleObj.type, -1); + }, + + teardown(element) { + const handlersCount = elementData(element, DX_DRAG_EVENTS_COUNT_KEY); + if (!handlersCount) { + const index = knownDropTargets.indexOf(element); + knownDropTargets.splice(index, 1); + knownDropTargetSelectors.splice(index, 1); + knownDropTargetConfigs.splice(index, 1); + removeData(element, DX_DRAG_EVENTS_COUNT_KEY); + } + }, + +}; + +registerEvent(DRAG_ENTER_EVENT, dropTargetRegistration); +registerEvent(DRAG_LEAVE_EVENT, dropTargetRegistration); +registerEvent(DROP_EVENT, dropTargetRegistration); + +const getItemDelegatedTargets = function ($element) { + const dropTargetIndex = knownDropTargets.indexOf($element.get(0)); + const dropTargetSelectors = knownDropTargetSelectors[dropTargetIndex].filter((selector) => selector); + + let $delegatedTargets = $element.find(dropTargetSelectors.join(', ')); + if (knownDropTargetSelectors[dropTargetIndex].includes(undefined)) { + $delegatedTargets = $delegatedTargets.add($element); + } + return $delegatedTargets; +}; + +const getItemConfig = function ($element) { + const dropTargetIndex = knownDropTargets.indexOf($element.get(0)); + return knownDropTargetConfigs[dropTargetIndex]; +}; + +const getItemPosition = function (dropTargetConfig, $element) { + if (dropTargetConfig.itemPositionFunc) { + return dropTargetConfig.itemPositionFunc($element); + } + return $element.offset(); +}; + +const getItemSize = function (dropTargetConfig, $element) { + if (dropTargetConfig.itemSizeFunc) { + return dropTargetConfig.itemSizeFunc($element); + } + + return { + width: $element.get(0).getBoundingClientRect().width, + height: $element.get(0).getBoundingClientRect().height, + }; +}; + +const DragEmitter = GestureEmitter.inherit({ + + ctor(element) { + this.callBase(element); + + this.direction = 'both'; + }, + + _init(e) { + this._initEvent = e; + }, + + _start(e) { + e = this._fireEvent(DRAG_START_EVENT, this._initEvent); + + this._maxLeftOffset = e.maxLeftOffset; + this._maxRightOffset = e.maxRightOffset; + this._maxTopOffset = e.maxTopOffset; + this._maxBottomOffset = e.maxBottomOffset; + + if (e.targetElements || e.targetElements === null) { + const dropTargets = wrapToArray(e.targetElements || []); + this._dropTargets = iteratorUtils.map(dropTargets, (element) => $(element).get(0)); + } else { + this._dropTargets = knownDropTargets; + } + }, + + _move(e) { + const eventData = eData(e); + const dragOffset = this._calculateOffset(eventData); + + e = this._fireEvent(DRAG_EVENT, e, { + offset: dragOffset, + }); + + this._processDropTargets(e); + + if (!e._cancelPreventDefault) { + e.preventDefault(); + } + }, + + _calculateOffset(eventData) { + return { + x: this._calculateXOffset(eventData), + y: this._calculateYOffset(eventData), + }; + }, + + _calculateXOffset(eventData) { + if (this.direction !== 'vertical') { + const offset = eventData.x - this._startEventData.x; + + return this._fitOffset(offset, this._maxLeftOffset, this._maxRightOffset); + } + return 0; + }, + + _calculateYOffset(eventData) { + if (this.direction !== 'horizontal') { + const offset = eventData.y - this._startEventData.y; + + return this._fitOffset(offset, this._maxTopOffset, this._maxBottomOffset); + } + return 0; + }, + + _fitOffset(offset, minOffset, maxOffset) { + if (minOffset != null) { + offset = Math.max(offset, -minOffset); + } + if (maxOffset != null) { + offset = Math.min(offset, maxOffset); + } + + return offset; + }, + + _processDropTargets(e) { + const target = this._findDropTarget(e); + const sameTarget = target === this._currentDropTarget; + + if (!sameTarget) { + this._fireDropTargetEvent(e, DRAG_LEAVE_EVENT); + this._currentDropTarget = target; + this._fireDropTargetEvent(e, DRAG_ENTER_EVENT); + } + }, + + _fireDropTargetEvent(event, eventName) { + if (!this._currentDropTarget) { + return; + } + + const eventData = { + type: eventName, + originalEvent: event, + draggingElement: this._$element.get(0), + target: this._currentDropTarget, + }; + + fireEvent(eventData); + }, + + _findDropTarget(e) { + const that = this; + let result; + + iteratorUtils.each(knownDropTargets, (_, target) => { + if (!that._checkDropTargetActive(target)) { + return; + } + + const $target = $(target); + iteratorUtils.each(getItemDelegatedTargets($target), (_, delegatedTarget) => { + const $delegatedTarget = $(delegatedTarget); + if (that._checkDropTarget(getItemConfig($target), $delegatedTarget, $(result), e)) { + result = delegatedTarget; + } + }); + }); + + return result; + }, + + _checkDropTargetActive(target) { + let active = false; + + iteratorUtils.each(this._dropTargets, (_, activeTarget) => { + active = active || activeTarget === target || contains(activeTarget, target); + return !active; + }); + + return active; + }, + + _checkDropTarget(config, $target, $prevTarget, e) { + const isDraggingElement = $target.get(0) === $(e.target).get(0); + if (isDraggingElement) { + return false; + } + + const targetPosition = getItemPosition(config, $target); + if (e.pageX < targetPosition.left) { + return false; + } + if (e.pageY < targetPosition.top) { + return false; + } + + const targetSize = getItemSize(config, $target); + if (e.pageX > targetPosition.left + targetSize.width) { + return false; + } + if (e.pageY > targetPosition.top + targetSize.height) { + return false; + } + + if ($prevTarget.length && $prevTarget.closest($target).length) { + return false; + } + + if (config.checkDropTarget && !config.checkDropTarget($target, e)) { + return false; + } + + return $target; + }, + + _end(e) { + const eventData = eData(e); + + this._fireEvent(DRAG_END_EVENT, e, { + offset: this._calculateOffset(eventData), + }); + + this._fireDropTargetEvent(e, DROP_EVENT); + delete this._currentDropTarget; + }, + +}); + +registerEmitter({ + emitter: DragEmitter, + events: [ + DRAG_START_EVENT, + DRAG_EVENT, + DRAG_END_EVENT, + ], +}); + +/// #DEBUG +export { knownDropTargets as dropTargets }; +/// #ENDDEBUG + +export { + DROP_EVENT as drop, + DRAG_END_EVENT as end, + DRAG_ENTER_EVENT as enter, + DRAG_LEAVE_EVENT as leave, + DRAG_EVENT as move, + DRAG_START_EVENT as start, +}; diff --git a/packages/devextreme/js/__internal/events/m_hold.ts b/packages/devextreme/js/__internal/events/m_hold.ts new file mode 100644 index 000000000000..f5fb13543588 --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_hold.ts @@ -0,0 +1,71 @@ +import Emitter from '@js/events/core/emitter'; +import registerEmitter from '@js/events/core/emitter_registrator'; +import { eventData, eventDelta } from '@js/events/utils/index'; + +const { abs } = Math; + +const HOLD_EVENT_NAME = 'dxhold'; +const HOLD_TIMEOUT = 750; +const TOUCH_BOUNDARY = 5; + +const HoldEmitter = Emitter.inherit({ + + start(e) { + this._startEventData = eventData(e); + + this._startTimer(e); + }, + + _startTimer(e) { + const holdTimeout = 'timeout' in this ? this.timeout : HOLD_TIMEOUT; + this._holdTimer = setTimeout(() => { + this._requestAccept(e); + this._fireEvent(HOLD_EVENT_NAME, e, { + target: e.target, + }); + this._forgetAccept(); + }, holdTimeout); + }, + + move(e) { + if (this._touchWasMoved(e)) { + this._cancel(e); + } + }, + + _touchWasMoved(e) { + // @ts-expect-error + const delta = eventDelta(this._startEventData, eventData(e)); + + return abs(delta.x) > TOUCH_BOUNDARY || abs(delta.y) > TOUCH_BOUNDARY; + }, + + end() { + this._stopTimer(); + }, + + _stopTimer() { + clearTimeout(this._holdTimer); + }, + + cancel() { + this._stopTimer(); + }, + + dispose() { + this._stopTimer(); + }, + +}); + +registerEmitter({ + emitter: HoldEmitter, + bubble: true, + events: [ + HOLD_EVENT_NAME, + ], +}); + +export default { + name: HOLD_EVENT_NAME, +}; diff --git a/packages/devextreme/js/__internal/events/m_hover.ts b/packages/devextreme/js/__internal/events/m_hover.ts new file mode 100644 index 000000000000..d28353334c2b --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_hover.ts @@ -0,0 +1,98 @@ +import Class from '@js/core/class'; +import devices from '@js/core/devices'; +import { data as elementData, removeData } from '@js/core/element_data'; +import registerEvent from '@js/events/core/event_registrator'; +import eventsEngine from '@js/events/core/events_engine'; +import pointerEvents from '@js/events/pointer'; +import { addNamespace, fireEvent, isTouchEvent } from '@js/events/utils/index'; + +const HOVERSTART_NAMESPACE = 'dxHoverStart'; +const HOVERSTART = 'dxhoverstart'; +const POINTERENTER_NAMESPACED_EVENT_NAME = addNamespace(pointerEvents.enter, HOVERSTART_NAMESPACE); + +const HOVEREND_NAMESPACE = 'dxHoverEnd'; +const HOVEREND = 'dxhoverend'; +const POINTERLEAVE_NAMESPACED_EVENT_NAME = addNamespace(pointerEvents.leave, HOVEREND_NAMESPACE); + +const Hover = Class.inherit({ + + noBubble: true, + + ctor() { + this._handlerArrayKeyPath = `${this._eventNamespace}_HandlerStore`; + }, + + setup(element) { + elementData(element, this._handlerArrayKeyPath, {}); + }, + + add(element, handleObj) { + const that = this; + const handler = function (e) { + that._handler(e); + }; + + eventsEngine.on(element, this._originalEventName, handleObj.selector, handler); + elementData(element, this._handlerArrayKeyPath)[handleObj.guid] = handler; + }, + + _handler(e) { + if (isTouchEvent(e) || devices.isSimulator()) { + return; + } + + fireEvent({ + type: this._eventName, + originalEvent: e, + delegateTarget: e.delegateTarget, + }); + }, + + remove(element, handleObj) { + const handler = elementData(element, this._handlerArrayKeyPath)[handleObj.guid]; + // @ts-expect-error + eventsEngine.off(element, this._originalEventName, handleObj.selector, handler); + }, + + teardown(element) { + removeData(element, this._handlerArrayKeyPath); + }, + +}); + +const HoverStart = Hover.inherit({ + + ctor() { + this._eventNamespace = HOVERSTART_NAMESPACE; + this._eventName = HOVERSTART; + this._originalEventName = POINTERENTER_NAMESPACED_EVENT_NAME; + this.callBase(); + }, + + _handler(e) { + const pointers = e.pointers || []; + if (!pointers.length) { + this.callBase(e); + } + }, + +}); + +const HoverEnd = Hover.inherit({ + + ctor() { + this._eventNamespace = HOVEREND_NAMESPACE; + this._eventName = HOVEREND; + this._originalEventName = POINTERLEAVE_NAMESPACED_EVENT_NAME; + this.callBase(); + }, + +}); + +registerEvent(HOVERSTART, new HoverStart()); +registerEvent(HOVEREND, new HoverEnd()); + +export { + HOVEREND as end, + HOVERSTART as start, +}; diff --git a/packages/devextreme/js/__internal/events/m_pointer.ts b/packages/devextreme/js/__internal/events/m_pointer.ts new file mode 100644 index 000000000000..7b1f09b1aae5 --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_pointer.ts @@ -0,0 +1,62 @@ +import GlobalConfig from '@js/core/config'; +import devices from '@js/core/devices'; +import { each } from '@js/core/utils/iterator'; +import * as support from '@js/core/utils/support'; +import registerEvent from '@js/events/core/event_registrator'; +import MouseStrategy from '@js/events/pointer/mouse'; +import MouseAndTouchStrategy from '@js/events/pointer/mouse_and_touch'; +import TouchStrategy from '@js/events/pointer/touch'; + +const getStrategy = (support, { tablet, phone }) => { + const pointerEventStrategy = getStrategyFromGlobalConfig(); + + if (pointerEventStrategy) { + return pointerEventStrategy; + } + + if (support.touch && !(tablet || phone)) { + return MouseAndTouchStrategy; + } + + if (support.touch) { + return TouchStrategy; + } + + return MouseStrategy; +}; +// @ts-expect-error +const EventStrategy = getStrategy(support, devices.real()); + +each(EventStrategy.map, (pointerEvent, originalEvents) => { + registerEvent(pointerEvent, new EventStrategy(pointerEvent, originalEvents)); +}); + +const pointer = { + down: 'dxpointerdown', + up: 'dxpointerup', + move: 'dxpointermove', + cancel: 'dxpointercancel', + enter: 'dxpointerenter', + leave: 'dxpointerleave', + over: 'dxpointerover', + out: 'dxpointerout', +}; + +function getStrategyFromGlobalConfig() { + const eventStrategyName = GlobalConfig().pointerEventStrategy; + + return { + 'mouse-and-touch': MouseAndTouchStrategy, + touch: TouchStrategy, + mouse: MouseStrategy, + // @ts-expect-error + }[eventStrategyName]; +} + +/// #DEBUG +// @ts-expect-error +pointer.getStrategy = getStrategy; + +/// #ENDDEBUG + +export default pointer; diff --git a/packages/devextreme/js/__internal/events/m_remove.ts b/packages/devextreme/js/__internal/events/m_remove.ts new file mode 100644 index 000000000000..7db6f04f4725 --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_remove.ts @@ -0,0 +1,27 @@ +import { beforeCleanData } from '@js/core/element_data'; +import $ from '@js/core/renderer'; +import registerEvent from '@js/events/core/event_registrator'; +import eventsEngine from '@js/events/core/events_engine'; + +export const removeEvent = 'dxremove'; +const eventPropName = 'dxRemoveEvent'; + +beforeCleanData((elements) => { + elements = [].slice.call(elements); + for (let i = 0; i < elements.length; i++) { + const $element = $(elements[i]); + // @ts-expect-error + if ($element.prop(eventPropName)) { + $element[0][eventPropName] = null; + // @ts-expect-error + eventsEngine.triggerHandler($element, removeEvent); + } + } +}); + +registerEvent(removeEvent, { + noBubble: true, + setup(element) { + $(element).prop(eventPropName, true); + }, +}); diff --git a/packages/devextreme/js/__internal/events/m_short.ts b/packages/devextreme/js/__internal/events/m_short.ts new file mode 100644 index 000000000000..4ef502a5058f --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_short.ts @@ -0,0 +1,136 @@ +import eventsEngine from '@js/events/core/events_engine'; +import KeyboardProcessor from '@js/events/core/keyboard_processor'; +import { addNamespace as pureAddNamespace } from '@js/events/utils/index'; + +function addNamespace(event, namespace) { + return namespace ? pureAddNamespace(event, namespace) : event; +} + +function executeAction(action, args) { + return typeof action === 'function' ? action(args) : action.execute(args); +} + +export const active = { + on: ($el, active, inactive, opts) => { + const { + selector, showTimeout, hideTimeout, namespace, + } = opts; + + eventsEngine.on( + $el, + addNamespace('dxactive', namespace), + selector, + { timeout: showTimeout }, + // @ts-expect-error + (event) => executeAction(active, { event, element: event.currentTarget }), + ); + eventsEngine.on( + $el, + addNamespace('dxinactive', namespace), + selector, + { timeout: hideTimeout }, + // @ts-expect-error + (event) => executeAction(inactive, { event, element: event.currentTarget }), + ); + }, + + off: ($el, { namespace, selector }) => { + eventsEngine.off($el, addNamespace('dxactive', namespace), selector); + eventsEngine.off($el, addNamespace('dxinactive', namespace), selector); + }, +}; + +export const resize = { + on: ($el, resize, { namespace }: any = {}) => { + eventsEngine.on($el, addNamespace('dxresize', namespace), resize); + }, + off: ($el, { namespace }: any = {}) => { + eventsEngine.off($el, addNamespace('dxresize', namespace)); + }, +}; + +export const hover = { + on: ($el, start, end, { selector, namespace }) => { + eventsEngine.on($el, addNamespace('dxhoverend', namespace), selector, (event) => end(event)); + eventsEngine.on( + $el, + addNamespace('dxhoverstart', namespace), + selector, + (event) => executeAction(start, { element: event.target, event }), + ); + }, + + off: ($el, { selector, namespace }) => { + eventsEngine.off($el, addNamespace('dxhoverstart', namespace), selector); + eventsEngine.off($el, addNamespace('dxhoverend', namespace), selector); + }, +}; + +export const visibility = { + on: ($el, shown, hiding, { namespace }) => { + eventsEngine.on($el, addNamespace('dxhiding', namespace), hiding); + eventsEngine.on($el, addNamespace('dxshown', namespace), shown); + }, + + off: ($el, { namespace }) => { + eventsEngine.off($el, addNamespace('dxhiding', namespace)); + eventsEngine.off($el, addNamespace('dxshown', namespace)); + }, +}; + +export const focus = { + on: ($el, focusIn, focusOut, { namespace }) => { + eventsEngine.on($el, addNamespace('focusin', namespace), focusIn); + eventsEngine.on($el, addNamespace('focusout', namespace), focusOut); + }, + + off: ($el, { namespace }) => { + eventsEngine.off($el, addNamespace('focusin', namespace)); + eventsEngine.off($el, addNamespace('focusout', namespace)); + }, + // @ts-expect-error + trigger: ($el) => eventsEngine.trigger($el, 'focus'), +}; + +export const dxClick = { + on: ($el, click, { namespace }: any = {}) => { + eventsEngine.on($el, addNamespace('dxclick', namespace), click); + }, + off: ($el, { namespace }: any = {}) => { + eventsEngine.off($el, addNamespace('dxclick', namespace)); + }, +}; + +export const click = { + on: ($el, click, { namespace }: any = {}) => { + eventsEngine.on($el, addNamespace('click', namespace), click); + }, + off: ($el, { namespace }: any = {}) => { + eventsEngine.off($el, addNamespace('click', namespace)); + }, +}; + +let index = 0; +const keyboardProcessors = {}; +const generateListenerId = () => `keyboardProcessorId${index++}`; + +export const keyboard = { + on: (element, focusTarget, handler) => { + const listenerId = generateListenerId(); + + keyboardProcessors[listenerId] = new KeyboardProcessor({ element, focusTarget, handler }); + + return listenerId; + }, + + off: (listenerId) => { + if (listenerId && keyboardProcessors[listenerId]) { + keyboardProcessors[listenerId].dispose(); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete keyboardProcessors[listenerId]; + } + }, + + // NOTE: For tests + _getProcessor: (listenerId) => keyboardProcessors[listenerId], +}; diff --git a/packages/devextreme/js/__internal/events/m_swipe.ts b/packages/devextreme/js/__internal/events/m_swipe.ts new file mode 100644 index 000000000000..fbea0dddd200 --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_swipe.ts @@ -0,0 +1,180 @@ +import { getHeight, getWidth } from '@js/core/utils/size'; +import registerEmitter from '@js/events/core/emitter_registrator'; +import GestureEmitter from '@js/events/gesture/emitter.gesture'; +import { eventData } from '@js/events/utils/index'; + +const SWIPE_START_EVENT = 'dxswipestart'; +const SWIPE_EVENT = 'dxswipe'; +const SWIPE_END_EVENT = 'dxswipeend'; + +const HorizontalStrategy = { + defaultItemSizeFunc() { + return getWidth(this.getElement()); + }, + + getBounds() { + return [ + this._maxLeftOffset, + this._maxRightOffset, + ]; + }, + + calcOffsetRatio(e) { + const endEventData = eventData(e); + return (endEventData.x - (this._savedEventData && this._savedEventData.x || 0)) / this._itemSizeFunc().call(this, e); + }, + + isFastSwipe(e) { + const endEventData = eventData(e); + return this.FAST_SWIPE_SPEED_LIMIT * Math.abs(endEventData.x - this._tickData.x) >= (endEventData.time - this._tickData.time); + }, +}; + +const VerticalStrategy = { + defaultItemSizeFunc() { + return getHeight(this.getElement()); + }, + + getBounds() { + return [ + this._maxTopOffset, + this._maxBottomOffset, + ]; + }, + + calcOffsetRatio(e) { + const endEventData = eventData(e); + return (endEventData.y - (this._savedEventData && this._savedEventData.y || 0)) / this._itemSizeFunc().call(this, e); + }, + + isFastSwipe(e) { + const endEventData = eventData(e); + return this.FAST_SWIPE_SPEED_LIMIT * Math.abs(endEventData.y - this._tickData.y) >= (endEventData.time - this._tickData.time); + }, +}; + +const STRATEGIES = { + horizontal: HorizontalStrategy, + vertical: VerticalStrategy, +}; + +const SwipeEmitter = GestureEmitter.inherit({ + + TICK_INTERVAL: 300, + FAST_SWIPE_SPEED_LIMIT: 10, + + ctor(element) { + this.callBase(element); + + this.direction = 'horizontal'; + this.elastic = true; + }, + + _getStrategy() { + return STRATEGIES[this.direction]; + }, + + _defaultItemSizeFunc() { + return this._getStrategy().defaultItemSizeFunc.call(this); + }, + + _itemSizeFunc() { + return this.itemSizeFunc || this._defaultItemSizeFunc; + }, + + _init(e) { + this._tickData = eventData(e); + }, + + _start(e) { + this._savedEventData = eventData(e); + + e = this._fireEvent(SWIPE_START_EVENT, e); + + if (!e.cancel) { + this._maxLeftOffset = e.maxLeftOffset; + this._maxRightOffset = e.maxRightOffset; + this._maxTopOffset = e.maxTopOffset; + this._maxBottomOffset = e.maxBottomOffset; + } + }, + + _move(e) { + const strategy = this._getStrategy(); + const moveEventData = eventData(e); + let offset = strategy.calcOffsetRatio.call(this, e); + + offset = this._fitOffset(offset, this.elastic); + + if (moveEventData.time - this._tickData.time > this.TICK_INTERVAL) { + this._tickData = moveEventData; + } + + this._fireEvent(SWIPE_EVENT, e, { + offset, + }); + + if (e.cancelable !== false) { + e.preventDefault(); + } + }, + + _end(e) { + const strategy = this._getStrategy(); + const offsetRatio = strategy.calcOffsetRatio.call(this, e); + const isFast = strategy.isFastSwipe.call(this, e); + let startOffset = offsetRatio; + let targetOffset = this._calcTargetOffset(offsetRatio, isFast); + + startOffset = this._fitOffset(startOffset, this.elastic); + targetOffset = this._fitOffset(targetOffset, false); + + this._fireEvent(SWIPE_END_EVENT, e, { + offset: startOffset, + targetOffset, + }); + }, + + _fitOffset(offset, elastic) { + const strategy = this._getStrategy(); + const bounds = strategy.getBounds.call(this); + + if (offset < -bounds[0]) { + return elastic ? (-2 * bounds[0] + offset) / 3 : -bounds[0]; + } + + if (offset > bounds[1]) { + return elastic ? (2 * bounds[1] + offset) / 3 : bounds[1]; + } + + return offset; + }, + + _calcTargetOffset(offsetRatio, isFast) { + let result; + if (isFast) { + result = Math.ceil(Math.abs(offsetRatio)); + if (offsetRatio < 0) { + result = -result; + } + } else { + result = Math.round(offsetRatio); + } + return result; + }, +}); + +registerEmitter({ + emitter: SwipeEmitter, + events: [ + SWIPE_START_EVENT, + SWIPE_EVENT, + SWIPE_END_EVENT, + ], +}); + +export { + SWIPE_END_EVENT as end, + SWIPE_START_EVENT as start, + SWIPE_EVENT as swipe, +}; diff --git a/packages/devextreme/js/__internal/events/m_transform.ts b/packages/devextreme/js/__internal/events/m_transform.ts new file mode 100644 index 000000000000..6f3ae247bc28 --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_transform.ts @@ -0,0 +1,172 @@ +import * as iteratorUtils from '@js/core/utils/iterator'; +import { fitIntoRange, sign as mathSign } from '@js/core/utils/math'; +import Emitter from '@js/events/core/emitter'; +import registerEmitter from '@js/events/core/emitter_registrator'; +import { hasTouches } from '@js/events/utils/index'; + +interface EventAlias { + name: string; + args: any; +} + +const DX_PREFIX = 'dx'; + +const TRANSFORM = 'transform'; +const TRANSLATE = 'translate'; +const PINCH = 'pinch'; +const ROTATE = 'rotate'; + +const START_POSTFIX = 'start'; +const UPDATE_POSTFIX = ''; +const END_POSTFIX = 'end'; + +const eventAliases: EventAlias[] = []; +const addAlias = function (eventName, eventArgs) { + eventAliases.push({ + name: eventName, + args: eventArgs, + }); +}; + +addAlias(TRANSFORM, { + scale: true, + deltaScale: true, + rotation: true, + deltaRotation: true, + translation: true, + deltaTranslation: true, +}); + +addAlias(TRANSLATE, { + translation: true, + deltaTranslation: true, +}); + +addAlias(PINCH, { + scale: true, + deltaScale: true, +}); + +addAlias(ROTATE, { + rotation: true, + deltaRotation: true, +}); + +const getVector = function (first, second) { + return { + x: second.pageX - first.pageX, + y: -second.pageY + first.pageY, + centerX: (second.pageX + first.pageX) * 0.5, + centerY: (second.pageY + first.pageY) * 0.5, + }; +}; + +const getEventVector = function (e) { + const { pointers } = e; + + return getVector(pointers[0], pointers[1]); +}; + +const getDistance = function (vector) { + return Math.sqrt(vector.x * vector.x + vector.y * vector.y); +}; + +const getScale = function (firstVector, secondVector) { + return getDistance(firstVector) / getDistance(secondVector); +}; + +const getRotation = function (firstVector, secondVector) { + const scalarProduct = firstVector.x * secondVector.x + firstVector.y * secondVector.y; + const distanceProduct = getDistance(firstVector) * getDistance(secondVector); + + if (distanceProduct === 0) { + return 0; + } + + const sign = mathSign(firstVector.x * secondVector.y - secondVector.x * firstVector.y); + const angle = Math.acos(fitIntoRange(scalarProduct / distanceProduct, -1, 1)); + + return sign * angle; +}; + +const getTranslation = function (firstVector, secondVector) { + return { + x: firstVector.centerX - secondVector.centerX, + y: firstVector.centerY - secondVector.centerY, + }; +}; + +const TransformEmitter = Emitter.inherit({ + + validatePointers(e) { + return hasTouches(e) > 1; + }, + + start(e) { + this._accept(e); + + const startVector = getEventVector(e); + this._startVector = startVector; + this._prevVector = startVector; + + this._fireEventAliases(START_POSTFIX, e); + }, + + move(e) { + const currentVector = getEventVector(e); + const eventArgs = this._getEventArgs(currentVector); + + this._fireEventAliases(UPDATE_POSTFIX, e, eventArgs); + this._prevVector = currentVector; + }, + + end(e) { + const eventArgs = this._getEventArgs(this._prevVector); + this._fireEventAliases(END_POSTFIX, e, eventArgs); + }, + + _getEventArgs(vector) { + return { + scale: getScale(vector, this._startVector), + deltaScale: getScale(vector, this._prevVector), + rotation: getRotation(vector, this._startVector), + deltaRotation: getRotation(vector, this._prevVector), + translation: getTranslation(vector, this._startVector), + deltaTranslation: getTranslation(vector, this._prevVector), + }; + }, + + _fireEventAliases(eventPostfix, originalEvent, eventArgs) { + eventArgs = eventArgs || {}; + + iteratorUtils.each(eventAliases, (_, eventAlias) => { + const args = {}; + iteratorUtils.each(eventAlias.args, (name) => { + if (name in eventArgs) { + args[name] = eventArgs[name]; + } + }); + + this._fireEvent(DX_PREFIX + eventAlias.name + eventPostfix, originalEvent, args); + }); + }, + +}); + +const eventNames = eventAliases.reduce((result: string[], eventAlias) => { + [START_POSTFIX, UPDATE_POSTFIX, END_POSTFIX].forEach((eventPostfix) => { + result.push(DX_PREFIX + eventAlias.name + eventPostfix); + }); + return result; +}, []); + +registerEmitter({ + emitter: TransformEmitter, + events: eventNames, +}); +const exportNames: Record = {}; +iteratorUtils.each(eventNames, (_, eventName: string) => { + exportNames[eventName.substring(DX_PREFIX.length)] = eventName; +}); + +export { exportNames }; diff --git a/packages/devextreme/js/__internal/events/m_visibility_change.ts b/packages/devextreme/js/__internal/events/m_visibility_change.ts new file mode 100644 index 000000000000..73649aaeb138 --- /dev/null +++ b/packages/devextreme/js/__internal/events/m_visibility_change.ts @@ -0,0 +1,28 @@ +import $ from '@js/core/renderer'; +import eventsEngine from '@js/events/core/events_engine'; + +const triggerVisibilityChangeEvent = function (eventName) { + const VISIBILITY_CHANGE_SELECTOR = '.dx-visibility-change-handler'; + + return function (element) { + const $element = $(element || 'body'); + + const changeHandlers = $element.filter(VISIBILITY_CHANGE_SELECTOR) + // @ts-expect-error + .add($element.find(VISIBILITY_CHANGE_SELECTOR)); + + for (let i = 0; i < changeHandlers.length; i++) { + eventsEngine.triggerHandler(changeHandlers[i], eventName); + } + }; +}; + +export const triggerShownEvent = triggerVisibilityChangeEvent('dxshown'); +export const triggerHidingEvent = triggerVisibilityChangeEvent('dxhiding'); +export const triggerResizeEvent = triggerVisibilityChangeEvent('dxresize'); + +export default { + triggerHidingEvent, + triggerResizeEvent, + triggerShownEvent, +}; diff --git a/packages/devextreme/js/__internal/events/pointer/m_base.ts b/packages/devextreme/js/__internal/events/pointer/m_base.ts new file mode 100644 index 000000000000..9f09bc77971f --- /dev/null +++ b/packages/devextreme/js/__internal/events/pointer/m_base.ts @@ -0,0 +1,114 @@ +import Class from '@js/core/class'; +import domAdapter from '@js/core/dom_adapter'; +import browser from '@js/core/utils/browser'; +import eventsEngine from '@js/events/core/events_engine'; +import { getEventTarget } from '@js/events/utils/event_target'; +import { addNamespace, eventSource, fireEvent } from '@js/events/utils/index'; + +const POINTER_EVENTS_NAMESPACE = 'dxPointerEvents'; + +const BaseStrategy = Class.inherit({ + + ctor(eventName, originalEvents) { + this._eventName = eventName; + this._originalEvents = addNamespace(originalEvents, POINTER_EVENTS_NAMESPACE); + this._handlerCount = 0; + this.noBubble = this._isNoBubble(); + }, + + _isNoBubble() { + const eventName = this._eventName; + + return eventName === 'dxpointerenter' + || eventName === 'dxpointerleave'; + }, + + _handler(e) { + const delegateTarget = this._getDelegateTarget(e); + + const event = { + type: this._eventName, + pointerType: e.pointerType || eventSource(e), + originalEvent: e, + delegateTarget, + // NOTE: TimeStamp normalization (FF bug #238041) (T277118) + timeStamp: browser.mozilla ? new Date().getTime() : e.timeStamp, + }; + + const target = getEventTarget(e); + // @ts-expect-error + event.target = target; + + return this._fireEvent(event); + }, + + _getDelegateTarget(e) { + let delegateTarget; + + if (this.noBubble) { + delegateTarget = e.delegateTarget; + } + + return delegateTarget; + }, + + _fireEvent(args) { + return fireEvent(args); + }, + + _setSelector(handleObj) { + this._selector = this.noBubble && handleObj ? handleObj.selector : null; + }, + + _getSelector() { + return this._selector; + }, + + setup() { + return true; + }, + + add(element, handleObj) { + if (this._handlerCount <= 0 || this.noBubble) { + element = this.noBubble ? element : domAdapter.getDocument(); + this._setSelector(handleObj); + + const that = this; + eventsEngine.on(element, this._originalEvents, this._getSelector(), (e) => { + that._handler(e); + }); + } + + if (!this.noBubble) { + this._handlerCount++; + } + }, + + remove(handleObj) { + this._setSelector(handleObj); + + if (!this.noBubble) { + this._handlerCount--; + } + }, + + teardown(element) { + if (this._handlerCount && !this.noBubble) { + return; + } + + element = this.noBubble ? element : domAdapter.getDocument(); + + if (this._originalEvents !== `.${POINTER_EVENTS_NAMESPACE}`) { + eventsEngine.off(element, this._originalEvents, this._getSelector()); + } + }, + + dispose(element) { + element = this.noBubble ? element : domAdapter.getDocument(); + + eventsEngine.off(element, this._originalEvents); + }, +}); + +export default BaseStrategy; diff --git a/packages/devextreme/js/__internal/events/pointer/m_mouse.ts b/packages/devextreme/js/__internal/events/pointer/m_mouse.ts new file mode 100644 index 000000000000..87cf81e5e738 --- /dev/null +++ b/packages/devextreme/js/__internal/events/pointer/m_mouse.ts @@ -0,0 +1,62 @@ +import { extend } from '@js/core/utils/extend'; +import BaseStrategy from '@js/events/pointer/base'; +import Observer from '@js/events/pointer/observer'; + +/* eslint-disable spellcheck/spell-checker */ +const eventMap = { + dxpointerdown: 'mousedown', + dxpointermove: 'mousemove', + dxpointerup: 'mouseup', + dxpointercancel: '', + dxpointerover: 'mouseover', + dxpointerout: 'mouseout', + dxpointerenter: 'mouseenter', + dxpointerleave: 'mouseleave', +}; + +const normalizeMouseEvent = function (e) { + e.pointerId = 1; + + return { + pointers: observer.pointers(), + pointerId: 1, + }; +}; + +let observer; +let activated = false; +const activateStrategy = function () { + if (activated) { + return; + } + // @ts-expect-error + observer = new Observer(eventMap, () => true); + + activated = true; +}; + +const MouseStrategy = BaseStrategy.inherit({ + + ctor() { + this.callBase.apply(this, arguments); + + activateStrategy(); + }, + + _fireEvent(args) { + return this.callBase(extend(normalizeMouseEvent(args.originalEvent), args)); + }, + +}); +// @ts-expect-error +MouseStrategy.map = eventMap; +// @ts-expect-error +MouseStrategy.normalize = normalizeMouseEvent; +// @ts-expect-error +MouseStrategy.activate = activateStrategy; +// @ts-expect-error +MouseStrategy.resetObserver = function () { + observer.reset(); +}; + +export default MouseStrategy; diff --git a/packages/devextreme/js/__internal/events/pointer/m_mouse_and_touch.ts b/packages/devextreme/js/__internal/events/pointer/m_mouse_and_touch.ts new file mode 100644 index 000000000000..ca198b4a1bf0 --- /dev/null +++ b/packages/devextreme/js/__internal/events/pointer/m_mouse_and_touch.ts @@ -0,0 +1,88 @@ +import { extend } from '@js/core/utils/extend'; +import BaseStrategy from '@js/events/pointer/base'; +import MouseStrategy from '@js/events/pointer/mouse'; +import TouchStrategy from '@js/events/pointer/touch'; +import { isMouseEvent } from '@js/events/utils/index'; + +/* eslint-disable spellcheck/spell-checker */ +const eventMap = { + dxpointerdown: 'touchstart mousedown', + dxpointermove: 'touchmove mousemove', + dxpointerup: 'touchend mouseup', + dxpointercancel: 'touchcancel', + dxpointerover: 'mouseover', + dxpointerout: 'mouseout', + dxpointerenter: 'mouseenter', + dxpointerleave: 'mouseleave', +}; + +let activated = false; +const activateStrategy = function () { + if (activated) { + return; + } + // @ts-expect-error + MouseStrategy.activate(); + + activated = true; +}; + +const MouseAndTouchStrategy = BaseStrategy.inherit({ + + EVENT_LOCK_TIMEOUT: 100, + + ctor() { + this.callBase.apply(this, arguments); + + activateStrategy(); + }, + + _handler(e) { + const isMouse = isMouseEvent(e); + + if (!isMouse) { + this._skipNextEvents = true; + } + + if (isMouse && this._mouseLocked) { + return; + } + + if (isMouse && this._skipNextEvents) { + this._skipNextEvents = false; + this._mouseLocked = true; + + clearTimeout(this._unlockMouseTimer); + + const that = this; + this._unlockMouseTimer = setTimeout(() => { + that._mouseLocked = false; + }, this.EVENT_LOCK_TIMEOUT); + + return; + } + + return this.callBase(e); + }, + + _fireEvent(args) { + // @ts-expect-error + const normalizer = isMouseEvent(args.originalEvent) ? MouseStrategy.normalize : TouchStrategy.normalize; + + return this.callBase(extend(normalizer(args.originalEvent), args)); + }, + + dispose() { + this.callBase(); + this._skipNextEvents = false; + this._mouseLocked = false; + clearTimeout(this._unlockMouseTimer); + }, +}); + +// @ts-expect-error +MouseAndTouchStrategy.map = eventMap; +// @ts-expect-error +MouseAndTouchStrategy.resetObserver = MouseStrategy.resetObserver; + +export default MouseAndTouchStrategy; diff --git a/packages/devextreme/js/__internal/events/pointer/m_observer.ts b/packages/devextreme/js/__internal/events/pointer/m_observer.ts new file mode 100644 index 000000000000..1a50a5f8f5cd --- /dev/null +++ b/packages/devextreme/js/__internal/events/pointer/m_observer.ts @@ -0,0 +1,69 @@ +import domAdapter from '@js/core/dom_adapter'; +import { each } from '@js/core/utils/iterator'; +import readyCallbacks from '@js/core/utils/ready_callbacks'; + +const addEventsListener = function (events, handler) { + readyCallbacks.add(() => { + events + .split(' ') + .forEach((event) => { + // @ts-expect-error + domAdapter.listen(domAdapter.getDocument(), event, handler, true); + }); + }); +}; + +const Observer = function (eventMap, pointerEquals, onPointerAdding) { + onPointerAdding = onPointerAdding || function () { }; + + let pointers: any = []; + + const getPointerIndex = function (e) { + let index = -1; + + each(pointers, (i: any, pointer) => { + if (!pointerEquals(e, pointer)) { + return true; + } + + index = i; + return false; + }); + + return index; + }; + + const addPointer = function (e) { + if (getPointerIndex(e) === -1) { + onPointerAdding(e); + pointers.push(e); + } + }; + + const removePointer = function (e) { + const index = getPointerIndex(e); + if (index > -1) { + pointers.splice(index, 1); + } + }; + + const updatePointer = function (e) { + pointers[getPointerIndex(e)] = e; + }; + + /* eslint-disable spellcheck/spell-checker */ + addEventsListener(eventMap.dxpointerdown, addPointer); + addEventsListener(eventMap.dxpointermove, updatePointer); + addEventsListener(eventMap.dxpointerup, removePointer); + addEventsListener(eventMap.dxpointercancel, removePointer); + + this.pointers = function () { + return pointers; + }; + + this.reset = function () { + pointers = []; + }; +}; + +export default Observer; diff --git a/packages/devextreme/js/__internal/events/pointer/m_touch.ts b/packages/devextreme/js/__internal/events/pointer/m_touch.ts new file mode 100644 index 000000000000..102f75060d26 --- /dev/null +++ b/packages/devextreme/js/__internal/events/pointer/m_touch.ts @@ -0,0 +1,68 @@ +import devices from '@js/core/devices'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import BaseStrategy from '@js/events/pointer/base'; + +/* eslint-disable spellcheck/spell-checker */ +const eventMap = { + dxpointerdown: 'touchstart', + dxpointermove: 'touchmove', + dxpointerup: 'touchend', + dxpointercancel: 'touchcancel', + dxpointerover: '', + dxpointerout: '', + dxpointerenter: '', + dxpointerleave: '', +}; + +const normalizeTouchEvent = function (e) { + const pointers: any = []; + + each(e.touches, (_, touch) => { + pointers.push(extend({ + pointerId: touch.identifier, + }, touch)); + }); + + return { + pointers, + pointerId: e.changedTouches[0].identifier, + }; +}; + +const skipTouchWithSameIdentifier = function (pointerEvent) { + return devices.real().platform === 'ios' && (pointerEvent === 'dxpointerdown' || pointerEvent === 'dxpointerup'); +}; + +const TouchStrategy = BaseStrategy.inherit({ + + ctor() { + this.callBase.apply(this, arguments); + this._pointerId = 0; + }, + + _handler(e) { + if (skipTouchWithSameIdentifier(this._eventName)) { + const touch = e.changedTouches[0]; + + if (this._pointerId === touch.identifier && this._pointerId !== 0) { + return; + } + + this._pointerId = touch.identifier; + } + + return this.callBase.apply(this, arguments); + }, + + _fireEvent(args) { + return this.callBase(extend(normalizeTouchEvent(args.originalEvent), args)); + }, + +}); +// @ts-expect-error +TouchStrategy.map = eventMap; +// @ts-expect-error +TouchStrategy.normalize = normalizeTouchEvent; + +export default TouchStrategy; diff --git a/packages/devextreme/js/__internal/events/utils/index.ts b/packages/devextreme/js/__internal/events/utils/index.ts new file mode 100644 index 000000000000..407f3dced19d --- /dev/null +++ b/packages/devextreme/js/__internal/events/utils/index.ts @@ -0,0 +1,219 @@ +import $ from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import { each } from '@js/core/utils/iterator'; +import eventsEngine from '@js/events/core/events_engine'; +import { focused } from '@js/ui/widget/selectors'; + +import mappedAddNamespace from './m_add_namespace'; +/* eslint-disable spellcheck/spell-checker */ +const KEY_MAP = { + backspace: 'backspace', + tab: 'tab', + enter: 'enter', + escape: 'escape', + pageup: 'pageUp', + pagedown: 'pageDown', + end: 'end', + home: 'home', + arrowleft: 'leftArrow', + arrowup: 'upArrow', + arrowright: 'rightArrow', + arrowdown: 'downArrow', + delete: 'del', + ' ': 'space', + f: 'F', + a: 'A', + '*': 'asterisk', + '-': 'minus', + alt: 'alt', + control: 'control', + shift: 'shift', +}; + +const LEGACY_KEY_CODES = { + // iOS 10.2 and lower didn't supports KeyboardEvent.key + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 27: 'escape', + 33: 'pageUp', + 34: 'pageDown', + 35: 'end', + 36: 'home', + 37: 'leftArrow', + 38: 'upArrow', + 39: 'rightArrow', + 40: 'downArrow', + 46: 'del', + 32: 'space', + 70: 'F', + 65: 'A', + 106: 'asterisk', + 109: 'minus', + 189: 'minus', + 173: 'minus', + 16: 'shift', + 17: 'control', + 18: 'alt', +}; + +const EVENT_SOURCES_REGEX = { + dx: /^dx/i, + mouse: /(mouse|wheel)/i, + touch: /^touch/i, + keyboard: /^key/i, + pointer: /^(ms)?pointer/i, +}; + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +export const eventSource = ({ type }) => { + let result = 'other'; + /* eslint-disable @typescript-eslint/no-invalid-void-type */ + // eslint-disable-next-line consistent-return + each(EVENT_SOURCES_REGEX, function (key): boolean | void { + // eslint-disable-next-line @typescript-eslint/no-invalid-this + if (this.test(type)) { + result = key; + + return false; + } + }); + + return result; +}; + +let fixMethod = (e) => e; +// @ts-expect-error +const getEvent = (originalEvent) => eventsEngine.Event(originalEvent, originalEvent); +// @ts-expect-error +const copyEvent = (originalEvent) => fixMethod(getEvent(originalEvent), originalEvent); + +const isDxEvent = (e) => eventSource(e) === 'dx'; +const isNativeMouseEvent = (e) => eventSource(e) === 'mouse'; +const isNativeTouchEvent = (e) => eventSource(e) === 'touch'; + +export const isPointerEvent = (e) => eventSource(e) === 'pointer'; + +export const isMouseEvent = (e) => isNativeMouseEvent(e) + || ((isPointerEvent(e) || isDxEvent(e)) && e.pointerType === 'mouse'); + +export const isDxMouseWheelEvent = (e) => e && e.type === 'dxmousewheel'; + +export const isTouchEvent = (e) => isNativeTouchEvent(e) + || ((isPointerEvent(e) || isDxEvent(e)) && e.pointerType === 'touch'); + +export const isKeyboardEvent = (e) => eventSource(e) === 'keyboard'; + +export const isFakeClickEvent = ({ + screenX, + offsetX, + pageX, +}) => screenX === 0 && !offsetX && pageX === 0; + +export const eventData = ({ pageX, pageY, timeStamp }) => ({ + x: pageX, + y: pageY, + time: timeStamp, +}); + +export const eventDelta = (from, to) => ({ + x: to.x - from.x, + y: to.y - from.y, + time: to.time - from.time || 1, +}); + +export const hasTouches = (e) => { + const { originalEvent, pointers } = e; + + if (isNativeTouchEvent(e)) { + return (originalEvent.touches || []).length; + } + + if (isDxEvent(e)) { + return (pointers || []).length; + } + + return 0; +}; + +// TODO: for tests +let skipEvents = false; +export const forceSkipEvents = () => { skipEvents = true; }; +export const stopEventsSkipping = () => { skipEvents = false; }; +// eslint-disable-next-line consistent-return +export const needSkipEvent = (e) => { + // TODO: for tests + if (skipEvents) { + return true; + } + + // TODO: this checking used in swipeable first move handler. is it correct? + const { target } = e; + const $target = $(target); + const isContentEditable = target?.isContentEditable || target?.hasAttribute('contenteditable'); + const touchInEditable = $target.is('input, textarea, select') || isContentEditable; + + if (isDxMouseWheelEvent(e)) { + const isTextArea = $target.is('textarea') && $target.hasClass('dx-texteditor-input'); + + if (isTextArea || isContentEditable) { + return false; + } + + const isInputFocused = $target.is('input[type=\'number\'], textarea, select') && $target.is(':focus'); + + return isInputFocused; + } + + if (isMouseEvent(e)) { + return touchInEditable || e.which > 1; // only left mouse button + } + + if (isTouchEvent(e)) { + return touchInEditable && focused($target); + } +}; + +export const setEventFixMethod = (func) => { fixMethod = func; }; + +export const createEvent = (originalEvent, args) => { + const event = copyEvent(originalEvent); + + if (args) { + extend(event, args); + } + + return event; +}; + +export const fireEvent = (props) => { + const { originalEvent, delegateTarget } = props; + const event = createEvent(originalEvent, props); + // @ts-expect-error + eventsEngine.trigger(delegateTarget || event.target, event); + + return event; +}; + +export const normalizeKeyName = ({ key, which }) => { + const normalizedKey = KEY_MAP[key?.toLowerCase()] || key; + const normalizedKeyFromWhich = LEGACY_KEY_CODES[which]; + if (normalizedKeyFromWhich && normalizedKey === key) { + return normalizedKeyFromWhich; + } + + if (!normalizedKey && which) { + return String.fromCharCode(which); + } + + return normalizedKey; +}; + +export const getChar = ({ key, which }) => key || String.fromCharCode(which); + +export const addNamespace = mappedAddNamespace; + +export const isCommandKeyPressed = ({ ctrlKey, metaKey }) => ctrlKey || metaKey; diff --git a/packages/devextreme/js/__internal/events/utils/m_add_namespace.ts b/packages/devextreme/js/__internal/events/utils/m_add_namespace.ts new file mode 100644 index 000000000000..f1f64eb8702c --- /dev/null +++ b/packages/devextreme/js/__internal/events/utils/m_add_namespace.ts @@ -0,0 +1,21 @@ +import errors from '@js/core/errors'; + +const addNamespace = (eventNames, namespace) => { + if (!namespace) { + throw errors.Error('E0017'); + } + + if (Array.isArray(eventNames)) { + return eventNames + .map((eventName) => addNamespace(eventName, namespace)) + .join(' '); + } + + if (eventNames.indexOf(' ') !== -1) { + return addNamespace(eventNames.split(/\s+/g), namespace); + } + + return `${eventNames}.${namespace}`; +}; + +export default addNamespace; diff --git a/packages/devextreme/js/__internal/events/utils/m_event_nodes_disposing.ts b/packages/devextreme/js/__internal/events/utils/m_event_nodes_disposing.ts new file mode 100644 index 000000000000..6d987c8aed01 --- /dev/null +++ b/packages/devextreme/js/__internal/events/utils/m_event_nodes_disposing.ts @@ -0,0 +1,20 @@ +import eventsEngine from '@js/events/core/events_engine'; +import { removeEvent } from '@js/events/remove'; + +function nodesByEvent(event) { + return event && [ + event.target, + event.delegateTarget, + event.relatedTarget, + event.currentTarget, + ].filter((node) => !!node); +} + +export const subscribeNodesDisposing = (event, callback) => { + // @ts-expect-error + eventsEngine.one(nodesByEvent(event), removeEvent, callback); +}; + +export const unsubscribeNodesDisposing = (event, callback) => { + eventsEngine.off(nodesByEvent(event), removeEvent, callback); +}; diff --git a/packages/devextreme/js/__internal/events/utils/m_event_target.ts b/packages/devextreme/js/__internal/events/utils/m_event_target.ts new file mode 100644 index 000000000000..28224cf5f616 --- /dev/null +++ b/packages/devextreme/js/__internal/events/utils/m_event_target.ts @@ -0,0 +1,18 @@ +export const getEventTarget = (event) => { + const { originalEvent } = event; + + if (!originalEvent) { + return event.target; + } + + const isShadowDOMUsed = Boolean(originalEvent.target?.shadowRoot); + + if (!isShadowDOMUsed) { + return originalEvent.target; + } + + const path = originalEvent.path ?? originalEvent.composedPath?.(); + const target = path?.[0] ?? event.target; + + return target; +}; diff --git a/packages/devextreme/js/events/click.js b/packages/devextreme/js/events/click.js index 155d2e4abbe8..b11d13e812b5 100644 --- a/packages/devextreme/js/events/click.js +++ b/packages/devextreme/js/events/click.js @@ -1,128 +1,8 @@ -import $ from '../core/renderer'; -import eventsEngine from '../events/core/events_engine'; -import devices from '../core/devices'; -import domAdapter from '../core/dom_adapter'; -import { resetActiveElement } from '../core/utils/dom'; -import { requestAnimationFrame, cancelAnimationFrame } from '../animation/frame'; -import { addNamespace, fireEvent } from './utils/index'; -import { subscribeNodesDisposing, unsubscribeNodesDisposing } from './utils/event_nodes_disposing'; -import { getEventTarget } from './utils/event_target'; -import pointerEvents from './pointer'; -import Emitter from './core/emitter'; -import registerEmitter from './core/emitter_registrator'; - -const CLICK_EVENT_NAME = 'dxclick'; - -const misc = { requestAnimationFrame, cancelAnimationFrame }; - -let prevented = null; -let lastFiredEvent = null; - -const onNodeRemove = () => { - lastFiredEvent = null; -}; - -const clickHandler = function(e) { - const originalEvent = e.originalEvent; - const eventAlreadyFired = lastFiredEvent === originalEvent || originalEvent && originalEvent.DXCLICK_FIRED; - const leftButton = !e.which || e.which === 1; - - if(leftButton && !prevented && !eventAlreadyFired) { - if(originalEvent) { - originalEvent.DXCLICK_FIRED = true; - } - - unsubscribeNodesDisposing(lastFiredEvent, onNodeRemove); - lastFiredEvent = originalEvent; - subscribeNodesDisposing(lastFiredEvent, onNodeRemove); - - fireEvent({ - type: CLICK_EVENT_NAME, - originalEvent: e - }); - } -}; - -const ClickEmitter = Emitter.inherit({ - - ctor: function(element) { - this.callBase(element); - eventsEngine.on(this.getElement(), 'click', clickHandler); - }, - - start: function(e) { - prevented = null; - }, - - cancel: function() { - prevented = true; - }, - - dispose: function() { - eventsEngine.off(this.getElement(), 'click', clickHandler); - } -}); - - -// NOTE: fixes native click blur on slow devices -(function() { - const desktopDevice = devices.real().generic; - - if(!desktopDevice) { - let startTarget = null; - let blurPrevented = false; - - const isInput = function(element) { - return $(element).is('input, textarea, select, button ,:focus, :focus *'); - }; - - const pointerDownHandler = function(e) { - startTarget = e.target; - blurPrevented = e.isDefaultPrevented(); - }; - - const getTarget = function(e) { - const target = getEventTarget(e); - return $(target); - }; - - const clickHandler = function(e) { - const $target = getTarget(e); - - if(!blurPrevented && startTarget && !$target.is(startTarget) && !$(startTarget).is('label') && isInput($target)) { - resetActiveElement(); - } - - startTarget = null; - blurPrevented = false; - }; - - const NATIVE_CLICK_FIXER_NAMESPACE = 'NATIVE_CLICK_FIXER'; - const document = domAdapter.getDocument(); - eventsEngine.subscribeGlobal(document, addNamespace(pointerEvents.down, NATIVE_CLICK_FIXER_NAMESPACE), pointerDownHandler); - eventsEngine.subscribeGlobal(document, addNamespace('click', NATIVE_CLICK_FIXER_NAMESPACE), clickHandler); - } -})(); - - /** * @name UI Events.dxclick * @type eventType * @type_function_param1 event:event * @module events/click */ -registerEmitter({ - emitter: ClickEmitter, - bubble: true, - events: [ - CLICK_EVENT_NAME - ] -}); - -export { CLICK_EVENT_NAME as name }; -///#DEBUG -export { - misc -}; -///#ENDDEBUG +export * from '../__internal/events/m_click'; diff --git a/packages/devextreme/js/events/contextmenu.js b/packages/devextreme/js/events/contextmenu.js index 88dd9290add1..d8c4c5830d05 100644 --- a/packages/devextreme/js/events/contextmenu.js +++ b/packages/devextreme/js/events/contextmenu.js @@ -1,57 +1,3 @@ -import $ from '../core/renderer'; -import eventsEngine from '../events/core/events_engine'; -import { touch } from '../core/utils/support'; -import devices from '../core/devices'; -import Class from '../core/class'; -import registerEvent from './core/event_registrator'; -import { addNamespace, fireEvent, isMouseEvent } from './utils/index'; -import holdEvent from './hold'; - -const CONTEXTMENU_NAMESPACE = 'dxContexMenu'; - -const CONTEXTMENU_NAMESPACED_EVENT_NAME = addNamespace('contextmenu', CONTEXTMENU_NAMESPACE); -const HOLD_NAMESPACED_EVENT_NAME = addNamespace(holdEvent.name, CONTEXTMENU_NAMESPACE); - -const CONTEXTMENU_EVENT_NAME = 'dxcontextmenu'; - - -const ContextMenu = Class.inherit({ - - setup: function(element) { - const $element = $(element); - - eventsEngine.on($element, CONTEXTMENU_NAMESPACED_EVENT_NAME, this._contextMenuHandler.bind(this)); - - if(touch || devices.isSimulator()) { - eventsEngine.on($element, HOLD_NAMESPACED_EVENT_NAME, this._holdHandler.bind(this)); - } - }, - - _holdHandler: function(e) { - if(isMouseEvent(e) && !devices.isSimulator()) { - return; - } - - this._fireContextMenu(e); - }, - - _contextMenuHandler: function(e) { - this._fireContextMenu(e); - }, - - _fireContextMenu: function(e) { - return fireEvent({ - type: CONTEXTMENU_EVENT_NAME, - originalEvent: e - }); - }, - - teardown: function(element) { - eventsEngine.off(element, '.' + CONTEXTMENU_NAMESPACE); - } - -}); - /** * @name UI Events.dxcontextmenu * @type eventType @@ -59,6 +5,4 @@ const ContextMenu = Class.inherit({ * @module events/contextmenu */ -registerEvent(CONTEXTMENU_EVENT_NAME, new ContextMenu()); - -export const name = CONTEXTMENU_EVENT_NAME; +export * from '../__internal/events/m_contextmenu'; diff --git a/packages/devextreme/js/events/core/emitter.feedback.js b/packages/devextreme/js/events/core/emitter.feedback.js index 8ec2ab5f0e8d..f086130e30a5 100644 --- a/packages/devextreme/js/events/core/emitter.feedback.js +++ b/packages/devextreme/js/events/core/emitter.feedback.js @@ -1,181 +1 @@ -import Class from '../../core/class'; -import { noop, ensureDefined } from '../../core/utils/common'; -import { contains } from '../../core/utils/dom'; -import devices from '../../core/devices'; -import { isMouseEvent } from '../utils/index'; -import pointerEvents from '../pointer'; -import Emitter from './emitter'; -import registerEmitter from './emitter_registrator'; - -const ACTIVE_EVENT_NAME = 'dxactive'; -const INACTIVE_EVENT_NAME = 'dxinactive'; - -const ACTIVE_TIMEOUT = 30; -const INACTIVE_TIMEOUT = 400; - - -const FeedbackEvent = Class.inherit({ - - ctor: function(timeout, fire) { - this._timeout = timeout; - this._fire = fire; - }, - - start: function() { - const that = this; - - this._schedule(function() { - that.force(); - }); - }, - - _schedule: function(fn) { - this.stop(); - this._timer = setTimeout(fn, this._timeout); - }, - - stop: function() { - clearTimeout(this._timer); - }, - - force: function() { - if(this._fired) { - return; - } - - this.stop(); - this._fire(); - this._fired = true; - }, - - fired: function() { - return this._fired; - } - -}); - - -let activeFeedback; - -const FeedbackEmitter = Emitter.inherit({ - - ctor: function() { - this.callBase.apply(this, arguments); - - this._active = new FeedbackEvent(0, noop); - this._inactive = new FeedbackEvent(0, noop); - }, - - configure: function(data, eventName) { - switch(eventName) { - case ACTIVE_EVENT_NAME: - data.activeTimeout = data.timeout; - break; - case INACTIVE_EVENT_NAME: - data.inactiveTimeout = data.timeout; - break; - } - - this.callBase(data); - }, - - start: function(e) { - if(activeFeedback) { - const activeChildExists = contains(this.getElement().get(0), activeFeedback.getElement().get(0)); - const childJustActivated = !activeFeedback._active.fired(); - - if(activeChildExists && childJustActivated) { - this._cancel(); - return; - } - - activeFeedback._inactive.force(); - } - activeFeedback = this; - - this._initEvents(e); - this._active.start(); - }, - - _initEvents: function(e) { - const that = this; - - const eventTarget = this._getEmitterTarget(e); - - const mouseEvent = isMouseEvent(e); - const isSimulator = devices.isSimulator(); - const deferFeedback = isSimulator || !mouseEvent; - - const activeTimeout = ensureDefined(this.activeTimeout, ACTIVE_TIMEOUT); - const inactiveTimeout = ensureDefined(this.inactiveTimeout, INACTIVE_TIMEOUT); - - this._active = new FeedbackEvent(deferFeedback ? activeTimeout : 0, function() { - that._fireEvent(ACTIVE_EVENT_NAME, e, { target: eventTarget }); - }); - this._inactive = new FeedbackEvent(deferFeedback ? inactiveTimeout : 0, function() { - that._fireEvent(INACTIVE_EVENT_NAME, e, { target: eventTarget }); - activeFeedback = null; - }); - }, - - cancel: function(e) { - this.end(e); - }, - - end: function(e) { - const skipTimers = e.type !== pointerEvents.up; - - if(skipTimers) { - this._active.stop(); - } else { - this._active.force(); - } - - this._inactive.start(); - - if(skipTimers) { - this._inactive.force(); - } - }, - - dispose: function() { - this._active.stop(); - this._inactive.stop(); - - if(activeFeedback === this) { - activeFeedback = null; - } - - this.callBase(); - }, - - lockInactive: function() { - this._active.force(); - this._inactive.stop(); - activeFeedback = null; - this._cancel(); - - return this._inactive.force.bind(this._inactive); - } - -}); -FeedbackEmitter.lock = function(deferred) { - const lockInactive = activeFeedback ? activeFeedback.lockInactive() : noop; - - deferred.done(lockInactive); -}; - - -registerEmitter({ - emitter: FeedbackEmitter, - events: [ - ACTIVE_EVENT_NAME, - INACTIVE_EVENT_NAME - ] -}); - -export const lock = FeedbackEmitter.lock; -export { - ACTIVE_EVENT_NAME as active, - INACTIVE_EVENT_NAME as inactive -}; +export * from '../../__internal/events/core/m_emitter.feedback'; diff --git a/packages/devextreme/js/events/core/emitter.js b/packages/devextreme/js/events/core/emitter.js index 5405d560c360..223a042f3077 100644 --- a/packages/devextreme/js/events/core/emitter.js +++ b/packages/devextreme/js/events/core/emitter.js @@ -1,106 +1 @@ -import $ from '../../core/renderer'; -import { noop } from '../../core/utils/common'; -import Class from '../../core/class'; -import Callbacks from '../../core/utils/callbacks'; -import { extend } from '../../core/utils/extend'; -import { isDxMouseWheelEvent, hasTouches, fireEvent } from '../utils/index'; - -const Emitter = Class.inherit({ - - ctor: function(element) { - this._$element = $(element); - - this._cancelCallback = Callbacks(); - this._acceptCallback = Callbacks(); - }, - - getElement: function() { - return this._$element; - }, - - validate: function(e) { - return !isDxMouseWheelEvent(e); - }, - - validatePointers: function(e) { - return hasTouches(e) === 1; - }, - - allowInterruptionByMouseWheel: function() { - return true; - }, - - configure: function(data) { - extend(this, data); - }, - - addCancelCallback: function(callback) { - this._cancelCallback.add(callback); - }, - - removeCancelCallback: function() { - this._cancelCallback.empty(); - }, - - _cancel: function(e) { - this._cancelCallback.fire(this, e); - }, - - addAcceptCallback: function(callback) { - this._acceptCallback.add(callback); - }, - - removeAcceptCallback: function() { - this._acceptCallback.empty(); - }, - - _accept: function(e) { - this._acceptCallback.fire(this, e); - }, - - _requestAccept: function(e) { - this._acceptRequestEvent = e; - }, - - _forgetAccept: function() { - this._accept(this._acceptRequestEvent); - this._acceptRequestEvent = null; - }, - - start: noop, - move: noop, - end: noop, - - cancel: noop, - reset: function() { - if(this._acceptRequestEvent) { - this._accept(this._acceptRequestEvent); - } - }, - - _fireEvent: function(eventName, e, params) { - const eventData = extend({ - type: eventName, - originalEvent: e, - target: this._getEmitterTarget(e), - delegateTarget: this.getElement().get(0) - }, params); - - e = fireEvent(eventData); - - if(e.cancel) { - this._cancel(e); - } - - return e; - }, - - _getEmitterTarget: function(e) { - return (this.delegateSelector ? $(e.target).closest(this.delegateSelector) : this.getElement()).get(0); - }, - - dispose: noop - -}); - -export default Emitter; +export { default } from '../../__internal/events/core/m_emitter'; diff --git a/packages/devextreme/js/events/core/emitter_registrator.js b/packages/devextreme/js/events/core/emitter_registrator.js index ccfbbfb4797f..c9f061241a98 100644 --- a/packages/devextreme/js/events/core/emitter_registrator.js +++ b/packages/devextreme/js/events/core/emitter_registrator.js @@ -1,297 +1 @@ -import $ from '../../core/renderer'; -import readyCallbacks from '../../core/utils/ready_callbacks'; -import domAdapter from '../../core/dom_adapter'; -import eventsEngine from '../../events/core/events_engine'; -import { data as elementData } from '../../core/element_data'; -import Class from '../../core/class'; -import { extend } from '../../core/utils/extend'; -import { each } from '../../core/utils/iterator'; -import registerEvent from './event_registrator'; -import { addNamespace, isMouseEvent } from '../utils/index'; -import pointerEvents from '../pointer'; -import { name as wheelEventName } from './wheel'; - -const MANAGER_EVENT = 'dxEventManager'; -const EMITTER_DATA = 'dxEmitter'; - -const EventManager = Class.inherit({ - - ctor: function() { - this._attachHandlers(); - this.reset(); - - this._proxiedCancelHandler = this._cancelHandler.bind(this); - this._proxiedAcceptHandler = this._acceptHandler.bind(this); - }, - - _attachHandlers: function() { - readyCallbacks.add(function() { - const document = domAdapter.getDocument(); - eventsEngine.subscribeGlobal(document, addNamespace(pointerEvents.down, MANAGER_EVENT), this._pointerDownHandler.bind(this)); - eventsEngine.subscribeGlobal(document, addNamespace(pointerEvents.move, MANAGER_EVENT), this._pointerMoveHandler.bind(this)); - eventsEngine.subscribeGlobal(document, addNamespace([pointerEvents.up, pointerEvents.cancel].join(' '), MANAGER_EVENT), this._pointerUpHandler.bind(this)); - eventsEngine.subscribeGlobal(document, addNamespace(wheelEventName, MANAGER_EVENT), this._mouseWheelHandler.bind(this)); - }.bind(this)); - }, - - _eachEmitter: function(callback) { - const activeEmitters = this._activeEmitters || []; - let i = 0; - - while(activeEmitters.length > i) { - const emitter = activeEmitters[i]; - if(callback(emitter) === false) { - break; - } - - if(activeEmitters[i] === emitter) { - i++; - } - } - }, - - _applyToEmitters: function(method, arg) { - this._eachEmitter(function(emitter) { - emitter[method].call(emitter, arg); - }); - }, - - reset: function() { - this._eachEmitter(this._proxiedCancelHandler); - this._activeEmitters = []; - }, - - resetEmitter: function(emitter) { - this._proxiedCancelHandler(emitter); - }, - - _pointerDownHandler: function(e) { - if(isMouseEvent(e) && e.which > 1) { - return; - } - - this._updateEmitters(e); - }, - - _updateEmitters: function(e) { - if(!this._isSetChanged(e)) { - return; - } - - this._cleanEmitters(e); - this._fetchEmitters(e); - }, - - _isSetChanged: function(e) { - const currentSet = this._closestEmitter(e); - const previousSet = this._emittersSet || []; - - let setChanged = currentSet.length !== previousSet.length; - - each(currentSet, function(index, emitter) { - setChanged = setChanged || previousSet[index] !== emitter; - return !setChanged; - }); - - this._emittersSet = currentSet; - - return setChanged; - }, - - _closestEmitter: function(e) { - const that = this; - - const result = []; - let $element = $(e.target); - - function handleEmitter(_, emitter) { - if(!!emitter && emitter.validatePointers(e) && emitter.validate(e)) { - emitter.addCancelCallback(that._proxiedCancelHandler); - emitter.addAcceptCallback(that._proxiedAcceptHandler); - result.push(emitter); - } - } - - while($element.length) { - const emitters = elementData($element.get(0), EMITTER_DATA) || []; - each(emitters, handleEmitter); - $element = $element.parent(); - } - - return result; - }, - - _acceptHandler: function(acceptedEmitter, e) { - const that = this; - - this._eachEmitter(function(emitter) { - if(emitter !== acceptedEmitter) { - that._cancelEmitter(emitter, e); - } - }); - }, - - _cancelHandler: function(canceledEmitter, e) { - this._cancelEmitter(canceledEmitter, e); - }, - - _cancelEmitter: function(emitter, e) { - const activeEmitters = this._activeEmitters; - - if(e) { - emitter.cancel(e); - } else { - emitter.reset(); - } - - emitter.removeCancelCallback(); - emitter.removeAcceptCallback(); - - const emitterIndex = activeEmitters.indexOf(emitter); - if(emitterIndex > -1) { - activeEmitters.splice(emitterIndex, 1); - } - }, - - _cleanEmitters: function(e) { - this._applyToEmitters('end', e); - this.reset(e); - }, - - _fetchEmitters: function(e) { - this._activeEmitters = this._emittersSet.slice(); - this._applyToEmitters('start', e); - }, - - _pointerMoveHandler: function(e) { - this._applyToEmitters('move', e); - }, - - _pointerUpHandler: function(e) { - this._updateEmitters(e); - }, - - _mouseWheelHandler: function(e) { - if(!this._allowInterruptionByMouseWheel()) { - return; - } - - e.pointers = [null]; - this._pointerDownHandler(e); - - this._adjustWheelEvent(e); - - this._pointerMoveHandler(e); - e.pointers = []; - this._pointerUpHandler(e); - }, - - _allowInterruptionByMouseWheel: function() { - let allowInterruption = true; - this._eachEmitter(function(emitter) { - allowInterruption = emitter.allowInterruptionByMouseWheel() && allowInterruption; - return allowInterruption; - }); - return allowInterruption; - }, - - _adjustWheelEvent: function(e) { - let closestGestureEmitter = null; - - this._eachEmitter(function(emitter) { - if(!(emitter.gesture)) { - return; - } - - const direction = emitter.getDirection(e); - if(direction !== 'horizontal' && !e.shiftKey || direction !== 'vertical' && e.shiftKey) { - closestGestureEmitter = emitter; - return false; - } - }); - - if(!closestGestureEmitter) { - return; - } - - const direction = closestGestureEmitter.getDirection(e); - const verticalGestureDirection = direction === 'both' && !e.shiftKey || direction === 'vertical'; - const prop = verticalGestureDirection ? 'pageY' : 'pageX'; - - e[prop] += e.delta; - }, - - isActive: function(element) { - let result = false; - this._eachEmitter(function(emitter) { - result = result || emitter.getElement().is(element); - }); - return result; - } -}); - -const eventManager = new EventManager(); - -const EMITTER_SUBSCRIPTION_DATA = 'dxEmitterSubscription'; - -const registerEmitter = function(emitterConfig) { - const emitterClass = emitterConfig.emitter; - const emitterName = emitterConfig.events[0]; - const emitterEvents = emitterConfig.events; - - each(emitterEvents, function(_, eventName) { - registerEvent(eventName, { - - noBubble: !emitterConfig.bubble, - - setup: function(element) { - const subscriptions = elementData(element, EMITTER_SUBSCRIPTION_DATA) || {}; - - const emitters = elementData(element, EMITTER_DATA) || {}; - const emitter = emitters[emitterName] || new emitterClass(element); - - subscriptions[eventName] = true; - emitters[emitterName] = emitter; - - elementData(element, EMITTER_DATA, emitters); - elementData(element, EMITTER_SUBSCRIPTION_DATA, subscriptions); - }, - - add: function(element, handleObj) { - const emitters = elementData(element, EMITTER_DATA); - const emitter = emitters[emitterName]; - - emitter.configure(extend({ - delegateSelector: handleObj.selector - }, handleObj.data), handleObj.type); - }, - - teardown: function(element) { - const subscriptions = elementData(element, EMITTER_SUBSCRIPTION_DATA); - - const emitters = elementData(element, EMITTER_DATA); - const emitter = emitters[emitterName]; - - delete subscriptions[eventName]; - - let disposeEmitter = true; - each(emitterEvents, function(_, eventName) { - disposeEmitter = disposeEmitter && !subscriptions[eventName]; - return disposeEmitter; - }); - - if(disposeEmitter) { - if(eventManager.isActive(element)) { - eventManager.resetEmitter(emitter); - } - - emitter && emitter.dispose(); - delete emitters[emitterName]; - } - } - - }); - }); -}; - -export default registerEmitter; +export { default } from '../../__internal/events/core/m_emitter_registrator'; diff --git a/packages/devextreme/js/events/core/event_registrator.js b/packages/devextreme/js/events/core/event_registrator.js index f15717cc1523..4de26caede8d 100644 --- a/packages/devextreme/js/events/core/event_registrator.js +++ b/packages/devextreme/js/events/core/event_registrator.js @@ -1,35 +1 @@ -import { each } from '../../core/utils/iterator'; -import callbacks from './event_registrator_callbacks'; - -const registerEvent = function(name, eventObject) { - const strategy = {}; - - if('noBubble' in eventObject) { - strategy.noBubble = eventObject.noBubble; - } - - if('bindType' in eventObject) { - strategy.bindType = eventObject.bindType; - } - - if('delegateType' in eventObject) { - strategy.delegateType = eventObject.delegateType; - } - - each(['setup', 'teardown', 'add', 'remove', 'trigger', 'handle', '_default', 'dispose'], function(_, methodName) { - if(!eventObject[methodName]) { - return; - } - - strategy[methodName] = function() { - const args = [].slice.call(arguments); - args.unshift(this); - return eventObject[methodName].apply(eventObject, args); - }; - }); - - callbacks.fire(name, strategy); -}; -registerEvent.callbacks = callbacks; - -export default registerEvent; +export { default } from '../../__internal/events/core/m_event_registrator'; diff --git a/packages/devextreme/js/events/core/events_engine.js b/packages/devextreme/js/events/core/events_engine.js index 853c9d5cee3b..a8e5d0c5e304 100644 --- a/packages/devextreme/js/events/core/events_engine.js +++ b/packages/devextreme/js/events/core/events_engine.js @@ -1,685 +1 @@ -import registerEventCallbacks from './event_registrator_callbacks'; -import { extend } from '../../core/utils/extend'; -import { getEventTarget } from '../utils/event_target'; -import domAdapter from '../../core/dom_adapter'; -import { getWindow, hasWindow } from '../../core/utils/window'; -const window = getWindow(); -import injector from '../../core/utils/dependency_injector'; -import { isWindow, isFunction, isString, isObject } from '../../core/utils/type'; -import Callbacks from '../../core/utils/callbacks'; -import errors from '../../core/errors'; -import hookTouchProps from '../../events/core/hook_touch_props'; -import callOnce from '../../core/utils/call_once'; - -const EMPTY_EVENT_NAME = 'dxEmptyEventType'; -const NATIVE_EVENTS_TO_SUBSCRIBE = { - 'mouseenter': 'mouseover', - 'mouseleave': 'mouseout', - 'pointerenter': 'pointerover', - 'pointerleave': 'pointerout' -}; -const NATIVE_EVENTS_TO_TRIGGER = { - 'focusin': 'focus', - 'focusout': 'blur' -}; -const NO_BUBBLE_EVENTS = ['blur', 'focus', 'load']; - -const forcePassiveFalseEventNames = ['touchmove', 'wheel', 'mousewheel', 'touchstart']; - -const EVENT_PROPERTIES = [ - 'target', - 'relatedTarget', - 'delegateTarget', - 'altKey', - 'bubbles', - 'cancelable', - 'changedTouches', - 'ctrlKey', - 'detail', - 'eventPhase', - 'metaKey', - 'shiftKey', - 'view', - 'char', - 'code', - 'charCode', - 'key', - 'keyCode', - 'button', - 'buttons', - 'offsetX', - 'offsetY', - 'pointerId', - 'pointerType', - 'targetTouches', - 'toElement', - 'touches' -]; - -function matchesSafe(target, selector) { - return !isWindow(target) && target.nodeName !== '#document' && domAdapter.elementMatches(target, selector); -} -const elementDataMap = new WeakMap(); -let guid = 0; -let skipEvent; - -const special = (function() { - const specialData = {}; - - registerEventCallbacks.add(function(eventName, eventObject) { - specialData[eventName] = eventObject; - }); - - return { - getField: function(eventName, field) { - return specialData[eventName] && specialData[eventName][field]; - }, - callMethod: function(eventName, methodName, context, args) { - return specialData[eventName] && specialData[eventName][methodName] && specialData[eventName][methodName].apply(context, args); - } - }; -}()); - -const eventsEngine = injector({ - on: getHandler(normalizeOnArguments(iterate(function(element, eventName, selector, data, handler) { - const handlersController = getHandlersController(element, eventName); - handlersController.addHandler(handler, selector, data); - }))), - - one: getHandler(normalizeOnArguments(function(element, eventName, selector, data, handler) { - const oneTimeHandler = function() { - eventsEngine.off(element, eventName, selector, oneTimeHandler); - handler.apply(this, arguments); - }; - - eventsEngine.on(element, eventName, selector, data, oneTimeHandler); - })), - - off: getHandler(normalizeOffArguments(iterate(function(element, eventName, selector, handler) { - const handlersController = getHandlersController(element, eventName); - handlersController.removeHandler(handler, selector); - }))), - - trigger: getHandler(normalizeTriggerArguments(function(element, event, extraParameters) { - const eventName = event.type; - const handlersController = getHandlersController(element, event.type); - - special.callMethod(eventName, 'trigger', element, [ event, extraParameters ]); - handlersController.callHandlers(event, extraParameters); - - const noBubble = special.getField(eventName, 'noBubble') - || event.isPropagationStopped() - || NO_BUBBLE_EVENTS.indexOf(eventName) !== -1; - - if(!noBubble) { - const parents = []; - const getParents = function(element) { - const parent = element.parentNode - ?? (isObject(element.host) ? element.host : null); - if(parent) { - parents.push(parent); - getParents(parent); - } - }; - getParents(element); - parents.push(window); - - let i = 0; - - while(parents[i] && !event.isPropagationStopped()) { - const parentDataByEvent = getHandlersController(parents[i], event.type); - parentDataByEvent.callHandlers(extend(event, { currentTarget: parents[i] }), extraParameters); - i++; - } - } - - if(element.nodeType || isWindow(element)) { - special.callMethod(eventName, '_default', element, [ event, extraParameters ]); - callNativeMethod(eventName, element); - } - })), - - triggerHandler: getHandler(normalizeTriggerArguments(function(element, event, extraParameters) { - const handlersController = getHandlersController(element, event.type); - handlersController.callHandlers(event, extraParameters); - })) -}); - -function applyForEach(args, method) { - const element = args[0]; - - if(!element) { - return; - } - - if(domAdapter.isNode(element) || isWindow(element)) { - method.apply(eventsEngine, args); - } else if(!isString(element) && 'length' in element) { - const itemArgs = Array.prototype.slice.call(args, 0); - - Array.prototype.forEach.call(element, function(itemElement) { - itemArgs[0] = itemElement; - applyForEach(itemArgs, method); - }); - } else { - throw errors.Error('E0025'); - } -} - -function getHandler(method) { - return function() { - applyForEach(arguments, method); - }; -} - -function detectPassiveEventHandlersSupport() { - let isSupported = false; - - try { - const options = Object.defineProperty({ }, 'passive', { - get: function() { - isSupported = true; - return true; - } - }); - - window.addEventListener('test', null, options); - } catch(e) { } - - return isSupported; -} - -const passiveEventHandlersSupported = callOnce(detectPassiveEventHandlersSupport); - -const contains = (container, element) => { - if(isWindow(container)) { - return contains(container.document, element); - } - - return container.contains - ? container.contains(element) - : !!(element.compareDocumentPosition(container) & element.DOCUMENT_POSITION_CONTAINS); -}; - -function getHandlersController(element, eventName) { - let elementData = elementDataMap.get(element); - - eventName = eventName || ''; - - const eventNameParts = eventName.split('.'); - const namespaces = eventNameParts.slice(1); - const eventNameIsDefined = !!eventNameParts[0]; - - eventName = eventNameParts[0] || EMPTY_EVENT_NAME; - - if(!elementData) { - elementData = {}; - elementDataMap.set(element, elementData); - } - - if(!elementData[eventName]) { - elementData[eventName] = { - handleObjects: [], - nativeHandler: null - }; - } - - const eventData = elementData[eventName]; - - return { - addHandler: function(handler, selector, data) { - const callHandler = function(e, extraParameters) { - const handlerArgs = [e]; - const target = e.currentTarget; - const relatedTarget = e.relatedTarget; - let secondaryTargetIsInside; - let result; - - if(eventName in NATIVE_EVENTS_TO_SUBSCRIBE) { - secondaryTargetIsInside = relatedTarget && target && (relatedTarget === target || contains(target, relatedTarget)); - } - - if(extraParameters !== undefined) { - handlerArgs.push(extraParameters); - } - - special.callMethod(eventName, 'handle', element, [ e, data ]); - - if(!secondaryTargetIsInside) { - result = handler.apply(target, handlerArgs); - } - - if(result === false) { - e.preventDefault(); - e.stopPropagation(); - } - }; - - const wrappedHandler = function(e, extraParameters) { - if(skipEvent && e.type === skipEvent) { - return; - } - - e.data = data; - e.delegateTarget = element; - - if(selector) { - let currentTarget = e.target; - - while(currentTarget && currentTarget !== element) { - if(matchesSafe(currentTarget, selector)) { - e.currentTarget = currentTarget; - callHandler(e, extraParameters); - } - currentTarget = currentTarget.parentNode; - } - } else { - e.currentTarget = e.delegateTarget || e.target; - - const isTargetInShadowDOM = Boolean(e.target?.shadowRoot); - if(isTargetInShadowDOM) { - const target = getEventTarget(e); - e.target = target; - } - - callHandler(e, extraParameters); - } - }; - - const handleObject = { - handler: handler, - wrappedHandler: wrappedHandler, - selector: selector, - type: eventName, - data: data, - namespace: namespaces.join('.'), - namespaces: namespaces, - guid: ++guid - }; - - eventData.handleObjects.push(handleObject); - - const firstHandlerForTheType = eventData.handleObjects.length === 1; - let shouldAddNativeListener = firstHandlerForTheType && eventNameIsDefined; - let nativeListenerOptions; - - if(shouldAddNativeListener) { - shouldAddNativeListener = !special.callMethod(eventName, 'setup', element, [ data, namespaces, handler ]); - } - - if(shouldAddNativeListener) { - eventData.nativeHandler = getNativeHandler(eventName); - - if(passiveEventHandlersSupported() && forcePassiveFalseEventNames.indexOf(eventName) > -1) { - nativeListenerOptions = { - passive: false - }; - } - - eventData.removeListener = domAdapter.listen(element, NATIVE_EVENTS_TO_SUBSCRIBE[eventName] || eventName, eventData.nativeHandler, nativeListenerOptions); - } - - special.callMethod(eventName, 'add', element, [ handleObject ]); - }, - - removeHandler: function(handler, selector) { - const removeByEventName = function(eventName) { - const eventData = elementData[eventName]; - - if(!eventData.handleObjects.length) { - delete elementData[eventName]; - return; - } - let removedHandler; - - eventData.handleObjects = eventData.handleObjects.filter(function(handleObject) { - const skip = namespaces.length && !isSubset(handleObject.namespaces, namespaces) - || handler && handleObject.handler !== handler - || selector && handleObject.selector !== selector; - - if(!skip) { - removedHandler = handleObject.handler; - special.callMethod(eventName, 'remove', element, [ handleObject ]); - } - - return skip; - }); - - const lastHandlerForTheType = !eventData.handleObjects.length; - const shouldRemoveNativeListener = lastHandlerForTheType && eventName !== EMPTY_EVENT_NAME; - - if(shouldRemoveNativeListener) { - special.callMethod(eventName, 'teardown', element, [ namespaces, removedHandler ]); - if(eventData.nativeHandler) { - eventData.removeListener(); - } - delete elementData[eventName]; - } - }; - - if(eventNameIsDefined) { - removeByEventName(eventName); - } else { - for(const name in elementData) { - removeByEventName(name); - } - } - - const elementDataIsEmpty = Object.keys(elementData).length === 0; - - if(elementDataIsEmpty) { - elementDataMap.delete(element); - } - }, - - callHandlers: function(event, extraParameters) { - let forceStop = false; - - const handleCallback = function(handleObject) { - if(forceStop) { - return; - } - - if(!namespaces.length || isSubset(handleObject.namespaces, namespaces)) { - handleObject.wrappedHandler(event, extraParameters); - forceStop = event.isImmediatePropagationStopped(); - } - }; - - eventData.handleObjects.forEach(handleCallback); - if(namespaces.length && elementData[EMPTY_EVENT_NAME]) { - elementData[EMPTY_EVENT_NAME].handleObjects.forEach(handleCallback); - } - } - }; -} - -function getNativeHandler(subscribeName) { - return function(event, extraParameters) { - const handlersController = getHandlersController(this, subscribeName); - event = eventsEngine.Event(event); - handlersController.callHandlers(event, extraParameters); - }; -} - -function isSubset(original, checked) { - for(let i = 0; i < checked.length; i++) { - if(original.indexOf(checked[i]) < 0) return false; - } - return true; -} - -function normalizeOnArguments(callback) { - return function(element, eventName, selector, data, handler) { - if(!handler) { - handler = data; - data = undefined; - } - if(typeof selector !== 'string') { - data = selector; - selector = undefined; - } - - if(!handler && typeof eventName === 'string') { - handler = data || selector; - selector = undefined; - data = undefined; - } - - callback(element, eventName, selector, data, handler); - }; -} - -function normalizeOffArguments(callback) { - return function(element, eventName, selector, handler) { - if(typeof selector === 'function') { - handler = selector; - selector = undefined; - } - - callback(element, eventName, selector, handler); - }; -} - -function normalizeTriggerArguments(callback) { - return function(element, src, extraParameters) { - if(typeof src === 'string') { - src = { - type: src - }; - } - - if(!src.target) { - src.target = element; - } - - src.currentTarget = element; - - if(!src.delegateTarget) { - src.delegateTarget = element; - } - - if(!src.type && src.originalEvent) { - src.type = src.originalEvent.type; - } - - callback(element, (src instanceof eventsEngine.Event) ? src : eventsEngine.Event(src), extraParameters); - }; -} - -function normalizeEventArguments(callback) { - eventsEngine.Event = function(src, config) { - if(!(this instanceof eventsEngine.Event)) { - return new eventsEngine.Event(src, config); - } - - if(!src) { - src = {}; - } - - if(typeof src === 'string') { - src = { - type: src - }; - } - - if(!config) { - config = {}; - } - - callback.call(this, src, config); - }; - Object.assign(eventsEngine.Event.prototype, { - _propagationStopped: false, - _immediatePropagationStopped: false, - _defaultPrevented: false, - isPropagationStopped: function() { - return !!(this._propagationStopped || this.originalEvent && this.originalEvent.propagationStopped); - }, - stopPropagation: function() { - this._propagationStopped = true; - this.originalEvent && this.originalEvent.stopPropagation(); - }, - isImmediatePropagationStopped: function() { - return this._immediatePropagationStopped; - }, - stopImmediatePropagation: function() { - this.stopPropagation(); - this._immediatePropagationStopped = true; - this.originalEvent && this.originalEvent.stopImmediatePropagation(); - }, - isDefaultPrevented: function() { - return !!(this._defaultPrevented || this.originalEvent && this.originalEvent.defaultPrevented); - }, - preventDefault: function() { - this._defaultPrevented = true; - this.originalEvent && this.originalEvent.preventDefault(); - } - - }); - return eventsEngine.Event; -} - -function iterate(callback) { - const iterateEventNames = function(element, eventName) { - if(eventName && eventName.indexOf(' ') > -1) { - const args = Array.prototype.slice.call(arguments, 0); - eventName.split(' ').forEach(function(eventName) { - args[1] = eventName; - callback.apply(this, args); - }); - } else { - callback.apply(this, arguments); - } - }; - - return function(element, eventName) { - if(typeof eventName === 'object') { - const args = Array.prototype.slice.call(arguments, 0); - - for(const name in eventName) { - args[1] = name; - args[args.length - 1] = eventName[name]; - iterateEventNames.apply(this, args); - } - } else { - iterateEventNames.apply(this, arguments); - } - }; -} - -function callNativeMethod(eventName, element) { - const nativeMethodName = NATIVE_EVENTS_TO_TRIGGER[eventName] || eventName; - - const isLinkClickEvent = function(eventName, element) { - return eventName === 'click' && element.localName === 'a'; - }; - - if(isLinkClickEvent(eventName, element)) return; - - if(isFunction(element[nativeMethodName])) { - skipEvent = eventName; - element[nativeMethodName](); - skipEvent = undefined; - } -} - -function calculateWhich(event) { - const setForMouseEvent = function(event) { - const mouseEventRegex = /^(?:mouse|pointer|contextmenu|drag|drop)|click/; - return !event.which && event.button !== undefined && mouseEventRegex.test(event.type); - }; - - const setForKeyEvent = function(event) { - return event.which == null && event.type.indexOf('key') === 0; - }; - - if(setForKeyEvent(event)) { - return event.charCode != null ? event.charCode : event.keyCode; - } - - if(setForMouseEvent(event)) { - const whichByButton = { 1: 1, 2: 3, 3: 1, 4: 2 }; - return whichByButton[event.button]; - } - - return event.which; -} - -function initEvent(EventClass) { - if(EventClass) { - eventsEngine.Event = EventClass; - eventsEngine.Event.prototype = EventClass.prototype; - } -} - -initEvent(normalizeEventArguments(function(src, config) { - const srcIsEvent = src instanceof eventsEngine.Event - || (hasWindow() && src instanceof window.Event) - || (src.view?.Event && src instanceof src.view.Event); - - if(srcIsEvent) { - this.originalEvent = src; - this.type = src.type; - this.currentTarget = undefined; - if(Object.prototype.hasOwnProperty.call(src, 'isTrusted')) { - this.isTrusted = src.isTrusted; - } - this.timeStamp = src.timeStamp || Date.now(); - } else { - Object.assign(this, src); - } - - addProperty('which', calculateWhich, this); - - if(src.type.indexOf('touch') === 0) { - delete config.pageX; - delete config.pageY; - } - - Object.assign(this, config); - - this.guid = ++guid; -})); - -function addProperty(propName, hook, eventInstance) { - Object.defineProperty(eventInstance || eventsEngine.Event.prototype, propName, { - enumerable: true, - configurable: true, - - get: function() { - return this.originalEvent && hook(this.originalEvent); - }, - - set: function(value) { - Object.defineProperty(this, propName, { - enumerable: true, - configurable: true, - writable: true, - value: value - }); - } - }); -} - -EVENT_PROPERTIES.forEach(prop => addProperty(prop, (event) => (event[prop]))); -hookTouchProps(addProperty); - -const beforeSetStrategy = Callbacks(); -const afterSetStrategy = Callbacks(); - -eventsEngine.set = function(engine) { - beforeSetStrategy.fire(); - eventsEngine.inject(engine); - initEvent(engine.Event); - afterSetStrategy.fire(); -}; - -eventsEngine.subscribeGlobal = function() { - applyForEach(arguments, normalizeOnArguments(function() { - const args = arguments; - - eventsEngine.on.apply(this, args); - - beforeSetStrategy.add(function() { - const offArgs = Array.prototype.slice.call(args, 0); - offArgs.splice(3, 1); - eventsEngine.off.apply(this, offArgs); - }); - - afterSetStrategy.add(function() { - eventsEngine.on.apply(this, args); - }); - })); -}; - -eventsEngine.forcePassiveFalseEventNames = forcePassiveFalseEventNames; -eventsEngine.passiveEventHandlersSupported = passiveEventHandlersSupported; - -///#DEBUG -eventsEngine.elementDataMap = elementDataMap; -eventsEngine.detectPassiveEventHandlersSupport = detectPassiveEventHandlersSupport; - -///#ENDDEBUG - -export default eventsEngine; +export { default } from '../../__internal/events/core/m_events_engine'; diff --git a/packages/devextreme/js/events/core/hook_touch_props.js b/packages/devextreme/js/events/core/hook_touch_props.js index fec9f76a8e75..96daa6e79e09 100644 --- a/packages/devextreme/js/events/core/hook_touch_props.js +++ b/packages/devextreme/js/events/core/hook_touch_props.js @@ -1,21 +1 @@ -const touchPropsToHook = ['pageX', 'pageY', 'screenX', 'screenY', 'clientX', 'clientY']; -const touchPropHook = function(name, event) { - if(event[name] && !event.touches || !event.touches) { - return event[name]; - } - - const touches = event.touches.length ? event.touches : event.changedTouches; - if(!touches.length) { - return; - } - - return touches[0][name]; -}; - -export default function(callback) { - touchPropsToHook.forEach(function(name) { - callback(name, function(event) { - return touchPropHook(name, event); - }); - }, this); -} +export { default } from '../../__internal/events/core/m_hook_touch_props'; diff --git a/packages/devextreme/js/events/core/keyboard_processor.js b/packages/devextreme/js/events/core/keyboard_processor.js index ad018beb86c9..d7e367d4c144 100644 --- a/packages/devextreme/js/events/core/keyboard_processor.js +++ b/packages/devextreme/js/events/core/keyboard_processor.js @@ -1,82 +1 @@ -import $ from '../../core/renderer'; -import eventsEngine from '../../events/core/events_engine'; -import Class from '../../core/class'; -import { addNamespace, normalizeKeyName } from '../../events/utils/index'; - -const COMPOSITION_START_EVENT = 'compositionstart'; -const COMPOSITION_END_EVENT = 'compositionend'; -const KEYDOWN_EVENT = 'keydown'; -const NAMESPACE = 'KeyboardProcessor'; - -const createKeyDownOptions = (e) => { - return { - keyName: normalizeKeyName(e), - key: e.key, - code: e.code, - ctrl: e.ctrlKey, - location: e.location, - metaKey: e.metaKey, - shift: e.shiftKey, - alt: e.altKey, - which: e.which, - originalEvent: e - }; -}; - -const KeyboardProcessor = Class.inherit({ - _keydown: addNamespace(KEYDOWN_EVENT, NAMESPACE), - _compositionStart: addNamespace(COMPOSITION_START_EVENT, NAMESPACE), - _compositionEnd: addNamespace(COMPOSITION_END_EVENT, NAMESPACE), - - ctor: function(options) { - options = options || {}; - if(options.element) { - this._element = $(options.element); - } - if(options.focusTarget) { - this._focusTarget = options.focusTarget; - } - this._handler = options.handler; - - if(this._element) { - this._processFunction = (e) => { - const focusTargets = $(this._focusTarget).toArray(); - const isNotFocusTarget = this._focusTarget && this._focusTarget !== e.target && !focusTargets.includes(e.target); - const shouldSkipProcessing = this._isComposingJustFinished && e.which === 229 || this._isComposing || isNotFocusTarget; - - this._isComposingJustFinished = false; - if(!shouldSkipProcessing) { - this.process(e); - } - }; - this._toggleProcessingWithContext = this.toggleProcessing.bind(this); - - eventsEngine.on(this._element, this._keydown, this._processFunction); - eventsEngine.on(this._element, this._compositionStart, this._toggleProcessingWithContext); - eventsEngine.on(this._element, this._compositionEnd, this._toggleProcessingWithContext); - } - }, - - dispose: function() { - if(this._element) { - eventsEngine.off(this._element, this._keydown, this._processFunction); - eventsEngine.off(this._element, this._compositionStart, this._toggleProcessingWithContext); - eventsEngine.off(this._element, this._compositionEnd, this._toggleProcessingWithContext); - } - this._element = undefined; - this._handler = undefined; - }, - - process: function(e) { - this._handler(createKeyDownOptions(e)); - }, - - toggleProcessing: function({ type }) { - this._isComposing = type === COMPOSITION_START_EVENT; - this._isComposingJustFinished = !this._isComposing; - } -}); - -KeyboardProcessor.createKeyDownOptions = createKeyDownOptions; - -export default KeyboardProcessor; +export { default } from '../../__internal/events/core/m_keyboard_processor'; diff --git a/packages/devextreme/js/events/core/wheel.js b/packages/devextreme/js/events/core/wheel.js index 4e99c61d5b6d..c9d2e35c81d2 100644 --- a/packages/devextreme/js/events/core/wheel.js +++ b/packages/devextreme/js/events/core/wheel.js @@ -1,54 +1 @@ -import $ from '../../core/renderer'; -import eventsEngine from '../../events/core/events_engine'; -import registerEvent from './event_registrator'; -import { addNamespace, fireEvent } from '../utils/index'; - - -const EVENT_NAME = 'dxmousewheel'; -const EVENT_NAMESPACE = 'dxWheel'; -const NATIVE_EVENT_NAME = 'wheel'; - -const PIXEL_MODE = 0; -const DELTA_MUTLIPLIER = 30; - -const wheel = { - setup: function(element) { - const $element = $(element); - eventsEngine.on($element, addNamespace(NATIVE_EVENT_NAME, EVENT_NAMESPACE), wheel._wheelHandler.bind(wheel)); - }, - - teardown: function(element) { - eventsEngine.off(element, `.${EVENT_NAMESPACE}`); - }, - - _wheelHandler: function(e) { - const { deltaMode, deltaY, deltaX, deltaZ } = e.originalEvent; - - fireEvent({ - type: EVENT_NAME, - originalEvent: e, - delta: this._normalizeDelta(deltaY, deltaMode), - deltaX, - deltaY, - deltaZ, - deltaMode, - pointerType: 'mouse' - }); - - e.stopPropagation(); - }, - - _normalizeDelta(delta, deltaMode = PIXEL_MODE) { - if(deltaMode === PIXEL_MODE) { - return -delta; - } else { - // Use multiplier to get rough delta value in px for the LINE or PAGE mode - // https://bugzilla.mozilla.org/show_bug.cgi?id=1392460 - return -DELTA_MUTLIPLIER * delta; - } - } -}; - -registerEvent(EVENT_NAME, wheel); - -export { EVENT_NAME as name }; +export * from '../../__internal/events/core/m_wheel'; diff --git a/packages/devextreme/js/events/double_click.js b/packages/devextreme/js/events/double_click.js index f095c10cda89..3e0cf4edb96e 100644 --- a/packages/devextreme/js/events/double_click.js +++ b/packages/devextreme/js/events/double_click.js @@ -1,4 +1,4 @@ -import { name, dblClick } from '../__internal/events/dblclick'; +import { name, dblClick } from '../__internal/events/m_dblclick'; import registerEvent from './core/event_registrator'; registerEvent(name, dblClick); diff --git a/packages/devextreme/js/events/drag.js b/packages/devextreme/js/events/drag.js index ceed272dd460..c03bdb173853 100644 --- a/packages/devextreme/js/events/drag.js +++ b/packages/devextreme/js/events/drag.js @@ -1,74 +1,3 @@ -import $ from '../core/renderer'; -import { data as elementData, removeData } from '../core/element_data'; -import { wrapToArray } from '../core/utils/array'; -import * as iteratorUtils from '../core/utils/iterator'; -import { contains } from '../core/utils/dom'; -import registerEvent from './core/event_registrator'; -import { eventData as eData, fireEvent } from './utils/index'; -import GestureEmitter from './gesture/emitter.gesture'; -import registerEmitter from './core/emitter_registrator'; - - -const DRAG_START_EVENT = 'dxdragstart'; -const DRAG_EVENT = 'dxdrag'; -const DRAG_END_EVENT = 'dxdragend'; - -const DRAG_ENTER_EVENT = 'dxdragenter'; -const DRAG_LEAVE_EVENT = 'dxdragleave'; -const DROP_EVENT = 'dxdrop'; - -const DX_DRAG_EVENTS_COUNT_KEY = 'dxDragEventsCount'; - - -const knownDropTargets = []; -const knownDropTargetSelectors = []; -const knownDropTargetConfigs = []; - -const dropTargetRegistration = { - - setup: function(element, data) { - const knownDropTarget = knownDropTargets.includes(element); - if(!knownDropTarget) { - knownDropTargets.push(element); - knownDropTargetSelectors.push([]); - knownDropTargetConfigs.push(data || {}); - } - }, - - add: function(element, handleObj) { - const index = knownDropTargets.indexOf(element); - this.updateEventsCounter(element, handleObj.type, 1); - - const selector = handleObj.selector; - if(!knownDropTargetSelectors[index].includes(selector)) { - knownDropTargetSelectors[index].push(selector); - } - }, - - updateEventsCounter: function(element, event, value) { - if([DRAG_ENTER_EVENT, DRAG_LEAVE_EVENT, DROP_EVENT].indexOf(event) > -1) { - const eventsCount = elementData(element, DX_DRAG_EVENTS_COUNT_KEY) || 0; - elementData(element, DX_DRAG_EVENTS_COUNT_KEY, Math.max(0, eventsCount + value)); - } - }, - - remove: function(element, handleObj) { - this.updateEventsCounter(element, handleObj.type, -1); - }, - - teardown: function(element) { - const handlersCount = elementData(element, DX_DRAG_EVENTS_COUNT_KEY); - if(!handlersCount) { - const index = knownDropTargets.indexOf(element); - knownDropTargets.splice(index, 1); - knownDropTargetSelectors.splice(index, 1); - knownDropTargetConfigs.splice(index, 1); - removeData(element, DX_DRAG_EVENTS_COUNT_KEY); - } - } - -}; - /** * @name UI Events.dxdragenter * @type eventType @@ -90,229 +19,6 @@ const dropTargetRegistration = { * @type_function_param1_field1 draggingElement:Element * @module events/drag */ - -registerEvent(DRAG_ENTER_EVENT, dropTargetRegistration); -registerEvent(DRAG_LEAVE_EVENT, dropTargetRegistration); -registerEvent(DROP_EVENT, dropTargetRegistration); - -const getItemDelegatedTargets = function($element) { - const dropTargetIndex = knownDropTargets.indexOf($element.get(0)); - const dropTargetSelectors = knownDropTargetSelectors[dropTargetIndex].filter((selector) => selector); - - let $delegatedTargets = $element.find(dropTargetSelectors.join(', ')); - if(knownDropTargetSelectors[dropTargetIndex].includes(undefined)) { - $delegatedTargets = $delegatedTargets.add($element); - } - return $delegatedTargets; -}; - -const getItemConfig = function($element) { - const dropTargetIndex = knownDropTargets.indexOf($element.get(0)); - return knownDropTargetConfigs[dropTargetIndex]; -}; - -const getItemPosition = function(dropTargetConfig, $element) { - if(dropTargetConfig.itemPositionFunc) { - return dropTargetConfig.itemPositionFunc($element); - } else { - return $element.offset(); - } -}; - -const getItemSize = function(dropTargetConfig, $element) { - if(dropTargetConfig.itemSizeFunc) { - return dropTargetConfig.itemSizeFunc($element); - } - - return { - width: $element.get(0).getBoundingClientRect().width, - height: $element.get(0).getBoundingClientRect().height - }; -}; - -const DragEmitter = GestureEmitter.inherit({ - - ctor: function(element) { - this.callBase(element); - - this.direction = 'both'; - }, - - _init: function(e) { - this._initEvent = e; - }, - - _start: function(e) { - e = this._fireEvent(DRAG_START_EVENT, this._initEvent); - - this._maxLeftOffset = e.maxLeftOffset; - this._maxRightOffset = e.maxRightOffset; - this._maxTopOffset = e.maxTopOffset; - this._maxBottomOffset = e.maxBottomOffset; - - if(e.targetElements || e.targetElements === null) { - const dropTargets = wrapToArray(e.targetElements || []); - this._dropTargets = iteratorUtils.map(dropTargets, function(element) { return $(element).get(0); }); - } else { - this._dropTargets = knownDropTargets; - } - }, - - _move: function(e) { - const eventData = eData(e); - const dragOffset = this._calculateOffset(eventData); - - e = this._fireEvent(DRAG_EVENT, e, { - offset: dragOffset - }); - - this._processDropTargets(e); - - if(!e._cancelPreventDefault) { - e.preventDefault(); - } - }, - - _calculateOffset: function(eventData) { - return { - x: this._calculateXOffset(eventData), - y: this._calculateYOffset(eventData) - }; - }, - - _calculateXOffset: function(eventData) { - if(this.direction !== 'vertical') { - const offset = eventData.x - this._startEventData.x; - - return this._fitOffset(offset, this._maxLeftOffset, this._maxRightOffset); - } - return 0; - }, - - _calculateYOffset: function(eventData) { - if(this.direction !== 'horizontal') { - const offset = eventData.y - this._startEventData.y; - - return this._fitOffset(offset, this._maxTopOffset, this._maxBottomOffset); - } - return 0; - }, - - _fitOffset: function(offset, minOffset, maxOffset) { - if(minOffset != null) { - offset = Math.max(offset, -minOffset); - } - if(maxOffset != null) { - offset = Math.min(offset, maxOffset); - } - - return offset; - }, - - _processDropTargets: function(e) { - const target = this._findDropTarget(e); - const sameTarget = target === this._currentDropTarget; - - if(!sameTarget) { - this._fireDropTargetEvent(e, DRAG_LEAVE_EVENT); - this._currentDropTarget = target; - this._fireDropTargetEvent(e, DRAG_ENTER_EVENT); - } - }, - - _fireDropTargetEvent: function(event, eventName) { - if(!this._currentDropTarget) { - return; - } - - const eventData = { - type: eventName, - originalEvent: event, - draggingElement: this._$element.get(0), - target: this._currentDropTarget - }; - - fireEvent(eventData); - }, - - _findDropTarget: function(e) { - const that = this; - let result; - - iteratorUtils.each(knownDropTargets, function(_, target) { - if(!that._checkDropTargetActive(target)) { - return; - } - - const $target = $(target); - iteratorUtils.each(getItemDelegatedTargets($target), function(_, delegatedTarget) { - const $delegatedTarget = $(delegatedTarget); - if(that._checkDropTarget(getItemConfig($target), $delegatedTarget, $(result), e)) { - result = delegatedTarget; - } - }); - }); - - return result; - }, - - _checkDropTargetActive: function(target) { - let active = false; - - iteratorUtils.each(this._dropTargets, function(_, activeTarget) { - active = active || activeTarget === target || contains(activeTarget, target); - return !active; - }); - - return active; - }, - - _checkDropTarget: function(config, $target, $prevTarget, e) { - const isDraggingElement = $target.get(0) === $(e.target).get(0); - if(isDraggingElement) { - return false; - } - - const targetPosition = getItemPosition(config, $target); - if(e.pageX < targetPosition.left) { - return false; - } - if(e.pageY < targetPosition.top) { - return false; - } - - const targetSize = getItemSize(config, $target); - if(e.pageX > targetPosition.left + targetSize.width) { - return false; - } - if(e.pageY > targetPosition.top + targetSize.height) { - return false; - } - - if($prevTarget.length && $prevTarget.closest($target).length) { - return false; - } - - if(config.checkDropTarget && !config.checkDropTarget($target, e)) { - return false; - } - - return $target; - }, - - _end: function(e) { - const eventData = eData(e); - - this._fireEvent(DRAG_END_EVENT, e, { - offset: this._calculateOffset(eventData) - }); - - this._fireDropTargetEvent(e, DROP_EVENT); - delete this._currentDropTarget; - } - -}); - /** * @name UI Events.dxdragstart * @type eventType @@ -336,26 +42,4 @@ const DragEmitter = GestureEmitter.inherit({ * @type_function_param1_field2 cancel:boolean * @module events/drag */ - -registerEmitter({ - emitter: DragEmitter, - events: [ - DRAG_START_EVENT, - DRAG_EVENT, - DRAG_END_EVENT - ] -}); - - -///#DEBUG -export { knownDropTargets as dropTargets }; -///#ENDDEBUG - -export { - DRAG_EVENT as move, - DRAG_START_EVENT as start, - DRAG_END_EVENT as end, - DRAG_ENTER_EVENT as enter, - DRAG_LEAVE_EVENT as leave, - DROP_EVENT as drop -}; +export * from '../__internal/events/m_drag'; diff --git a/packages/devextreme/js/events/gesture/emitter.gesture.js b/packages/devextreme/js/events/gesture/emitter.gesture.js index bbf8c1a2146a..bacdf0ee957e 100644 --- a/packages/devextreme/js/events/gesture/emitter.gesture.js +++ b/packages/devextreme/js/events/gesture/emitter.gesture.js @@ -1,236 +1 @@ -import $ from '../../core/renderer'; -import eventsEngine from '../../events/core/events_engine'; -import devices from '../../core/devices'; -import { styleProp } from '../../core/utils/style'; -import callOnce from '../../core/utils/call_once'; -import { resetActiveElement, clearSelection } from '../../core/utils/dom'; -import readyCallbacks from '../../core/utils/ready_callbacks'; -const ready = readyCallbacks.add; -import { sign } from '../../core/utils/math'; -import { noop } from '../../core/utils/common'; -import { isDefined } from '../../core/utils/type'; -import { needSkipEvent, createEvent, eventData, isDxMouseWheelEvent, eventDelta, isTouchEvent } from '../utils/index'; -import Emitter from '../core/emitter'; -const abs = Math.abs; - -const SLEEP = 0; -const INITED = 1; -const STARTED = 2; - -let TOUCH_BOUNDARY = 10; -const IMMEDIATE_TOUCH_BOUNDARY = 0; -const IMMEDIATE_TIMEOUT = 180; - -const supportPointerEvents = function() { - return styleProp('pointer-events'); -}; - -const setGestureCover = callOnce(function() { - const GESTURE_COVER_CLASS = 'dx-gesture-cover'; - - const isDesktop = devices.real().deviceType === 'desktop'; - - if(!supportPointerEvents() || !isDesktop) { - return noop; - } - - const $cover = $('
') - .addClass(GESTURE_COVER_CLASS) - .css('pointerEvents', 'none'); - - eventsEngine.subscribeGlobal($cover, 'dxmousewheel', function(e) { - e.preventDefault(); - }); - - ready(function() { - $cover.appendTo('body'); - }); - - return function(toggle, cursor) { - $cover.css('pointerEvents', toggle ? 'all' : 'none'); - toggle && $cover.css('cursor', cursor); - }; -}); - -const gestureCover = function(toggle, cursor) { - const gestureCoverStrategy = setGestureCover(); - gestureCoverStrategy(toggle, cursor); -}; - -const GestureEmitter = Emitter.inherit({ - - gesture: true, - - configure: function(data) { - this.getElement().css('msTouchAction', data.immediate ? 'pinch-zoom' : ''); - - this.callBase(data); - }, - - allowInterruptionByMouseWheel: function() { - return this._stage !== STARTED; - }, - - getDirection: function() { - return this.direction; - }, - - _cancel: function() { - this.callBase.apply(this, arguments); - - this._toggleGestureCover(false); - this._stage = SLEEP; - }, - - start: function(e) { - if(e._needSkipEvent || needSkipEvent(e)) { - this._cancel(e); - return; - } - - this._startEvent = createEvent(e); - this._startEventData = eventData(e); - - this._stage = INITED; - this._init(e); - - this._setupImmediateTimer(); - }, - - _setupImmediateTimer: function() { - clearTimeout(this._immediateTimer); - this._immediateAccepted = false; - - if(!this.immediate) { - return; - } - - if(this.immediateTimeout === 0) { - this._immediateAccepted = true; - return; - } - - this._immediateTimer = setTimeout((function() { - this._immediateAccepted = true; - }).bind(this), this.immediateTimeout ?? IMMEDIATE_TIMEOUT); - }, - - move: function(e) { - if(this._stage === INITED && this._directionConfirmed(e)) { - this._stage = STARTED; - - this._resetActiveElement(); - this._toggleGestureCover(true); - this._clearSelection(e); - - this._adjustStartEvent(e); - this._start(this._startEvent); - - if(this._stage === SLEEP) { - return; - } - - this._requestAccept(e); - this._move(e); - this._forgetAccept(); - } else if(this._stage === STARTED) { - this._clearSelection(e); - this._move(e); - } - }, - - _directionConfirmed: function(e) { - const touchBoundary = this._getTouchBoundary(e); - const delta = eventDelta(this._startEventData, eventData(e)); - const deltaX = abs(delta.x); - const deltaY = abs(delta.y); - - const horizontalMove = this._validateMove(touchBoundary, deltaX, deltaY); - const verticalMove = this._validateMove(touchBoundary, deltaY, deltaX); - - const direction = this.getDirection(e); - const bothAccepted = direction === 'both' && (horizontalMove || verticalMove); - const horizontalAccepted = direction === 'horizontal' && horizontalMove; - const verticalAccepted = direction === 'vertical' && verticalMove; - - return bothAccepted || horizontalAccepted || verticalAccepted || this._immediateAccepted; - }, - - _validateMove: function(touchBoundary, mainAxis, crossAxis) { - return mainAxis && mainAxis >= touchBoundary && (this.immediate ? mainAxis >= crossAxis : true); - }, - - _getTouchBoundary: function(e) { - return (this.immediate || isDxMouseWheelEvent(e)) ? IMMEDIATE_TOUCH_BOUNDARY : TOUCH_BOUNDARY; - }, - - _adjustStartEvent: function(e) { - const touchBoundary = this._getTouchBoundary(e); - const delta = eventDelta(this._startEventData, eventData(e)); - - this._startEvent.pageX += sign(delta.x) * touchBoundary; - this._startEvent.pageY += sign(delta.y) * touchBoundary; - }, - - _resetActiveElement: function() { - if(devices.real().platform === 'ios' && this.getElement().find(':focus').length) { - resetActiveElement(); - } - }, - - _toggleGestureCover: function(toggle) { - this._toggleGestureCoverImpl(toggle); - }, - - _toggleGestureCoverImpl: function(toggle) { - const isStarted = this._stage === STARTED; - - if(isStarted) { - gestureCover(toggle, this.getElement().css('cursor')); - } - }, - - _clearSelection: function(e) { - if(isDxMouseWheelEvent(e) || isTouchEvent(e)) { - return; - } - - clearSelection(); - }, - - end: function(e) { - this._toggleGestureCover(false); - - if(this._stage === STARTED) { - this._end(e); - } else if(this._stage === INITED) { - this._stop(e); - } - - this._stage = SLEEP; - }, - - dispose: function() { - clearTimeout(this._immediateTimer); - this.callBase.apply(this, arguments); - this._toggleGestureCover(false); - }, - - _init: noop, - _start: noop, - _move: noop, - _stop: noop, - _end: noop - -}); -GestureEmitter.initialTouchBoundary = TOUCH_BOUNDARY; -GestureEmitter.touchBoundary = function(newBoundary) { - if(isDefined(newBoundary)) { - TOUCH_BOUNDARY = newBoundary; - return; - } - - return TOUCH_BOUNDARY; -}; - -export default GestureEmitter; +export { default } from '../../__internal/events/gesture/m_emitter.gesture'; diff --git a/packages/devextreme/js/events/gesture/emitter.gesture.scroll.js b/packages/devextreme/js/events/gesture/emitter.gesture.scroll.js index 68848097870c..d3ae4e160f92 100644 --- a/packages/devextreme/js/events/gesture/emitter.gesture.scroll.js +++ b/packages/devextreme/js/events/gesture/emitter.gesture.scroll.js @@ -1,355 +1 @@ -import eventsEngine from '../../events/core/events_engine'; -import Class from '../../core/class'; -const abstract = Class.abstract; -import { addNamespace, isDxMouseWheelEvent, isMouseEvent, eventData, eventDelta } from '../../events/utils/index'; -import GestureEmitter from '../../events/gesture/emitter.gesture'; -import registerEmitter from '../../events/core/emitter_registrator'; -import { requestAnimationFrame, cancelAnimationFrame } from '../../animation/frame'; -import devices from '../../core/devices'; - -const realDevice = devices.real(); - -const SCROLL_EVENT = 'scroll'; -const SCROLL_INIT_EVENT = 'dxscrollinit'; -const SCROLL_START_EVENT = 'dxscrollstart'; -const SCROLL_MOVE_EVENT = 'dxscroll'; -const SCROLL_END_EVENT = 'dxscrollend'; -const SCROLL_STOP_EVENT = 'dxscrollstop'; -const SCROLL_CANCEL_EVENT = 'dxscrollcancel'; - - -const Locker = Class.inherit((function() { - - const NAMESPACED_SCROLL_EVENT = addNamespace(SCROLL_EVENT, 'dxScrollEmitter'); - - return { - - ctor: function(element) { - this._element = element; - - this._locked = false; - - this._proxiedScroll = (e) => { - if(!this._disposed) { - this._scroll(e); - } - }; - eventsEngine.on(this._element, NAMESPACED_SCROLL_EVENT, this._proxiedScroll); - }, - - _scroll: abstract, - - check: function(e, callback) { - if(this._locked) { - callback(); - } - }, - - dispose: function() { - this._disposed = true; - eventsEngine.off(this._element, NAMESPACED_SCROLL_EVENT, this._proxiedScroll); - } - - }; - -})()); - - -const TimeoutLocker = Locker.inherit((function() { - - return { - - ctor: function(element, timeout) { - this.callBase(element); - - this._timeout = timeout; - }, - - _scroll: function() { - this._prepare(); - this._forget(); - }, - - _prepare: function() { - if(this._timer) { - this._clearTimer(); - } - this._locked = true; - }, - - _clearTimer: function() { - clearTimeout(this._timer); - this._locked = false; - this._timer = null; - }, - - _forget: function() { - const that = this; - - this._timer = setTimeout(function() { - that._clearTimer(); - }, this._timeout); - }, - - dispose: function() { - this.callBase(); - - this._clearTimer(); - } - - }; - -})()); - - -const WheelLocker = TimeoutLocker.inherit((function() { - - const WHEEL_UNLOCK_TIMEOUT = 400; - - return { - - ctor: function(element) { - this.callBase(element, WHEEL_UNLOCK_TIMEOUT); - - this._lastWheelDirection = null; - }, - - check: function(e, callback) { - this._checkDirectionChanged(e); - - this.callBase(e, callback); - }, - - _checkDirectionChanged: function(e) { - if(!isDxMouseWheelEvent(e)) { - this._lastWheelDirection = null; - return; - } - - const direction = e.shiftKey || false; - const directionChange = this._lastWheelDirection !== null && direction !== this._lastWheelDirection; - this._lastWheelDirection = direction; - - this._locked = this._locked && !directionChange; - } - - }; - -})()); - - -let PointerLocker = TimeoutLocker.inherit((function() { - - const POINTER_UNLOCK_TIMEOUT = 400; - - return { - - ctor: function(element) { - this.callBase(element, POINTER_UNLOCK_TIMEOUT); - } - - }; - -})()); - -(function() { - const { ios: isIos, android: isAndroid } = realDevice; - - if(!(isIos || isAndroid)) { - return; - } - - PointerLocker = Locker.inherit((function() { - - return { - - _scroll: function() { - this._locked = true; - - const that = this; - cancelAnimationFrame(this._scrollFrame); - this._scrollFrame = requestAnimationFrame(function() { - that._locked = false; - }); - }, - - check: function(e, callback) { - cancelAnimationFrame(this._scrollFrame); - cancelAnimationFrame(this._checkFrame); - - const that = this; - const callBase = this.callBase; - this._checkFrame = requestAnimationFrame(function() { - callBase.call(that, e, callback); - - that._locked = false; - }); - }, - - dispose: function() { - this.callBase(); - - cancelAnimationFrame(this._scrollFrame); - cancelAnimationFrame(this._checkFrame); - } - - }; - - })()); - -})(); - - -const ScrollEmitter = GestureEmitter.inherit((function() { - - const INERTIA_TIMEOUT = 100; - const VELOCITY_CALC_TIMEOUT = 200; - const FRAME_DURATION = Math.round(1000 / 60); - - return { - - ctor: function(element) { - this.callBase.apply(this, arguments); - this.direction = 'both'; - - this._pointerLocker = new PointerLocker(element); - this._wheelLocker = new WheelLocker(element); - }, - - validate: function() { - return true; - }, - - configure: function(data) { - if(data.scrollTarget) { - this._pointerLocker.dispose(); - this._wheelLocker.dispose(); - this._pointerLocker = new PointerLocker(data.scrollTarget); - this._wheelLocker = new WheelLocker(data.scrollTarget); - } - - this.callBase(data); - }, - - _init: function(e) { - this._wheelLocker.check(e, function() { - if(isDxMouseWheelEvent(e)) { - this._accept(e); - } - }.bind(this)); - - this._pointerLocker.check(e, function() { - const skipCheck = this.isNative && isMouseEvent(e); - if(!isDxMouseWheelEvent(e) && !skipCheck) { - this._accept(e); - } - }.bind(this)); - - this._fireEvent(SCROLL_INIT_EVENT, e); - - this._prevEventData = eventData(e); - }, - - move: function(e) { - this.callBase.apply(this, arguments); - - e.isScrollingEvent = this.isNative || e.isScrollingEvent; - }, - - _start: function(e) { - this._savedEventData = eventData(e); - - this._fireEvent(SCROLL_START_EVENT, e); - - this._prevEventData = eventData(e); - }, - - _move: function(e) { - const currentEventData = eventData(e); - - this._fireEvent(SCROLL_MOVE_EVENT, e, { - delta: eventDelta(this._prevEventData, currentEventData) - }); - - const delta = eventDelta(this._savedEventData, currentEventData); - if(delta.time > VELOCITY_CALC_TIMEOUT) { - this._savedEventData = this._prevEventData; - } - - this._prevEventData = eventData(e); - }, - - _end: function(e) { - const endEventDelta = eventDelta(this._prevEventData, eventData(e)); - let velocity = { x: 0, y: 0 }; - - if(!isDxMouseWheelEvent(e) && endEventDelta.time < INERTIA_TIMEOUT) { - const delta = eventDelta(this._savedEventData, this._prevEventData); - const velocityMultiplier = FRAME_DURATION / delta.time; - - velocity = { x: delta.x * velocityMultiplier, y: delta.y * velocityMultiplier }; - } - - this._fireEvent(SCROLL_END_EVENT, e, { - velocity: velocity - }); - }, - - _stop: function(e) { - this._fireEvent(SCROLL_STOP_EVENT, e); - }, - - cancel: function(e) { - this.callBase.apply(this, arguments); - - this._fireEvent(SCROLL_CANCEL_EVENT, e); - }, - - dispose: function() { - this.callBase.apply(this, arguments); - - this._pointerLocker.dispose(); - this._wheelLocker.dispose(); - }, - - _clearSelection: function() { - if(this.isNative) { - return; - } - - return this.callBase.apply(this, arguments); - }, - - _toggleGestureCover: function() { - if(this.isNative) { - return; - } - - return this.callBase.apply(this, arguments); - } - - }; - -})()); - -registerEmitter({ - emitter: ScrollEmitter, - events: [ - SCROLL_INIT_EVENT, - SCROLL_START_EVENT, - SCROLL_MOVE_EVENT, - SCROLL_END_EVENT, - SCROLL_STOP_EVENT, - SCROLL_CANCEL_EVENT - ] -}); - -export default { - init: SCROLL_INIT_EVENT, - start: SCROLL_START_EVENT, - move: SCROLL_MOVE_EVENT, - end: SCROLL_END_EVENT, - stop: SCROLL_STOP_EVENT, - cancel: SCROLL_CANCEL_EVENT, - scroll: SCROLL_EVENT -}; +export { default } from '../../__internal/events/gesture/m_emitter.gesture.scroll'; diff --git a/packages/devextreme/js/events/gesture/swipeable.js b/packages/devextreme/js/events/gesture/swipeable.js index 622880a7168f..e6d44d8d75e9 100644 --- a/packages/devextreme/js/events/gesture/swipeable.js +++ b/packages/devextreme/js/events/gesture/swipeable.js @@ -1,113 +1 @@ -import { - start as swipeEventStart, - swipe as swipeEventSwipe, - end as swipeEventEnd -} from '../swipe'; -import eventsEngine from '../../events/core/events_engine'; -import DOMComponent from '../../core/dom_component'; -import { each } from '../../core/utils/iterator'; -import { addNamespace } from '../utils/index'; -import { extend } from '../../core/utils/extend'; -import { name } from '../../core/utils/public_component'; - -const DX_SWIPEABLE = 'dxSwipeable'; -const SWIPEABLE_CLASS = 'dx-swipeable'; - -const ACTION_TO_EVENT_MAP = { - 'onStart': swipeEventStart, - 'onUpdated': swipeEventSwipe, - 'onEnd': swipeEventEnd, - 'onCancel': 'dxswipecancel' -}; - -const IMMEDIATE_TIMEOUT = 180; - -const Swipeable = DOMComponent.inherit({ - - _getDefaultOptions: function() { - return extend(this.callBase(), { - elastic: true, - immediate: false, - immediateTimeout: IMMEDIATE_TIMEOUT, - direction: 'horizontal', - itemSizeFunc: null, - onStart: null, - onUpdated: null, - onEnd: null, - onCancel: null - }); - }, - - _render: function() { - this.callBase(); - - this.$element().addClass(SWIPEABLE_CLASS); - this._attachEventHandlers(); - }, - - _attachEventHandlers: function() { - this._detachEventHandlers(); - - if(this.option('disabled')) { - return; - } - - const NAME = this.NAME; - - this._createEventData(); - - each(ACTION_TO_EVENT_MAP, (function(actionName, eventName) { - const action = this._createActionByOption(actionName, { context: this }); - - eventName = addNamespace(eventName, NAME); - - eventsEngine.on(this.$element(), eventName, this._eventData, function(e) { - return action({ event: e }); - }); - }).bind(this)); - }, - - _createEventData: function() { - this._eventData = { - elastic: this.option('elastic'), - itemSizeFunc: this.option('itemSizeFunc'), - direction: this.option('direction'), - immediate: this.option('immediate'), - immediateTimeout: this.option('immediateTimeout'), - }; - }, - - _detachEventHandlers: function() { - eventsEngine.off(this.$element(), '.' + DX_SWIPEABLE); - }, - - _optionChanged: function(args) { - switch(args.name) { - case 'disabled': - case 'onStart': - case 'onUpdated': - case 'onEnd': - case 'onCancel': - case 'elastic': - case 'immediate': - case 'itemSizeFunc': - case 'direction': - this._detachEventHandlers(); - this._attachEventHandlers(); - break; - case 'rtlEnabled': - break; - default: - this.callBase(args); - } - - }, - - _useTemplates: function() { - return false; - }, -}); - -name(Swipeable, DX_SWIPEABLE); - -export default Swipeable; +export { default } from '../../__internal/events/gesture/m_swipeable'; diff --git a/packages/devextreme/js/events/hold.js b/packages/devextreme/js/events/hold.js index 2459cb62976b..2174679ed7e7 100644 --- a/packages/devextreme/js/events/hold.js +++ b/packages/devextreme/js/events/hold.js @@ -1,62 +1,3 @@ -import { eventData, eventDelta } from './utils/index'; -import Emitter from './core/emitter'; -import registerEmitter from './core/emitter_registrator'; -const abs = Math.abs; - -const HOLD_EVENT_NAME = 'dxhold'; -const HOLD_TIMEOUT = 750; -const TOUCH_BOUNDARY = 5; - - -const HoldEmitter = Emitter.inherit({ - - start: function(e) { - this._startEventData = eventData(e); - - this._startTimer(e); - }, - - _startTimer: function(e) { - const holdTimeout = ('timeout' in this) ? this.timeout : HOLD_TIMEOUT; - this._holdTimer = setTimeout((function() { - this._requestAccept(e); - this._fireEvent(HOLD_EVENT_NAME, e, { - target: e.target - }); - this._forgetAccept(); - }).bind(this), holdTimeout); - }, - - move: function(e) { - if(this._touchWasMoved(e)) { - this._cancel(e); - } - }, - - _touchWasMoved: function(e) { - const delta = eventDelta(this._startEventData, eventData(e)); - - return abs(delta.x) > TOUCH_BOUNDARY || abs(delta.y) > TOUCH_BOUNDARY; - }, - - end: function() { - this._stopTimer(); - }, - - _stopTimer: function() { - clearTimeout(this._holdTimer); - }, - - cancel: function() { - this._stopTimer(); - }, - - dispose: function() { - this._stopTimer(); - } - -}); - /** * @name UI Events.dxhold * @type eventType @@ -64,14 +5,4 @@ const HoldEmitter = Emitter.inherit({ * @module events/hold */ -registerEmitter({ - emitter: HoldEmitter, - bubble: true, - events: [ - HOLD_EVENT_NAME - ] -}); - -export default { - name: HOLD_EVENT_NAME -}; +export { default } from '../__internal/events/m_hold'; diff --git a/packages/devextreme/js/events/hover.js b/packages/devextreme/js/events/hover.js index b75977591040..e603188362d0 100644 --- a/packages/devextreme/js/events/hover.js +++ b/packages/devextreme/js/events/hover.js @@ -1,95 +1,3 @@ -import eventsEngine from '../events/core/events_engine'; -import { removeData, data as elementData } from '../core/element_data'; -import Class from '../core/class'; -import devices from '../core/devices'; -import registerEvent from './core/event_registrator'; -import { addNamespace, isTouchEvent, fireEvent } from './utils/index'; -import pointerEvents from './pointer'; - -const HOVERSTART_NAMESPACE = 'dxHoverStart'; -const HOVERSTART = 'dxhoverstart'; -const POINTERENTER_NAMESPACED_EVENT_NAME = addNamespace(pointerEvents.enter, HOVERSTART_NAMESPACE); - -const HOVEREND_NAMESPACE = 'dxHoverEnd'; -const HOVEREND = 'dxhoverend'; -const POINTERLEAVE_NAMESPACED_EVENT_NAME = addNamespace(pointerEvents.leave, HOVEREND_NAMESPACE); - - -const Hover = Class.inherit({ - - noBubble: true, - - ctor: function() { - this._handlerArrayKeyPath = this._eventNamespace + '_HandlerStore'; - }, - - setup: function(element) { - elementData(element, this._handlerArrayKeyPath, {}); - }, - - add: function(element, handleObj) { - const that = this; - const handler = function(e) { - that._handler(e); - }; - - eventsEngine.on(element, this._originalEventName, handleObj.selector, handler); - elementData(element, this._handlerArrayKeyPath)[handleObj.guid] = handler; - }, - - _handler: function(e) { - if(isTouchEvent(e) || devices.isSimulator()) { - return; - } - - fireEvent({ - type: this._eventName, - originalEvent: e, - delegateTarget: e.delegateTarget - }); - }, - - remove: function(element, handleObj) { - const handler = elementData(element, this._handlerArrayKeyPath)[handleObj.guid]; - - eventsEngine.off(element, this._originalEventName, handleObj.selector, handler); - }, - - teardown: function(element) { - removeData(element, this._handlerArrayKeyPath); - } - -}); - -const HoverStart = Hover.inherit({ - - ctor: function() { - this._eventNamespace = HOVERSTART_NAMESPACE; - this._eventName = HOVERSTART; - this._originalEventName = POINTERENTER_NAMESPACED_EVENT_NAME; - this.callBase(); - }, - - _handler: function(e) { - const pointers = e.pointers || []; - if(!pointers.length) { - this.callBase(e); - } - } - -}); - -const HoverEnd = Hover.inherit({ - - ctor: function() { - this._eventNamespace = HOVEREND_NAMESPACE; - this._eventName = HOVEREND; - this._originalEventName = POINTERLEAVE_NAMESPACED_EVENT_NAME; - this.callBase(); - } - -}); - /** * @name UI Events.dxhoverstart * @type eventType @@ -104,10 +12,4 @@ const HoverEnd = Hover.inherit({ * @module events/hover */ -registerEvent(HOVERSTART, new HoverStart()); -registerEvent(HOVEREND, new HoverEnd()); - -export { - HOVERSTART as start, - HOVEREND as end -}; +export * from '../__internal/events/m_hover'; diff --git a/packages/devextreme/js/events/pointer.js b/packages/devextreme/js/events/pointer.js index e6839c01cf18..76b7978e98d7 100644 --- a/packages/devextreme/js/events/pointer.js +++ b/packages/devextreme/js/events/pointer.js @@ -1,12 +1,3 @@ -import GlobalConfig from '../core/config'; -import * as support from '../core/utils/support'; -import { each } from '../core/utils/iterator'; -import devices from '../core/devices'; -import registerEvent from './core/event_registrator'; -import TouchStrategy from './pointer/touch'; -import MouseStrategy from './pointer/mouse'; -import MouseAndTouchStrategy from './pointer/mouse_and_touch'; - /** * @name UI Events.dxpointerdown * @type eventType @@ -64,54 +55,4 @@ import MouseAndTouchStrategy from './pointer/mouse_and_touch'; * @module events/pointer */ -const getStrategy = (support, { tablet, phone }) => { - const pointerEventStrategy = getStrategyFromGlobalConfig(); - - if(pointerEventStrategy) { - return pointerEventStrategy; - } - - if(support.touch && !(tablet || phone)) { - return MouseAndTouchStrategy; - } - - if(support.touch) { - return TouchStrategy; - } - - return MouseStrategy; -}; - -const EventStrategy = getStrategy(support, devices.real()); - -each(EventStrategy.map, (pointerEvent, originalEvents) => { - registerEvent(pointerEvent, new EventStrategy(pointerEvent, originalEvents)); -}); - -const pointer = { - down: 'dxpointerdown', - up: 'dxpointerup', - move: 'dxpointermove', - cancel: 'dxpointercancel', - enter: 'dxpointerenter', - leave: 'dxpointerleave', - over: 'dxpointerover', - out: 'dxpointerout' -}; - -function getStrategyFromGlobalConfig() { - const eventStrategyName = GlobalConfig().pointerEventStrategy; - - return { - 'mouse-and-touch': MouseAndTouchStrategy, - 'touch': TouchStrategy, - 'mouse': MouseStrategy, - }[eventStrategyName]; -} - -///#DEBUG -pointer.getStrategy = getStrategy; - -///#ENDDEBUG - -export default pointer; +export { default } from '../__internal/events/m_pointer'; diff --git a/packages/devextreme/js/events/pointer/base.js b/packages/devextreme/js/events/pointer/base.js index 75baf7ab8111..305dab9967e0 100644 --- a/packages/devextreme/js/events/pointer/base.js +++ b/packages/devextreme/js/events/pointer/base.js @@ -1,114 +1 @@ -import eventsEngine from '../../events/core/events_engine'; -import browser from '../../core/utils/browser'; -import domAdapter from '../../core/dom_adapter'; -import Class from '../../core/class'; -import { addNamespace, eventSource, fireEvent } from '../utils/index'; -import { getEventTarget } from '../utils/event_target'; - -const POINTER_EVENTS_NAMESPACE = 'dxPointerEvents'; - - -const BaseStrategy = Class.inherit({ - - ctor: function(eventName, originalEvents) { - this._eventName = eventName; - this._originalEvents = addNamespace(originalEvents, POINTER_EVENTS_NAMESPACE); - this._handlerCount = 0; - this.noBubble = this._isNoBubble(); - }, - - _isNoBubble: function() { - const eventName = this._eventName; - - return eventName === 'dxpointerenter' || - eventName === 'dxpointerleave'; - }, - - _handler: function(e) { - const delegateTarget = this._getDelegateTarget(e); - - const event = { - type: this._eventName, - pointerType: e.pointerType || eventSource(e), - originalEvent: e, - delegateTarget: delegateTarget, - // NOTE: TimeStamp normalization (FF bug #238041) (T277118) - timeStamp: browser.mozilla ? (new Date()).getTime() : e.timeStamp - }; - - const target = getEventTarget(e); - event.target = target; - - return this._fireEvent(event); - }, - - _getDelegateTarget: function(e) { - let delegateTarget; - - if(this.noBubble) { - delegateTarget = e.delegateTarget; - } - - return delegateTarget; - }, - - _fireEvent: function(args) { - return fireEvent(args); - }, - - _setSelector: function(handleObj) { - this._selector = this.noBubble && handleObj ? handleObj.selector : null; - }, - - _getSelector: function() { - return this._selector; - }, - - setup: function() { - return true; - }, - - add: function(element, handleObj) { - if(this._handlerCount <= 0 || this.noBubble) { - element = this.noBubble ? element : domAdapter.getDocument(); - this._setSelector(handleObj); - - const that = this; - eventsEngine.on(element, this._originalEvents, this._getSelector(), function(e) { - that._handler(e); - }); - } - - if(!this.noBubble) { - this._handlerCount++; - } - }, - - remove: function(handleObj) { - this._setSelector(handleObj); - - if(!this.noBubble) { - this._handlerCount--; - } - }, - - teardown: function(element) { - if(this._handlerCount && !this.noBubble) { - return; - } - - element = this.noBubble ? element : domAdapter.getDocument(); - - if(this._originalEvents !== '.' + POINTER_EVENTS_NAMESPACE) { - eventsEngine.off(element, this._originalEvents, this._getSelector()); - } - }, - - dispose: function(element) { - element = this.noBubble ? element : domAdapter.getDocument(); - - eventsEngine.off(element, this._originalEvents); - } -}); - -export default BaseStrategy; +export { default } from '../../__internal/events/pointer/m_base'; diff --git a/packages/devextreme/js/events/pointer/mouse.js b/packages/devextreme/js/events/pointer/mouse.js index 895bc0ccadb5..aaded4766988 100644 --- a/packages/devextreme/js/events/pointer/mouse.js +++ b/packages/devextreme/js/events/pointer/mouse.js @@ -1,61 +1 @@ -import { extend } from '../../core/utils/extend'; -import BaseStrategy from './base'; -import Observer from './observer'; - -const eventMap = { - 'dxpointerdown': 'mousedown', - 'dxpointermove': 'mousemove', - 'dxpointerup': 'mouseup', - 'dxpointercancel': '', - 'dxpointerover': 'mouseover', - 'dxpointerout': 'mouseout', - 'dxpointerenter': 'mouseenter', - 'dxpointerleave': 'mouseleave' -}; - -const normalizeMouseEvent = function(e) { - e.pointerId = 1; - - return { - pointers: observer.pointers(), - pointerId: 1 - }; -}; - - -let observer; -let activated = false; -const activateStrategy = function() { - if(activated) { - return; - } - - observer = new Observer(eventMap, function() { - return true; - }); - - activated = true; -}; - -const MouseStrategy = BaseStrategy.inherit({ - - ctor: function() { - this.callBase.apply(this, arguments); - - activateStrategy(); - }, - - _fireEvent: function(args) { - return this.callBase(extend(normalizeMouseEvent(args.originalEvent), args)); - } - -}); -MouseStrategy.map = eventMap; -MouseStrategy.normalize = normalizeMouseEvent; -MouseStrategy.activate = activateStrategy; -MouseStrategy.resetObserver = function() { - observer.reset(); -}; - - -export default MouseStrategy; +export { default } from '../../__internal/events/pointer/m_mouse'; diff --git a/packages/devextreme/js/events/pointer/mouse_and_touch.js b/packages/devextreme/js/events/pointer/mouse_and_touch.js index be242f223fcb..dde0522c0306 100644 --- a/packages/devextreme/js/events/pointer/mouse_and_touch.js +++ b/packages/devextreme/js/events/pointer/mouse_and_touch.js @@ -1,85 +1 @@ -import { extend } from '../../core/utils/extend'; -import BaseStrategy from './base'; -import MouseStrategy from './mouse'; -import TouchStrategy from './touch'; -import { isMouseEvent } from '../utils/index'; - -const eventMap = { - 'dxpointerdown': 'touchstart mousedown', - 'dxpointermove': 'touchmove mousemove', - 'dxpointerup': 'touchend mouseup', - 'dxpointercancel': 'touchcancel', - 'dxpointerover': 'mouseover', - 'dxpointerout': 'mouseout', - 'dxpointerenter': 'mouseenter', - 'dxpointerleave': 'mouseleave' -}; - - -let activated = false; -const activateStrategy = function() { - if(activated) { - return; - } - - MouseStrategy.activate(); - - activated = true; -}; - -const MouseAndTouchStrategy = BaseStrategy.inherit({ - - EVENT_LOCK_TIMEOUT: 100, - - ctor: function() { - this.callBase.apply(this, arguments); - - activateStrategy(); - }, - - _handler: function(e) { - const isMouse = isMouseEvent(e); - - if(!isMouse) { - this._skipNextEvents = true; - } - - if(isMouse && this._mouseLocked) { - return; - } - - if(isMouse && this._skipNextEvents) { - this._skipNextEvents = false; - this._mouseLocked = true; - - clearTimeout(this._unlockMouseTimer); - - const that = this; - this._unlockMouseTimer = setTimeout(function() { - that._mouseLocked = false; - }, this.EVENT_LOCK_TIMEOUT); - - return; - } - - return this.callBase(e); - }, - - _fireEvent: function(args) { - const normalizer = isMouseEvent(args.originalEvent) ? MouseStrategy.normalize : TouchStrategy.normalize; - - return this.callBase(extend(normalizer(args.originalEvent), args)); - }, - - dispose: function() { - this.callBase(); - this._skipNextEvents = false; - this._mouseLocked = false; - clearTimeout(this._unlockMouseTimer); - } -}); -MouseAndTouchStrategy.map = eventMap; -MouseAndTouchStrategy.resetObserver = MouseStrategy.resetObserver; - - -export default MouseAndTouchStrategy; +export { default } from '../../__internal/events/pointer/m_mouse_and_touch'; diff --git a/packages/devextreme/js/events/pointer/observer.js b/packages/devextreme/js/events/pointer/observer.js index 374d4b336a3c..6b5fefe472db 100644 --- a/packages/devextreme/js/events/pointer/observer.js +++ b/packages/devextreme/js/events/pointer/observer.js @@ -1,69 +1 @@ -import { each } from '../../core/utils/iterator'; -import readyCallbacks from '../../core/utils/ready_callbacks'; -import domAdapter from '../../core/dom_adapter'; - -const addEventsListener = function(events, handler) { - readyCallbacks.add(function() { - events - .split(' ') - .forEach(function(event) { - domAdapter.listen(domAdapter.getDocument(), event, handler, true); - }); - }); -}; - -const Observer = function(eventMap, pointerEquals, onPointerAdding) { - - onPointerAdding = onPointerAdding || function() { }; - - let pointers = []; - - const getPointerIndex = function(e) { - let index = -1; - - each(pointers, function(i, pointer) { - if(!pointerEquals(e, pointer)) { - return true; - } - - index = i; - return false; - }); - - return index; - }; - - const addPointer = function(e) { - if(getPointerIndex(e) === -1) { - onPointerAdding(e); - pointers.push(e); - } - }; - - const removePointer = function(e) { - const index = getPointerIndex(e); - if(index > -1) { - pointers.splice(index, 1); - } - }; - - const updatePointer = function(e) { - pointers[getPointerIndex(e)] = e; - }; - - addEventsListener(eventMap['dxpointerdown'], addPointer); - addEventsListener(eventMap['dxpointermove'], updatePointer); - addEventsListener(eventMap['dxpointerup'], removePointer); - addEventsListener(eventMap['dxpointercancel'], removePointer); - - this.pointers = function() { - return pointers; - }; - - this.reset = function() { - pointers = []; - }; - -}; - -export default Observer; +export { default } from '../../__internal/events/pointer/m_observer'; diff --git a/packages/devextreme/js/events/pointer/touch.js b/packages/devextreme/js/events/pointer/touch.js index 6b2969bf8d45..f41aa71ccc75 100644 --- a/packages/devextreme/js/events/pointer/touch.js +++ b/packages/devextreme/js/events/pointer/touch.js @@ -1,67 +1 @@ -import devices from '../../core/devices'; -import { extend } from '../../core/utils/extend'; -import { each } from '../../core/utils/iterator'; -import BaseStrategy from './base'; - -const eventMap = { - 'dxpointerdown': 'touchstart', - 'dxpointermove': 'touchmove', - 'dxpointerup': 'touchend', - 'dxpointercancel': 'touchcancel', - 'dxpointerover': '', - 'dxpointerout': '', - 'dxpointerenter': '', - 'dxpointerleave': '' -}; - - -const normalizeTouchEvent = function(e) { - const pointers = []; - - each(e.touches, function(_, touch) { - pointers.push(extend({ - pointerId: touch.identifier - }, touch)); - }); - - return { - pointers: pointers, - pointerId: e.changedTouches[0].identifier - }; -}; - -const skipTouchWithSameIdentifier = function(pointerEvent) { - return devices.real().platform === 'ios' && (pointerEvent === 'dxpointerdown' || pointerEvent === 'dxpointerup'); -}; - -const TouchStrategy = BaseStrategy.inherit({ - - ctor: function() { - this.callBase.apply(this, arguments); - this._pointerId = 0; - }, - - _handler: function(e) { - if(skipTouchWithSameIdentifier(this._eventName)) { - const touch = e.changedTouches[0]; - - if(this._pointerId === touch.identifier && this._pointerId !== 0) { - return; - } - - this._pointerId = touch.identifier; - } - - return this.callBase.apply(this, arguments); - }, - - _fireEvent: function(args) { - return this.callBase(extend(normalizeTouchEvent(args.originalEvent), args)); - } - -}); -TouchStrategy.map = eventMap; -TouchStrategy.normalize = normalizeTouchEvent; - - -export default TouchStrategy; +export { default } from '../../__internal/events/pointer/m_touch'; diff --git a/packages/devextreme/js/events/remove.js b/packages/devextreme/js/events/remove.js index ffde4455a3d6..b475e099cbec 100644 --- a/packages/devextreme/js/events/remove.js +++ b/packages/devextreme/js/events/remove.js @@ -1,11 +1,3 @@ -import $ from '../core/renderer'; -import { beforeCleanData } from '../core/element_data'; -import eventsEngine from './core/events_engine'; -import registerEvent from './core/event_registrator'; - -export const removeEvent = 'dxremove'; -const eventPropName = 'dxRemoveEvent'; - /** * @name UI Events.dxremove * @type eventType @@ -13,20 +5,4 @@ const eventPropName = 'dxRemoveEvent'; * @module events/remove */ -beforeCleanData(function(elements) { - elements = [].slice.call(elements); - for(let i = 0; i < elements.length; i++) { - const $element = $(elements[i]); - if($element.prop(eventPropName)) { - $element[0][eventPropName] = null; - eventsEngine.triggerHandler($element, removeEvent); - } - } -}); - -registerEvent(removeEvent, { - noBubble: true, - setup: function(element) { - $(element).prop(eventPropName, true); - } -}); +export * from '../__internal/events/m_remove'; diff --git a/packages/devextreme/js/events/short.js b/packages/devextreme/js/events/short.js index c47f8aead18b..70e38ebf6b7b 100644 --- a/packages/devextreme/js/events/short.js +++ b/packages/devextreme/js/events/short.js @@ -1,119 +1 @@ -import eventsEngine from './core/events_engine'; -import KeyboardProcessor from './core/keyboard_processor'; -import { addNamespace as pureAddNamespace } from './utils/index'; - -function addNamespace(event, namespace) { - return namespace ? pureAddNamespace(event, namespace) : event; -} - -function executeAction(action, args) { - return typeof action === 'function' ? action(args) : action.execute(args); -} - -export const active = { - on: ($el, active, inactive, opts) => { - const { selector, showTimeout, hideTimeout, namespace } = opts; - - eventsEngine.on($el, addNamespace('dxactive', namespace), selector, { timeout: showTimeout }, - event => executeAction(active, { event, element: event.currentTarget }) - ); - eventsEngine.on($el, addNamespace('dxinactive', namespace), selector, { timeout: hideTimeout }, - event => executeAction(inactive, { event, element: event.currentTarget }) - ); - }, - - off: ($el, { namespace, selector }) => { - eventsEngine.off($el, addNamespace('dxactive', namespace), selector); - eventsEngine.off($el, addNamespace('dxinactive', namespace), selector); - } -}; - -export const resize = { - on: ($el, resize, { namespace } = {}) => { - eventsEngine.on($el, addNamespace('dxresize', namespace), resize); - }, - off: ($el, { namespace } = {}) => { - eventsEngine.off($el, addNamespace('dxresize', namespace)); - } -}; - -export const hover = { - on: ($el, start, end, { selector, namespace }) => { - eventsEngine.on($el, addNamespace('dxhoverend', namespace), selector, event => end(event)); - eventsEngine.on($el, addNamespace('dxhoverstart', namespace), selector, - event => executeAction(start, { element: event.target, event })); - }, - - off: ($el, { selector, namespace }) => { - eventsEngine.off($el, addNamespace('dxhoverstart', namespace), selector); - eventsEngine.off($el, addNamespace('dxhoverend', namespace), selector); - } -}; - -export const visibility = { - on: ($el, shown, hiding, { namespace }) => { - eventsEngine.on($el, addNamespace('dxhiding', namespace), hiding); - eventsEngine.on($el, addNamespace('dxshown', namespace), shown); - }, - - off: ($el, { namespace }) => { - eventsEngine.off($el, addNamespace('dxhiding', namespace)); - eventsEngine.off($el, addNamespace('dxshown', namespace)); - } -}; - -export const focus = { - on: ($el, focusIn, focusOut, { namespace }) => { - eventsEngine.on($el, addNamespace('focusin', namespace), focusIn); - eventsEngine.on($el, addNamespace('focusout', namespace), focusOut); - }, - - off: ($el, { namespace }) => { - eventsEngine.off($el, addNamespace('focusin', namespace)); - eventsEngine.off($el, addNamespace('focusout', namespace)); - }, - - trigger: $el => eventsEngine.trigger($el, 'focus') -}; - -export const dxClick = { - on: ($el, click, { namespace } = {}) => { - eventsEngine.on($el, addNamespace('dxclick', namespace), click); - }, - off: ($el, { namespace } = {}) => { - eventsEngine.off($el, addNamespace('dxclick', namespace)); - } -}; - -export const click = { - on: ($el, click, { namespace } = {}) => { - eventsEngine.on($el, addNamespace('click', namespace), click); - }, - off: ($el, { namespace } = {}) => { - eventsEngine.off($el, addNamespace('click', namespace)); - } -}; - -let index = 0; -const keyboardProcessors = {}; -const generateListenerId = () => `keyboardProcessorId${index++}`; - -export const keyboard = { - on: (element, focusTarget, handler) => { - const listenerId = generateListenerId(); - - keyboardProcessors[listenerId] = new KeyboardProcessor({ element, focusTarget, handler }); - - return listenerId; - }, - - off: listenerId => { - if(listenerId && keyboardProcessors[listenerId]) { - keyboardProcessors[listenerId].dispose(); - delete keyboardProcessors[listenerId]; - } - }, - - // NOTE: For tests - _getProcessor: listenerId => keyboardProcessors[listenerId] -}; +export * from '../__internal/events/m_short'; diff --git a/packages/devextreme/js/events/swipe.js b/packages/devextreme/js/events/swipe.js index 3f5d8e65fe35..31e61bbd529e 100644 --- a/packages/devextreme/js/events/swipe.js +++ b/packages/devextreme/js/events/swipe.js @@ -1,171 +1,3 @@ -import { getWidth, getHeight } from '../core/utils/size'; -import { eventData } from './utils/index'; -import GestureEmitter from './gesture/emitter.gesture'; -import registerEmitter from './core/emitter_registrator'; - -const SWIPE_START_EVENT = 'dxswipestart'; -const SWIPE_EVENT = 'dxswipe'; -const SWIPE_END_EVENT = 'dxswipeend'; - - -const HorizontalStrategy = { - defaultItemSizeFunc: function() { - return getWidth(this.getElement()); - }, - - getBounds: function() { - return [ - this._maxLeftOffset, - this._maxRightOffset - ]; - }, - - calcOffsetRatio: function(e) { - const endEventData = eventData(e); - return (endEventData.x - (this._savedEventData && this._savedEventData.x || 0)) / this._itemSizeFunc().call(this, e); - }, - - isFastSwipe: function(e) { - const endEventData = eventData(e); - return this.FAST_SWIPE_SPEED_LIMIT * Math.abs(endEventData.x - this._tickData.x) >= (endEventData.time - this._tickData.time); - } -}; - -const VerticalStrategy = { - defaultItemSizeFunc: function() { - return getHeight(this.getElement()); - }, - - getBounds: function() { - return [ - this._maxTopOffset, - this._maxBottomOffset - ]; - }, - - calcOffsetRatio: function(e) { - const endEventData = eventData(e); - return (endEventData.y - (this._savedEventData && this._savedEventData.y || 0)) / this._itemSizeFunc().call(this, e); - }, - - isFastSwipe: function(e) { - const endEventData = eventData(e); - return this.FAST_SWIPE_SPEED_LIMIT * Math.abs(endEventData.y - this._tickData.y) >= (endEventData.time - this._tickData.time); - } -}; - - -const STRATEGIES = { - 'horizontal': HorizontalStrategy, - 'vertical': VerticalStrategy -}; - -const SwipeEmitter = GestureEmitter.inherit({ - - TICK_INTERVAL: 300, - FAST_SWIPE_SPEED_LIMIT: 10, - - ctor: function(element) { - this.callBase(element); - - this.direction = 'horizontal'; - this.elastic = true; - }, - - _getStrategy: function() { - return STRATEGIES[this.direction]; - }, - - _defaultItemSizeFunc: function() { - return this._getStrategy().defaultItemSizeFunc.call(this); - }, - - _itemSizeFunc: function() { - return this.itemSizeFunc || this._defaultItemSizeFunc; - }, - - _init: function(e) { - this._tickData = eventData(e); - }, - - _start: function(e) { - this._savedEventData = eventData(e); - - e = this._fireEvent(SWIPE_START_EVENT, e); - - if(!e.cancel) { - this._maxLeftOffset = e.maxLeftOffset; - this._maxRightOffset = e.maxRightOffset; - this._maxTopOffset = e.maxTopOffset; - this._maxBottomOffset = e.maxBottomOffset; - } - }, - - _move: function(e) { - const strategy = this._getStrategy(); - const moveEventData = eventData(e); - let offset = strategy.calcOffsetRatio.call(this, e); - - offset = this._fitOffset(offset, this.elastic); - - if(moveEventData.time - this._tickData.time > this.TICK_INTERVAL) { - this._tickData = moveEventData; - } - - this._fireEvent(SWIPE_EVENT, e, { - offset: offset, - }); - - if(e.cancelable !== false) { - e.preventDefault(); - } - }, - - _end: function(e) { - const strategy = this._getStrategy(); - const offsetRatio = strategy.calcOffsetRatio.call(this, e); - const isFast = strategy.isFastSwipe.call(this, e); - let startOffset = offsetRatio; - let targetOffset = this._calcTargetOffset(offsetRatio, isFast); - - startOffset = this._fitOffset(startOffset, this.elastic); - targetOffset = this._fitOffset(targetOffset, false); - - this._fireEvent(SWIPE_END_EVENT, e, { - offset: startOffset, - targetOffset: targetOffset - }); - }, - - _fitOffset: function(offset, elastic) { - const strategy = this._getStrategy(); - const bounds = strategy.getBounds.call(this); - - if(offset < -bounds[0]) { - return elastic ? (-2 * bounds[0] + offset) / 3 : -bounds[0]; - } - - if(offset > bounds[1]) { - return elastic ? (2 * bounds[1] + offset) / 3 : bounds[1]; - } - - return offset; - }, - - _calcTargetOffset: function(offsetRatio, isFast) { - let result; - if(isFast) { - result = Math.ceil(Math.abs(offsetRatio)); - if(offsetRatio < 0) { - result = -result; - } - } else { - result = Math.round(offsetRatio); - } - return result; - } -}); - /** * @name UI Events.dxswipestart * @type eventType @@ -190,17 +22,4 @@ const SwipeEmitter = GestureEmitter.inherit({ * @module events/swipe */ -registerEmitter({ - emitter: SwipeEmitter, - events: [ - SWIPE_START_EVENT, - SWIPE_EVENT, - SWIPE_END_EVENT - ] -}); - -export { - SWIPE_EVENT as swipe, - SWIPE_START_EVENT as start, - SWIPE_END_EVENT as end -}; +export * from '../__internal/events/m_swipe'; diff --git a/packages/devextreme/js/events/transform.js b/packages/devextreme/js/events/transform.js index 9e34d9c39b3f..db22a4cec363 100644 --- a/packages/devextreme/js/events/transform.js +++ b/packages/devextreme/js/events/transform.js @@ -1,154 +1,4 @@ -import { sign as mathSign, fitIntoRange } from '../core/utils/math'; -import * as iteratorUtils from '../core/utils/iterator'; -import { hasTouches } from './utils/index'; -import Emitter from './core/emitter'; -import registerEmitter from './core/emitter_registrator'; - -const DX_PREFIX = 'dx'; - -const TRANSFORM = 'transform'; -const TRANSLATE = 'translate'; -const PINCH = 'pinch'; -const ROTATE = 'rotate'; - -const START_POSTFIX = 'start'; -const UPDATE_POSTFIX = ''; -const END_POSTFIX = 'end'; - -const eventAliases = []; -const addAlias = function(eventName, eventArgs) { - eventAliases.push({ - name: eventName, - args: eventArgs - }); -}; - -addAlias(TRANSFORM, { - scale: true, - deltaScale: true, - rotation: true, - deltaRotation: true, - translation: true, - deltaTranslation: true -}); - -addAlias(TRANSLATE, { - translation: true, - deltaTranslation: true -}); - -addAlias(PINCH, { - scale: true, - deltaScale: true -}); - -addAlias(ROTATE, { - rotation: true, - deltaRotation: true -}); - - -const getVector = function(first, second) { - return { - x: second.pageX - first.pageX, - y: -second.pageY + first.pageY, - centerX: (second.pageX + first.pageX) * 0.5, - centerY: (second.pageY + first.pageY) * 0.5 - }; -}; - -const getEventVector = function(e) { - const pointers = e.pointers; - - return getVector(pointers[0], pointers[1]); -}; - -const getDistance = function(vector) { - return Math.sqrt(vector.x * vector.x + vector.y * vector.y); -}; - -const getScale = function(firstVector, secondVector) { - return getDistance(firstVector) / getDistance(secondVector); -}; - -const getRotation = function(firstVector, secondVector) { - const scalarProduct = firstVector.x * secondVector.x + firstVector.y * secondVector.y; - const distanceProduct = getDistance(firstVector) * getDistance(secondVector); - - if(distanceProduct === 0) { - return 0; - } - - const sign = mathSign(firstVector.x * secondVector.y - secondVector.x * firstVector.y); - const angle = Math.acos(fitIntoRange(scalarProduct / distanceProduct, -1, 1)); - - return sign * angle; -}; - -const getTranslation = function(firstVector, secondVector) { - return { - x: firstVector.centerX - secondVector.centerX, - y: firstVector.centerY - secondVector.centerY - }; -}; - -const TransformEmitter = Emitter.inherit({ - - validatePointers: function(e) { - return hasTouches(e) > 1; - }, - - start: function(e) { - this._accept(e); - - const startVector = getEventVector(e); - this._startVector = startVector; - this._prevVector = startVector; - - this._fireEventAliases(START_POSTFIX, e); - }, - - move: function(e) { - const currentVector = getEventVector(e); - const eventArgs = this._getEventArgs(currentVector); - - this._fireEventAliases(UPDATE_POSTFIX, e, eventArgs); - this._prevVector = currentVector; - }, - - end: function(e) { - const eventArgs = this._getEventArgs(this._prevVector); - this._fireEventAliases(END_POSTFIX, e, eventArgs); - }, - - _getEventArgs: function(vector) { - return { - scale: getScale(vector, this._startVector), - deltaScale: getScale(vector, this._prevVector), - rotation: getRotation(vector, this._startVector), - deltaRotation: getRotation(vector, this._prevVector), - translation: getTranslation(vector, this._startVector), - deltaTranslation: getTranslation(vector, this._prevVector) - }; - }, - - _fireEventAliases: function(eventPostfix, originalEvent, eventArgs) { - eventArgs = eventArgs || {}; - - iteratorUtils.each(eventAliases, (function(_, eventAlias) { - const args = {}; - iteratorUtils.each(eventAlias.args, function(name) { - if(name in eventArgs) { - args[name] = eventArgs[name]; - } - }); - - this._fireEvent(DX_PREFIX + eventAlias.name + eventPostfix, originalEvent, args); - }).bind(this)); - } - -}); - +import { exportNames } from '../__internal/events/m_transform'; /** * @name UI Events.dxtransformstart @@ -262,21 +112,6 @@ const TransformEmitter = Emitter.inherit({ * @module events/transform */ -const eventNames = eventAliases.reduce((result, eventAlias) => { - [START_POSTFIX, UPDATE_POSTFIX, END_POSTFIX].forEach(eventPostfix => { - result.push(DX_PREFIX + eventAlias.name + eventPostfix); - }); - return result; -}, []); - -registerEmitter({ - emitter: TransformEmitter, - events: eventNames -}); -const exportNames = {}; -iteratorUtils.each(eventNames, function(_, eventName) { - exportNames[eventName.substring(DX_PREFIX.length)] = eventName; -}); /* eslint-disable spellcheck/spell-checker */ export const { transformstart, @@ -293,5 +128,6 @@ export const { pinchend, rotatestart, rotate, - rotateend + rotateend, } = exportNames; + diff --git a/packages/devextreme/js/events/utils/add_namespace.js b/packages/devextreme/js/events/utils/add_namespace.js index c41b78ce9a03..ffa6a046fa06 100644 --- a/packages/devextreme/js/events/utils/add_namespace.js +++ b/packages/devextreme/js/events/utils/add_namespace.js @@ -1,21 +1 @@ -import errors from '../../core/errors'; - -const addNamespace = (eventNames, namespace) => { - if(!namespace) { - throw errors.Error('E0017'); - } - - if(Array.isArray(eventNames)) { - return eventNames - .map(eventName => addNamespace(eventName, namespace)) - .join(' '); - } - - if(eventNames.indexOf(' ') !== -1) { - return addNamespace(eventNames.split(/\s+/g), namespace); - } - - return `${eventNames}.${namespace}`; -}; - -export default addNamespace; +export { default } from '../../__internal/events/utils/m_add_namespace'; diff --git a/packages/devextreme/js/events/utils/event_nodes_disposing.js b/packages/devextreme/js/events/utils/event_nodes_disposing.js index ae6513a267ba..0dd3c94270d2 100644 --- a/packages/devextreme/js/events/utils/event_nodes_disposing.js +++ b/packages/devextreme/js/events/utils/event_nodes_disposing.js @@ -1,19 +1 @@ -import eventsEngine from '../core/events_engine'; -import { removeEvent } from '../remove'; - -function nodesByEvent(event) { - return event && [ - event.target, - event.delegateTarget, - event.relatedTarget, - event.currentTarget - ].filter(node => !!node); -} - -export const subscribeNodesDisposing = (event, callback) => { - eventsEngine.one(nodesByEvent(event), removeEvent, callback); -}; - -export const unsubscribeNodesDisposing = (event, callback) => { - eventsEngine.off(nodesByEvent(event), removeEvent, callback); -}; +export * from '../../__internal/events/utils/m_event_nodes_disposing'; diff --git a/packages/devextreme/js/events/utils/event_target.js b/packages/devextreme/js/events/utils/event_target.js index cb8c63726e6b..6b0f3d4dd2a0 100644 --- a/packages/devextreme/js/events/utils/event_target.js +++ b/packages/devextreme/js/events/utils/event_target.js @@ -1,18 +1 @@ -export const getEventTarget = (event) => { - const originalEvent = event.originalEvent; - - if(!originalEvent) { - return event.target; - } - - const isShadowDOMUsed = Boolean(originalEvent.target?.shadowRoot); - - if(!isShadowDOMUsed) { - return originalEvent.target; - } - - const path = originalEvent.path ?? originalEvent.composedPath?.(); - const target = path?.[0] ?? event.target; - - return target; -}; +export * from '../../__internal/events/utils/m_event_target'; diff --git a/packages/devextreme/js/events/utils/index.js b/packages/devextreme/js/events/utils/index.js index 3b24393d0fa5..4e21a47cee31 100644 --- a/packages/devextreme/js/events/utils/index.js +++ b/packages/devextreme/js/events/utils/index.js @@ -1,200 +1 @@ -import $ from '../../core/renderer'; -import mappedAddNamespace from './add_namespace'; -import eventsEngine from '../core/events_engine'; -import { each } from '../../core/utils/iterator'; -import { extend } from '../../core/utils/extend'; -import { focused } from '../../ui/widget/selectors'; - -const KEY_MAP = { - 'backspace': 'backspace', - 'tab': 'tab', - 'enter': 'enter', - 'escape': 'escape', - 'pageup': 'pageUp', - 'pagedown': 'pageDown', - 'end': 'end', - 'home': 'home', - 'arrowleft': 'leftArrow', - 'arrowup': 'upArrow', - 'arrowright': 'rightArrow', - 'arrowdown': 'downArrow', - 'delete': 'del', - ' ': 'space', - 'f': 'F', - 'a': 'A', - '*': 'asterisk', - '-': 'minus', - 'alt': 'alt', - 'control': 'control', - 'shift': 'shift', -}; - -const LEGACY_KEY_CODES = { - // iOS 10.2 and lower didn't supports KeyboardEvent.key - '8': 'backspace', - '9': 'tab', - '13': 'enter', - '27': 'escape', - '33': 'pageUp', - '34': 'pageDown', - '35': 'end', - '36': 'home', - '37': 'leftArrow', - '38': 'upArrow', - '39': 'rightArrow', - '40': 'downArrow', - '46': 'del', - '32': 'space', - '70': 'F', - '65': 'A', - '106': 'asterisk', - '109': 'minus', - '189': 'minus', - '173': 'minus', - '16': 'shift', - '17': 'control', - '18': 'alt' -}; - -const EVENT_SOURCES_REGEX = { - dx: /^dx/i, - mouse: /(mouse|wheel)/i, - touch: /^touch/i, - keyboard: /^key/i, - pointer: /^(ms)?pointer/i -}; - -let fixMethod = e => e; -const copyEvent = originalEvent => fixMethod(eventsEngine.Event(originalEvent, originalEvent), originalEvent); -const isDxEvent = e => eventSource(e) === 'dx'; -const isNativeMouseEvent = e => eventSource(e) === 'mouse'; -const isNativeTouchEvent = e => eventSource(e) === 'touch'; - -export const eventSource = ({ type }) => { - let result = 'other'; - - each(EVENT_SOURCES_REGEX, function(key) { - if(this.test(type)) { - result = key; - - return false; - } - }); - - return result; -}; - -export const isPointerEvent = e => eventSource(e) === 'pointer'; - -export const isMouseEvent = e => isNativeMouseEvent(e) || - ((isPointerEvent(e) || isDxEvent(e)) && e.pointerType === 'mouse'); - -export const isDxMouseWheelEvent = e => e && e.type === 'dxmousewheel'; - -export const isTouchEvent = e => isNativeTouchEvent(e) || - ((isPointerEvent(e) || isDxEvent(e)) && e.pointerType === 'touch'); - -export const isKeyboardEvent = e => eventSource(e) === 'keyboard'; - -export const isFakeClickEvent = ({ screenX, offsetX, pageX }) => screenX === 0 && !offsetX && pageX === 0; - -export const eventData = ({ pageX, pageY, timeStamp }) => ({ - x: pageX, - y: pageY, - time: timeStamp -}); - -export const eventDelta = (from, to) => ({ - x: to.x - from.x, - y: to.y - from.y, - time: to.time - from.time || 1 -}); - -export const hasTouches = e => { - const { originalEvent, pointers } = e; - - if(isNativeTouchEvent(e)) { - return (originalEvent.touches || []).length; - } - - if(isDxEvent(e)) { - return (pointers || []).length; - } - - return 0; -}; - -// TODO: for tests -let skipEvents = false; -export const forceSkipEvents = () => skipEvents = true; -export const stopEventsSkipping = () => skipEvents = false; - -export const needSkipEvent = e => { - // TODO: for tests - if(skipEvents) { - return true; - } - - // TODO: this checking used in swipeable first move handler. is it correct? - const { target } = e; - const $target = $(target); - const isContentEditable = target?.isContentEditable || target?.hasAttribute('contenteditable'); - const touchInEditable = $target.is('input, textarea, select') || isContentEditable; - - if(isDxMouseWheelEvent(e)) { - const isTextArea = $target.is('textarea') && $target.hasClass('dx-texteditor-input'); - - if(isTextArea || isContentEditable) { - return false; - } - - const isInputFocused = $target.is('input[type=\'number\'], textarea, select') && $target.is(':focus'); - - return isInputFocused; - } - - if(isMouseEvent(e)) { - return touchInEditable || e.which > 1; // only left mouse button - } - - if(isTouchEvent(e)) { - return touchInEditable && focused($target); - } -}; - -export const setEventFixMethod = func => fixMethod = func; - -export const createEvent = (originalEvent, args) => { - const event = copyEvent(originalEvent); - - args && extend(event, args); - - return event; -}; - -export const fireEvent = props => { - const { originalEvent, delegateTarget } = props; - const event = createEvent(originalEvent, props); - - eventsEngine.trigger(delegateTarget || event.target, event); - - return event; -}; - -export const normalizeKeyName = ({ key, which }) => { - const normalizedKey = KEY_MAP[key?.toLowerCase()] || key; - const normalizedKeyFromWhich = LEGACY_KEY_CODES[which]; - if(normalizedKeyFromWhich && normalizedKey === key) { - return normalizedKeyFromWhich; - } else if(!normalizedKey && which) { - return String.fromCharCode(which); - } - - return normalizedKey; -}; - -export const getChar = ({ key, which }) => key || String.fromCharCode(which); - -export const addNamespace = mappedAddNamespace; - -export const isCommandKeyPressed = ({ ctrlKey, metaKey }) => ctrlKey || metaKey; +export * from '../../__internal/events/utils/index'; diff --git a/packages/devextreme/js/events/visibility_change.js b/packages/devextreme/js/events/visibility_change.js index 623329c76d45..eae6f20c886a 100644 --- a/packages/devextreme/js/events/visibility_change.js +++ b/packages/devextreme/js/events/visibility_change.js @@ -1,22 +1,7 @@ -import $ from '../core/renderer'; -import eventsEngine from './core/events_engine'; +import VisibilityChangeModule from '../__internal/events/m_visibility_change'; -const triggerVisibilityChangeEvent = function(eventName) { - const VISIBILITY_CHANGE_SELECTOR = '.dx-visibility-change-handler'; +export const triggerShownEvent = VisibilityChangeModule.triggerShownEvent; +export const triggerHidingEvent = VisibilityChangeModule.triggerHidingEvent; +export const triggerResizeEvent = VisibilityChangeModule.triggerResizeEvent; - return function(element) { - const $element = $(element || 'body'); - - const changeHandlers = $element.filter(VISIBILITY_CHANGE_SELECTOR). - add($element.find(VISIBILITY_CHANGE_SELECTOR)); - - for(let i = 0; i < changeHandlers.length; i++) { - eventsEngine.triggerHandler(changeHandlers[i], eventName); - } - }; -}; - - -export const triggerShownEvent = triggerVisibilityChangeEvent('dxshown'); -export const triggerHidingEvent = triggerVisibilityChangeEvent('dxhiding'); -export const triggerResizeEvent = triggerVisibilityChangeEvent('dxresize'); +export default VisibilityChangeModule; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.events/dblclick.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.events/dblclick.tests.js index 8178c3e8966d..0d6d62db4230 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.events/dblclick.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.events/dblclick.tests.js @@ -1,6 +1,6 @@ const $ = require('jquery'); const dblclickEvent = require('events/dblclick'); -const { dblClick } = require('__internal/events/dblclick'); +const { dblClick } = require('__internal/events/m_dblclick'); const pointerMock = require('../../helpers/pointerMock.js'); QUnit.testStart(function() { @@ -44,7 +44,7 @@ QUnit.test('dxdblclick should not be handled on a usual dxclick even if dblClick el.off(dblclickEvent.name); el.on(dblclickEvent.name, handler); - el.on(dblclickEvent.name, () => {}); + el.on(dblclickEvent.name, () => { }); el.trigger('dxclick');