/** * [[include:plugins/inline-popup/README.md]] * @packageDocumentation * @module plugins/inline-popup */ import './inline-popup.less'; import type { Buttons, HTMLTagNames, IBound, IJodit, IPopup, IToolbarCollection, IViewComponent, Nullable } from 'jodit/types'; import { Plugin } from 'jodit/core/plugin'; import { makeCollection } from 'jodit/modules/toolbar/factory'; import { Popup } from 'jodit/core/ui/popup'; import { camelCase, isArray, isFunction, isString, keys, position, splitArray, toArray } from 'jodit/core/helpers'; import { Dom } from 'jodit/core/dom'; import { UIElement } from 'jodit/core/ui'; import type { Table } from 'jodit/modules/table/table'; import { autobind, debounce, wait, watch } from 'jodit/core/decorators'; import { pluginSystem } from 'jodit/core/global'; import './config/config'; export enum IframeType { VOTE = 'vote' } export enum InlinePopupAttribute { IFRAME_TYPE = 'iframe-type' } /** * Plugin for show inline popup dialog */ export class inlinePopup extends Plugin { override requires = ['select']; private type: Nullable = null; private popup: IPopup = new Popup(this.jodit, false, 'centerTop'); private toolbar: IToolbarCollection = makeCollection( this.jodit, this.popup ); private previousTarget?: HTMLElement; private snapRange: Nullable = null; private elmsList: string[] = keys(this.j.o.popup, false).filter( s => !this.isExcludedTarget(s) ); /** * Shortcut for Table module */ private get tableModule(): Table { return this.j.getInstance('Table', this.j.o); } @watch(':outsideClick') protected onOutsideClick(): void { this.popup.close(); } /** @override **/ protected afterInit(jodit: IJodit): void { this.j.e .on( 'getDiffButtons.mobile', (toolbar: IToolbarCollection): void | Buttons => { if (this.toolbar === toolbar) { const names = this.toolbar.getButtonsNames(); return toArray(jodit.registeredButtons) .filter( btn => !this.j.o.toolbarInlineDisabledButtons.includes( btn.name ) ) .filter(item => { const name = isString(item) ? item : item.name; return ( name && name !== '|' && name !== '\n' && !names.includes(name) ); }); } } ) .on('hidePopup', this.hidePopup) .on('showInlineToolbar', this.showInlineToolbar) .on( 'showPopup', ( elm: HTMLElement | string, rect: () => IBound, type?: string ) => { this.showPopup( rect, type || (isString(elm) ? elm : elm.nodeName), isString(elm) ? undefined : elm ); } ) .on('mousedown keydown', this.onSelectionStart) .on('change', () => { if ( this.popup.isOpened && this.previousTarget && !this.previousTarget.parentNode ) { this.hidePopup(); this.previousTarget = undefined; } }) .on([this.j.ew, this.j.ow], 'mouseup keyup', this.onSelectionEnd); this.addListenersForElements(); } /** @override **/ protected beforeDestruct(jodit: IJodit): void { jodit.e .off('showPopup') .off([this.j.ew, this.j.ow], 'mouseup keyup', this.onSelectionEnd); this.removeListenersForElements(); } @autobind private onClick(node: Node): void | false { const elements = this.elmsList as HTMLTagNames[], target = Dom.isTag(node, 'img') ? node : Dom.closest(node, elements, this.j.editor); // console.log('target: ', target); // console.log('elements: ', elements); if (target && this.canShowPopupForType(target.nodeName.toLowerCase())) { this.showPopup( () => position(target, this.j), target.nodeName.toLowerCase(), target, target?.getAttribute( InlinePopupAttribute.IFRAME_TYPE ) as IframeType | null ); return false; } } /** * Show inline popup with some toolbar * * @param type - selection, img, a etc. */ @wait((ctx: IViewComponent) => !ctx.j.isLocked) private showPopup( rect: () => IBound, type: string, target?: HTMLElement, // vote만의 인라인 팝업을 위해 새로 만듦 iframeType?: IframeType | null ): boolean { type = type.toLowerCase(); // 인라인 팝업이 나오는 타입인지 체크 if (!this.canShowPopupForType(type)) { return false; } // console.log('----------------------------'); // console.log(''); // console.log('type: ', type); // console.log('target: ', target); // console.log('this.j.o.popup: ', this.j.o.popup); if (this.type !== type || target !== this.previousTarget) { // 이전에 띄웠던 팝업 target 저장 this.previousTarget = target; // 오픈할 팝업 타입에 따라 데이터가 다름 let data; if (iframeType != null) { data = this.j.o.popup[iframeType]; } else { data = this.j.o.popup[type]; } let content; // setContent할 content 만들기 if (isFunction(data)) { content = data(this.j, target, this.popup.close); } else { content = data; } // console.log('data: ', data); // console.log('content: ', content); // console.log('this.toolbar: ', this.toolbar); // 배열일 경우 toolbar로 빌드 if (isArray(content)) { this.toolbar.build(content, target); this.toolbar.elements.map(i => { i.container.classList.add( `${i.componentName}_inline_popup` ); }); content = this.toolbar.container; } this.popup.setContent(content); this.type = type; } this.popup.open(rect); return true; } /** * Hide opened popup */ @watch(':clickEditor') @autobind private hidePopup(type?: string): void { // console.log(2, 'hidePopup'); if (!isString(type) || type === this.type) { this.popup.close(); } } /** * Can show popup for this type */ private canShowPopupForType(type: string): boolean { const data = this.j.o.popup[type.toLowerCase()]; if (this.j.o.readonly || !this.j.o.toolbarInline || !data) { return false; } return !this.isExcludedTarget(type); } /** * For some elements do not show popup */ private isExcludedTarget(type: string): boolean { return splitArray(this.j.o.toolbarInlineDisableFor) .map(a => a.toLowerCase()) .includes(type.toLowerCase()); } @autobind private onSelectionStart(): void { // console.log(5, 'onSelectionStart'); this.snapRange = this.j.s.range.cloneRange(); } @autobind private onSelectionEnd(e: MouseEvent): void { if ( e && e.target && UIElement.closestElement(e.target as Node, Popup) ) { return; } const { snapRange } = this, { range } = this.j.s; if ( !snapRange || range.collapsed || range.startContainer !== snapRange.startContainer || range.startOffset !== snapRange.startOffset || range.endContainer !== snapRange.endContainer || range.endOffset !== snapRange.endOffset ) { this.onSelectionChange(); } } /** * Selection change handler */ @debounce(ctx => ctx.defaultTimeout) private onSelectionChange(): void { if (!this.j.o.toolbarInlineForSelection) { return; } const type = 'selection', sel = this.j.s.sel, range = this.j.s.range; if ( sel?.isCollapsed || this.isSelectedTarget(range) || this.tableModule.getAllSelectedCells().length ) { if (this.type === type && this.popup.isOpened) { this.hidePopup(); } return; } const node = this.j.s.current(); if (!node) { return; } this.showPopup(() => range.getBoundingClientRect(), type); } /** * In not collapsed selection - only one image */ private isSelectedTarget(r: Range): boolean { const sc = r.startContainer; return ( Dom.isElement(sc) && sc === r.endContainer && Dom.isTag( sc.childNodes[r.startOffset], keys(this.j.o.popup, false) as any ) && r.startOffset === r.endOffset - 1 ); } private _eventsList(): string { const el = this.elmsList; // clickA // clickImg // clickCells // clickToolbar // clickJodit // clickIframe // clickJoditMedia // clickSelection // clickButton // touchstartA // touchstartImg // touchstartCells // touchstartToolbar // touchstartJodit // touchstartIframe // touchstartJoditMedia // touchstartSelection // touchstartButton return el .map(e => camelCase(`click_${e}`)) .concat(el.map(e => camelCase(`touchstart_${e}`))) .join(' '); } private addListenersForElements(): void { this.j.e.on(this._eventsList(), this.onClick); } private removeListenersForElements(): void { this.j.e.off(this._eventsList(), this.onClick); } /** * Show the inline WYSIWYG toolbar editor. */ @autobind private showInlineToolbar(bound?: IBound): void { // console.log(3, 'showInlineToolbar'); this.showPopup(() => { if (bound) { return bound; } const { range } = this.j.s; return range.getBoundingClientRect(); }, 'toolbar'); } } pluginSystem.add('inlinePopup', inlinePopup);