UNPKG

devexpress-richedit

Version:

DevExpress Rich Text Editor is an advanced word-processing tool designed for working with rich text documents.

407 lines (406 loc) 22.2 kB
import { EvtUtils } from '@devexpress/utils/lib/utils/evt'; import { LayoutPoint } from '../layout/layout-point'; import { Log } from '../rich-utils/debug/logger/base-logger/log'; import { LogSource } from '../rich-utils/debug/logger/base-logger/log-source'; import { Browser } from '@devexpress/utils/lib/browser'; import { BatchUpdatableObject } from '@devexpress/utils/lib/class/batch-updatable'; import { DomEventHandlersHolder } from '@devexpress/utils/lib/class/event-handlers-holder'; import { DomUtils } from '@devexpress/utils/lib/utils/dom'; import { PopupUtils } from '@devexpress/utils/lib/utils/popup'; import { RichMouseEvent } from '../event-manager'; import { MouseEventSource } from '../mouse-handler/mouse-event-source'; import { CursorPointer } from '../mouse-handler/mouse-handler/mouse-handler'; import { ResizeBoxListener } from './listeners/resize-box-listener'; import { SimpleViewCanvasSizeManager } from './renderes/common/document-renderer'; import { SizeUtils } from '../utils/size-utils'; import { MixedSize } from '../utils/mixed-size'; const SCROLL_INTERVAL_MS = 50; const CSSCLASS_FOCUSED = "dxreInFocus"; const AUTOSCROLL_AREA_SIZE = 10; const AUTOSCROLL_STEP = 10; const MSTOUCH_MOVE_SENSITIVITY = 5; export class CanvasManager extends BatchUpdatableObject { get sizes() { return this.viewManager.sizes; } get scroll() { return this.viewManager.scroll; } get controlHeightProvider() { return this.sizes; } constructor(viewManager, eventManager) { super(); this.lastMousePosition = { x: -1, y: -1 }; this.canvasPosition = { x: -1, y: -1 }; this.pointer = CursorPointer.Auto; this.blockNotPointerEvents = false; this.lastPointerPosition = { x: -1, y: -1 }; this.evtHandlersHolder = new DomEventHandlersHolder(); this.simpleViewCanvasSizeManager = new SimpleViewCanvasSizeManager(this, viewManager.control); this.viewManager = viewManager; this.eventManager = eventManager; this.initCommonEvents(); if (!Browser.WebKitTouchUI) this.initMouseEvents(); if (Browser.TouchUI) this.initTouchEvents(); if (Browser.MSTouchUI) if (Browser.MajorVersion > 10) this.initPointerEvents(); else this.initMSPointerEvents(); } get canvas() { return this.viewManager.canvas; } dispose() { this.evtHandlersHolder.removeAllListeners(); this.simpleViewCanvasSizeManager.dispose(); this.simpleViewCanvasSizeManager = null; } onUpdateUnlocked(_occurredEvents) { this.viewManager.canvasListener.updateVisibleParts(); } setCursorPointer(pointer) { Log.print(LogSource.CanvasManager, "setCursorPointer", () => `pointer: ${CursorPointer[pointer]}`); if (this.pointer === pointer) return; if (this.pointer !== CursorPointer.Auto) DomUtils.removeClassName(this.viewManager.canvas, CanvasManager.getCursorClassName(this.pointer)); const newClassName = CanvasManager.getCursorClassName(pointer); if (newClassName) DomUtils.addClassName(this.viewManager.canvas, newClassName); this.pointer = pointer; } closeDocument() { this.scroll.init(this.viewManager.canvas, this.sizes); } focusChanged(inFocus) { Log.print(LogSource.CanvasManager, "focusChanged", `to: ${inFocus}`); if (inFocus) DomUtils.addClassName(this.viewManager.canvas, CSSCLASS_FOCUSED); else DomUtils.removeClassName(this.viewManager.canvas, CSSCLASS_FOCUSED); } getCanvasWidth() { return SizeUtils.getClientWidth(this.viewManager.canvas) / this.viewManager.zoomLevel; } onCanvasMouseWheel(evt) { if (!this.viewManager.layout) return; const point = this.getLayoutPoint(evt, false); point.y += evt.deltaY; this.eventManager.mouseWheelEvent = true; this.eventManager.onMouseMove(new RichMouseEvent(evt, point, CanvasManager.getMouseEventSource(EvtUtils.getEventSource(evt)), this.scroll.lastScrollTop, this.scroll.lastScrollLeft)); this.eventManager.mouseWheelEvent = false; this.viewManager.canvasListener.updateVisibleParts(); } onCanvasMouseDown(evt) { Log.print(LogSource.CanvasManager, "onCanvasMouseDown", `evt.button: ${evt.button}, evt.buttons: ${evt.buttons}`); if (!this.blockNotPointerEvents) this.onCanvasMouseDownInternal(evt); EvtUtils.preventEvent(evt); } onCanvasMouseDownInternal(evt) { const point = this.getLayoutPoint(evt, true); this.eventManager.onMouseDown(new RichMouseEvent(evt, point, CanvasManager.getMouseEventSource(EvtUtils.getEventSource(evt)), this.scroll.lastScrollTop, this.scroll.lastScrollLeft)); this.saveMousePosition(evt); this.resetScrollInterval(); this.canvasPosition.x = DomUtils.getAbsolutePositionX(this.viewManager.canvas); this.canvasPosition.y = DomUtils.getAbsolutePositionY(this.viewManager.canvas); if (!point.isEmpty()) { this.scrollIntervalID = setInterval(() => { this.onScrollIntervalTick(); }, SCROLL_INTERVAL_MS); } } onCanvasMouseUp(evt) { Log.print(LogSource.CanvasManager, "onCanvasMouseUp", ""); if (!this.blockNotPointerEvents) this.onCanvasMouseUpInternal(evt); } onCanvasMouseUpInternal(evt) { this.eventManager.onMouseUp(new RichMouseEvent(evt, this.getLayoutPoint(evt, false), CanvasManager.getMouseEventSource(EvtUtils.getEventSource(evt)), this.scroll.lastScrollTop, this.scroll.lastScrollLeft)); this.resetScrollInterval(); } onCanvasMouseMove(evt) { if (!this.blockNotPointerEvents) this.onCanvasMouseMoveInternal(evt); } onCanvasMouseMoveInternal(evt) { this.eventManager.onMouseMove(new RichMouseEvent(evt, this.getLayoutPoint(evt, false), CanvasManager.getMouseEventSource(EvtUtils.getEventSource(evt)), this.scroll.lastScrollTop, this.scroll.lastScrollLeft)); } onCanvasMouseDblClick(evt) { this.eventManager.onMouseDblClick(new RichMouseEvent(evt, this.getLayoutPoint(evt, true), CanvasManager.getMouseEventSource(EvtUtils.getEventSource(evt)), this.scroll.lastScrollTop, this.scroll.lastScrollLeft)); return EvtUtils.preventEventAndBubble(evt); } onCanvasTouchStart(evt) { if (!this.blockNotPointerEvents) this.onCanvasTouchStartInternal(evt); return true; } onCanvasTouchStartInternal(evt) { this.saveMousePosition(evt); let richMouseEvent = new RichMouseEvent(evt, this.getLayoutPoint(evt, true), CanvasManager.getMouseEventSource(EvtUtils.getEventSource(evt)), this.scroll.lastScrollTop, this.scroll.lastScrollLeft); if (this.doubleTapStartDate && ((new Date()) - this.doubleTapStartDate) < 600) { this.doubleTapStartDate = null; this.onCanvasDoubleTap(richMouseEvent); } else { this.doubleTapStartDate = new Date(); this.eventManager.onTouchStart(richMouseEvent); } } onCanvasDoubleTap(evt) { this.eventManager.onDoubleTap(evt); } onCanvasTouchEnd(evt) { if (!this.blockNotPointerEvents) this.onCanvasTouchEndInternal(evt); EvtUtils.preventEventAndBubble(evt); } onCanvasTouchEndInternal(evt) { return this.eventManager.onTouchEnd(new RichMouseEvent(evt, this.getLayoutPoint(evt, false), CanvasManager.getMouseEventSource(EvtUtils.getEventSource(evt)), this.scroll.lastScrollTop, this.scroll.lastScrollLeft)); } onCanvasTouchMove(evt) { if (!this.blockNotPointerEvents) return this.onCanvasTouchMoveInternal(evt); return true; } onCanvasTouchMoveInternal(evt) { if (!this.eventManager.onTouchMove(new RichMouseEvent(evt, this.getLayoutPoint(evt, false), CanvasManager.getMouseEventSource(EvtUtils.getEventSource(evt)), this.scroll.lastScrollTop, this.scroll.lastScrollLeft))) { EvtUtils.preventEventAndBubble(evt); return; } return true; } onCanvasPointerDown(evt) { if (evt.pointerType == "mouse") this.onCanvasMouseDownInternal(evt); else if (evt.pointerType == "touch") this.onCanvasTouchStartInternal(evt); this.blockNotPointerEvents = true; this.lastPointerPosition.x = evt.x; this.lastPointerPosition.y = evt.y; } onCanvasPointerMove(evt) { if (Math.abs(evt.x - this.lastPointerPosition.x) > MSTOUCH_MOVE_SENSITIVITY || Math.abs(evt.y - this.lastPointerPosition.y) > MSTOUCH_MOVE_SENSITIVITY) { if (evt.pointerType == "mouse") this.onCanvasMouseMoveInternal(evt); else if (evt.pointerType == "touch") { this.onCanvasTouchMoveInternal(evt); return; } EvtUtils.preventEventAndBubble(evt); } } onCanvasPointerUp(evt) { if (evt.pointerType == "mouse") this.onCanvasMouseUpInternal(evt); else if (evt.pointerType == "touch") this.onCanvasTouchEndInternal(evt); setTimeout(() => { this.blockNotPointerEvents = false; }, 0); EvtUtils.preventEventAndBubble(evt); } onCanvasGestureStart(evt) { this.eventManager.onGestureStart(evt); } onDocumentMouseUp(evt) { if (DomUtils.isItParent(this.viewManager.canvas, EvtUtils.getEventSource(evt))) { if (!EvtUtils.isLeftButtonPressed(evt)) if (this.eventManager.shouldPreventContextMenuEvent) PopupUtils.preventContextMenu(evt); this.onCanvasMouseUp(evt); } else { this.eventManager.onMouseUp(new RichMouseEvent(evt, null, MouseEventSource.Undefined, this.scroll.lastScrollTop, this.scroll.lastScrollLeft)); this.resetScrollInterval(); } } onDocumentContextMenu(evt) { if (!this.viewManager.canvas.parentNode) return; if (this.shouldPreventContextMenuEvent(evt) && this.eventManager.shouldPreventContextMenuEvent) { PopupUtils.preventContextMenu(evt); return EvtUtils.cancelBubble(evt); } } shouldPreventContextMenuEvent(evt) { const eventSource = EvtUtils.getEventSource(evt); if (this.viewManager.control.isClientMode()) return DomUtils.isItParent(this.viewManager.canvas.parentNode, eventSource); else return DomUtils.isItParent(this.viewManager.canvas.parentNode.parentNode, eventSource); } onDocumentMouseMove(evt) { this.saveMousePosition(evt); } onDocumentTouchEnd(evt) { if (DomUtils.isItParent(this.viewManager.canvas, EvtUtils.getEventSource(evt))) return; this.eventManager.onTouchEnd(new RichMouseEvent(evt, null, MouseEventSource.Undefined, this.scroll.lastScrollTop, this.scroll.lastScrollLeft)); this.resetScrollInterval(); } onDocumentTouchMove(evt) { this.saveMousePosition(evt); } getScale(actualSize, originalSize) { return (actualSize != 0 && originalSize != 0 ? actualSize / originalSize : 1) * this.viewManager.zoomLevel; } getLayoutPoint(evt, checkScroll) { if (!this.viewManager.layout) return LayoutPoint.Empty(); const canvas = this.viewManager.canvas; const clientRect = canvas.getBoundingClientRect(); const scaleX = this.getScale(clientRect.width, canvas.offsetWidth); const scaleY = this.getScale(clientRect.height, canvas.offsetHeight); const clientX = MixedSize.fromUI(EvtUtils.getEventX(evt)); const clientY = MixedSize.fromUI(EvtUtils.getEventY(evt)); const canvasX = MixedSize.fromUI(DomUtils.getAbsolutePositionX(canvas)); const canvasY = MixedSize.fromUI(DomUtils.getAbsolutePositionY(canvas)); const offsetY = MixedSize.fromUI(canvas.scrollTop).addSize(clientY).subtractSize(canvasY).useScale(scaleY); const pageIndex = this.viewManager.layout.findPageIndexByOffsetY(offsetY.LayoutSize * scaleY, this.sizes); const visibleAreaWidth = MixedSize.fromUI(this.sizes.getVisibleAreaWidth(false)); const visibleAreaHeight = MixedSize.fromUI(this.sizes.getVisibleAreaHeight(false)); if (checkScroll) { const relativeX = new MixedSize().useScale(scaleX).addSize(canvasX).addSize(visibleAreaWidth).subtractSize(clientX); if (this.sizes.scrollYVisible && relativeX.LayoutSize < 0) return LayoutPoint.Empty(); const relativeY = new MixedSize().useScale(scaleY).addSize(canvasY).addSize(visibleAreaHeight).subtractSize(clientY); if (this.sizes.scrollXVisible && relativeY.LayoutSize < 0) return LayoutPoint.Empty(); } const layoutPage = this.viewManager.layout.pages[pageIndex]; const renderPageCacheElem = this.viewManager.cache[pageIndex]; if (!layoutPage || !renderPageCacheElem) return LayoutPoint.Empty(); const pageX = new MixedSize().useScale(scaleX).addUISize(canvas.scrollLeft).addSize(clientX).subtractSize(canvasX).subtractLayoutSize(renderPageCacheElem.page.offsetLeft).LayoutSize; const pageY = new MixedSize().useScale(scaleY).addSize(offsetY).subtractLayoutSize(this.sizes.getPageOffsetY(layoutPage)).LayoutSize; return new LayoutPoint(pageIndex, pageX, pageY); } isVisiblePosition(layoutPoint) { const layout = this.viewManager.layout; const zoomLevel = this.viewManager.zoomLevel; this.scroll.updatePageIndexesInfo(layout); if (layoutPoint.pageIndex < this.scroll.startVisiblePageIndex || layoutPoint.pageIndex > this.scroll.endVisiblePageIndex) return false; const pageY = MixedSize.fromLayout(this.sizes.getPageOffsetY(layout.pages[layoutPoint.pageIndex])).useScale(zoomLevel); const pageX = MixedSize.fromLayout(this.viewManager.cache[layoutPoint.pageIndex].page.offsetLeft).useScale(zoomLevel); const x = pageX.addLayoutSize(layoutPoint.x).UISize; const y = pageY.addLayoutSize(layoutPoint.y).UISize; return x >= this.scroll.lastScrollLeft && x <= this.sizes.getVisibleAreaWidth(false) + this.scroll.lastScrollLeft && y >= this.scroll.lastScrollTop && y <= this.sizes.getVisibleAreaHeight(false) + this.scroll.lastScrollTop; } initCommonEvents() { this.evtHandlersHolder.addListener(this.viewManager.canvas, "scroll", () => this.viewManager.canvasListener.onCanvasScroll()); this.evtHandlersHolder.addListener(this.viewManager.canvas, "focus", () => this.viewManager.control.focusManager.captureFocus()); } initMouseEvents() { this.evtHandlersHolder.addListener(this.viewManager.canvas, "mousedown", this.onCanvasMouseDown.bind(this)); this.evtHandlersHolder.addListener(this.viewManager.canvas, "mousemove", this.onCanvasMouseMove.bind(this)); this.evtHandlersHolder.addListener(this.viewManager.canvas, "dblclick", this.onCanvasMouseDblClick.bind(this)); this.evtHandlersHolder.addListener(this.viewManager.canvas, EvtUtils.getMouseWheelEventName(), this.onCanvasMouseWheel.bind(this), { passive: true }); this.evtHandlersHolder.addListenerToDocument("mouseup", this.onDocumentMouseUp.bind(this)); this.evtHandlersHolder.addListenerToDocument("mousemove", this.onDocumentMouseMove.bind(this)); this.evtHandlersHolder.addListenerToDocument("contextmenu", this.onDocumentContextMenu.bind(this)); } initTouchEvents() { this.evtHandlersHolder.addListener(this.viewManager.canvas, "touchstart", this.onCanvasTouchStart.bind(this)); this.evtHandlersHolder.addListener(this.viewManager.canvas, "touchend", this.onCanvasTouchEnd.bind(this)); this.evtHandlersHolder.addListener(this.viewManager.canvas, "touchmove", this.onCanvasTouchMove.bind(this)); this.evtHandlersHolder.addListener(this.viewManager.canvas, "gesturestart", this.onCanvasGestureStart.bind(this)); this.evtHandlersHolder.addListenerToDocument("touchend", this.onDocumentTouchEnd.bind(this)); this.evtHandlersHolder.addListenerToDocument("touchmove", this.onDocumentTouchMove.bind(this)); } initPointerEvents() { this.evtHandlersHolder.addListener(this.viewManager.canvas, "pointerdown", this.onCanvasPointerDown.bind(this)); this.evtHandlersHolder.addListener(this.viewManager.canvas, "pointermove", this.onCanvasPointerMove.bind(this)); this.evtHandlersHolder.addListener(this.viewManager.canvas, "pointerup", this.onCanvasPointerUp.bind(this)); } initMSPointerEvents() { this.evtHandlersHolder.addListener(this.viewManager.canvas, "mspointerdown", this.onCanvasPointerDown.bind(this)); this.evtHandlersHolder.addListener(this.viewManager.canvas, "mspointermove", this.onCanvasPointerMove.bind(this)); this.evtHandlersHolder.addListener(this.viewManager.canvas, "mspointerup", this.onCanvasPointerUp.bind(this)); } resetScrollInterval() { if (this.scrollIntervalID) { clearInterval(this.scrollIntervalID); this.scrollIntervalID = null; } } saveMousePosition(evt) { this.lastMousePosition.x = EvtUtils.getEventX(evt); this.lastMousePosition.y = EvtUtils.getEventY(evt); } onScrollIntervalTick() { const evtX = this.lastMousePosition.x; const evtY = this.lastMousePosition.y; const inHorizontalArea = evtX >= this.canvasPosition.x && evtX <= this.canvasPosition.x + this.sizes.getVisibleAreaWidth(false); const inVerticalArea = evtY >= this.canvasPosition.y && evtY <= this.canvasPosition.y + this.sizes.getVisibleAreaHeight(false); if (!inHorizontalArea && !inVerticalArea) return; const yOffsetWithoutScrollbar = this.canvasPosition.y + this.sizes.getVisibleAreaHeight(false) - evtY; const yOffsetWithScrollbar = this.canvasPosition.y + this.sizes.getVisibleAreaHeight(true) - evtY; const outsideHorizontalScrollbar = yOffsetWithoutScrollbar > 0 || yOffsetWithScrollbar < 0; if (inHorizontalArea && evtY - this.canvasPosition.y <= AUTOSCROLL_AREA_SIZE) this.viewManager.canvas.scrollTop -= AUTOSCROLL_STEP; else if (inHorizontalArea && yOffsetWithoutScrollbar <= AUTOSCROLL_AREA_SIZE && outsideHorizontalScrollbar) this.viewManager.canvas.scrollTop += AUTOSCROLL_STEP; const xOffsetWithoutScrollbar = this.canvasPosition.x + this.sizes.getVisibleAreaWidth(false) - evtX; const xOffsetWithScrollbar = this.canvasPosition.x + this.sizes.getVisibleAreaWidth(true) - evtX; const outsideVerticalScrollbar = xOffsetWithoutScrollbar > 0 || xOffsetWithScrollbar < 0; if (inVerticalArea && evtX - this.canvasPosition.x <= AUTOSCROLL_AREA_SIZE) this.viewManager.canvas.scrollLeft -= AUTOSCROLL_STEP; else if (inVerticalArea && xOffsetWithoutScrollbar <= AUTOSCROLL_AREA_SIZE && outsideVerticalScrollbar) this.viewManager.canvas.scrollLeft += AUTOSCROLL_STEP; } static getCursorClassName(pointer) { switch (pointer) { case CursorPointer.Copy: return "dxreCursorCopy"; case CursorPointer.NoDrop: return "dxreCursorNoDrop"; case CursorPointer.EResize: return "dxreCursorEResize"; case CursorPointer.NResize: return "dxreCursorNResize"; case CursorPointer.SResize: return "dxreCursorSResize"; case CursorPointer.WResize: return "dxreCursorWResize"; case CursorPointer.SEResize: return "dxreCursorSEResize"; case CursorPointer.SWResize: return "dxreCursorSWResize"; case CursorPointer.NWResize: return "dxreCursorNWResize"; case CursorPointer.NEResize: return "dxreCursorNEResize"; case CursorPointer.NSResize: return "dxreCursorNSResize"; case CursorPointer.EWResize: return "dxreCursorEWResize"; case CursorPointer.Move: case CursorPointer.Default: return "dxreCursorDefault"; } } static getMouseEventSource(initSource) { const source = initSource.nodeType === Node.ELEMENT_NODE ? initSource : initSource.parentNode; const className = source.className; const cornerPrefix = ResizeBoxListener.getCornerPrefix(); const ind = className.indexOf(cornerPrefix); if (ind != 0) return MouseEventSource.Undefined; return ResizeBoxListener.directionToSource[className.substr(ind + cornerPrefix.length, 2).trim()]; } getScrollTopInfo() { const scrollTop = this.viewManager.canvas.scrollTop; const pageIndex = this.viewManager.layout.findPageIndexByOffsetY(scrollTop, this.sizes); const pages = this.viewManager.layout.pages; const zoomLevel = this.viewManager.zoomLevel; const pageOffsetY = this.sizes.getPageOffsetY(pages[pageIndex]); return new ScrollTopInfo(pageIndex, new MixedSize().useScale(zoomLevel).addUISize(scrollTop).subtractLayoutSize(pageOffsetY).LayoutSize); } } export class ScrollTopInfo { constructor(pageIndex, topPositionRelativePage) { this.pageIndex = pageIndex; this.topPositionRelativePage = topPositionRelativePage; } }