// import { Stage } from 'drag-drop-stage-component'; // this is not recognized by my IDE import { Stage } from '../../../node_modules/drag-drop-stage-component/src/ts/index'; import { ScrollData, SelectableState } from '../../../node_modules/drag-drop-stage-component/src/ts/Types'; import { Constants } from '../../Constants'; import { Body } from '../model/body'; import { Controller, Model } from '../types'; import { SilexNotification } from '../utils/notification'; export class StageWrapper { private stage: Stage; private dragging = false; private toBeUnsubscribed = []; /** * * @param element container to render the UI * @param model model class which holds * the model instances - views use it for * read operation only * @param controller structure which holds * the controller instances */ constructor(protected element: HTMLElement, protected model: Model, protected controller: Controller) {} redraw() { if (!this.stage) { return; } this.stage.redraw(); } reset() { if (!this.stage) { return; } this.stage.reset(this.model.page.getElementsForPage()); } resizeWindow() { if (!this.stage) { return; } this.stage.resizeWindow(); } addElement(element: HTMLElement) { if (!this.stage) { return; } this.stage.addElement(element); } removeElement(element: HTMLElement) { if (!this.stage) { return; } this.stage.removeElement(element); } center(elements: HTMLElement[]) { if (!this.stage) { return; } this.stage.center(elements); } getDropZone(posX: number, posY: number, element: HTMLElement): HTMLElement { if (!this.stage) { return this.model.body.getBodyElement(); } return this.stage.getDropZone(posX, posY, element); } getScroll(): ScrollData { if (!this.stage) { return {x: 0, y: 0}; } return this.stage.getScroll(); } setScroll(scroll: ScrollData) { if (!this.stage) { return; } this.stage.setScroll(scroll); } getEnableSticky(): boolean { if (!this.stage) { return false; } return this.stage.enableSticky; } setEnableSticky(enable: boolean) { if (!this.stage) { return; } this.stage.enableSticky = enable; } toggleSticky() { if (!this.stage) { return; } this.stage.enableSticky = !this.stage.enableSticky; } getState(el: HTMLElement): SelectableState { if (!this.stage) { return null; } return this.stage.getState(el); } setState(el: HTMLElement, state: SelectableState) { if (!this.stage) { return; } this.stage.setState(el, state); } getSelection(): SelectableState[] { if (!this.stage) { return []; } return this.stage.getSelection(); } setSelection(elements: HTMLElement[]) { if (!this.stage) { return; } this.stage.setSelection(elements); } getEditMode(): boolean { if (!this.stage) { return false; } return this.stage.catchingEvents; } setEditMode(mode: boolean) { if (!this.stage) { return; } if (this.stage.visible === mode) { this.stage.visible = !mode; } } cleanup() { // cleanup if (!!this.stage) { this.stage.cleanup(); this.stage = null; } this.toBeUnsubscribed.forEach((u) => u()); this.toBeUnsubscribed = []; } /** * safe subscribe to mouse event * handle the multiple iframes and the current window * @return function to call to unsubscribe */ subscribeMouseEvent(type: string, cbk: (e) => void): () => void { if (!this.stage) { return; } return this.stage.subscribeMouseEvent(type, cbk); } hideScrolls(hide: boolean) { if (!this.stage) { return; } this.stage.hideScrolls(hide); } init(iframe: HTMLIFrameElement) { this.cleanup(); this.stage = new Stage(iframe, [], { isSelectable: ((el) => this.model.page.isVisible(el) && !el.classList.contains(Constants.PREVENT_SELECTABLE_CLASS_NAME)), isDraggable: ((el) => !el.classList.contains(Constants.PREVENT_DRAGGABLE_CLASS_NAME)), isDropZone: ((el) => !el.classList.contains(Constants.PREVENT_DROPPABLE_CLASS_NAME) && el.classList.contains(Constants.TYPE_CONTAINER)), isResizeable: ((el) => { // section content is not resizeable on mobile const isSectionContentOnMobile = this.controller.stageController.getMobileMode() && this.model.element.isSectionContent(el); // css classes which prevent resize const hasPreventCssClass = el.classList.contains(Constants.PREVENT_RESIZABLE_CLASS_NAME); if (isSectionContentOnMobile || hasPreventCssClass) { return false; } // case of all or part of the sides are resizeable return { top: !el.classList.contains(Constants.PREVENT_RESIZABLE_TOP_CLASS_NAME), left: !el.classList.contains(Constants.PREVENT_RESIZABLE_LEFT_CLASS_NAME), bottom: !el.classList.contains(Constants.PREVENT_RESIZABLE_BOTTOM_CLASS_NAME), right: !el.classList.contains(Constants.PREVENT_RESIZABLE_RIGHT_CLASS_NAME), }; }), useMinHeight: ((el) => !el.classList.contains(Constants.SILEX_USE_HEIGHT_NOT_MINHEIGHT)), canDrop: ((el: HTMLElement, dropZone: HTMLElement) => { // sections can only be dropped in the body return !el.classList.contains(Constants.TYPE_SECTION) || dropZone.tagName.toLowerCase() === 'body'; }), onEdit: () => { if (!SilexNotification.isActive) { this.controller.editMenuController.editElement(); } }, onChange: (change) => this.applyStyle(change), onDrop: (change) => this.stopDrag(change), onResizeEnd: (change) => this.stopDrag(change, true), // onDrag: (change) => this.updateView(), // onResize: (change) => this.updateView(), onSelect: (change) => this.onSelectionChanged(change), onStartDrag: (change) => this.startDrag(), onStartResize: (change) => this.startDrag(), }); // give time to iframes to initialize setTimeout(() => { // TODO: this should go in a "selection" model this.toBeUnsubscribed.push( this.subscribeMouseEvent('mousedown', (e: MouseEvent) => { // reset focus when the stage is clicked if (window !== (e.target as HTMLElement).ownerDocument.defaultView) { Body.resetFocus(); } }), ); }, 0); } startDrag() { this.hideScrolls(true); this.dragging = true; this.prepareUndo(); } stopDrag(change, redraw = false) { this.hideScrolls(false); this.dragging = false; this.applyStyle(change); this.redraw(); this.updateView(); } prepareUndo() { this.controller.stageController.undoCheckPoint(); } updateView() { this.model.body.refreshViews(); } onSelectionChanged(_) { this.model.body.refreshViews(); } applyStyle(change) { // do not mess up the css translation applyed by stage during drag if (!this.dragging) { change.forEach((s) => { // removed the inline styles // these are all the properties that can be set by the stage component s.el.style.top = ''; s.el.style.left = ''; s.el.style.right = ''; s.el.style.bottom = ''; s.el.style.width = ''; s.el.style.height = ''; s.el.style.margin = ''; s.el.style.padding = ''; s.el.style.border = ''; s.el.style.minHeight = ''; s.el.style.position = ''; // get element style const styleObject = this.model.property.getStyle(s.el) || {}; // compute style styleObject[s.useMinHeight ? 'min-height' : 'height'] = s.metrics.computedStyleRect.height + 'px'; if (!this.model.element.isSection(s.el)) { // sections have no top, left, width if (!this.model.element.isSectionContent(s.el)) { // section contents have no top, left styleObject.top = s.metrics.computedStyleRect.top + 'px'; styleObject.left = s.metrics.computedStyleRect.left + 'px'; } // apply width styleObject.width = s.metrics.computedStyleRect.width + 'px'; } // apply styles this.model.property.setStyle(s.el, styleObject); }); this.updateView(); } } }