Server IP : 80.87.202.40 / Your IP : 216.73.216.169 Web Server : Apache System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64 User : bitrix ( 600) PHP Version : 8.2.27 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/bitrix/ext_www/cvetdv.ru/bitrix/js/im/v2/component/textarea/src/ |
Upload File : |
import 'ui.icon-set.outline'; import { Extension, Type } from 'main.core'; import { BaseEvent, EventEmitter } from 'main.core.events'; import { BIcon, Outline as OutlineIcons } from 'ui.icon-set.api.vue'; import { EventType, LocalStorageKey, SoundType, TextareaPanelType as PanelType, Color } from 'im.v2.const'; import { Analytics } from 'im.v2.lib.analytics'; import { Logger } from 'im.v2.lib.logger'; import { DraftManager } from 'im.v2.lib.draft'; import { Utils } from 'im.v2.lib.utils'; import { Parser } from 'im.v2.lib.parser'; import { LocalStorageManager } from 'im.v2.lib.local-storage'; import { UploadingService, MultiUploadingService } from 'im.v2.provider.service.uploading'; import { SendingService } from 'im.v2.provider.service.sending'; import { MessageService } from 'im.v2.provider.service.message'; import { SoundNotificationManager } from 'im.v2.lib.sound-notification'; import { isSendMessageCombination, isNewLineCombination } from 'im.v2.lib.hotkey'; import { Textarea } from 'im.v2.lib.textarea'; import { ChannelManager } from 'im.v2.lib.channel'; import { InputAction } from 'im.v2.lib.input-action'; import { MentionManager, MentionManagerEvents } from './classes/mention-manager'; import { InputSenderService } from './classes/input-sender-service'; import { ResizeDirection, ResizeManager } from './classes/resize-manager'; import { AudioInput } from './components/audio-input/audio-input'; import { SmileSelector } from './components/smile-selector/smile-selector'; import { UploadMenu } from './components/upload-menu/upload-menu'; import { SendButton } from './components/send-button'; import { UploadPreviewPopup } from './components/upload-preview/upload-preview-popup'; import { MentionPopup } from './components/mention/mention-popup'; import { TextareaPanel } from './components/panel/panel'; import { AutoDeleteSelector } from './components/auto-delete-selector/auto-delete-selector'; import './css/textarea.css'; import type { JsonObject } from 'main.core'; import type { ImModelChat, ImModelMessage } from 'im.v2.model'; import type { InsertTextEvent, InsertMentionEvent } from 'im.v2.const'; import type { ForwardedEntityConfig, PanelContextWithMultipleIds } from 'im.v2.provider.service.sending'; import type { MultiUploadingResult } from 'im.v2.provider.service.uploading'; const MESSAGE_ACTION_PANELS = new Set([PanelType.edit, PanelType.reply, PanelType.forward, PanelType.forwardEntity]); const TextareaHeight = { max: 400, min: 22, }; const ICON_SIZE = 24; // @vue/component export const ChatTextarea = { components: { UploadMenu, SmileSelector, SendButton, UploadPreviewPopup, MentionPopup, TextareaPanel, AudioInput, AutoDeleteSelector, BIcon, }, props: { dialogId: { type: String, default: '', }, placeholder: { type: String, default: '', }, withCreateMenu: { type: Boolean, default: true, }, withMarket: { type: Boolean, default: true, }, withEdit: { type: Boolean, default: true, }, withUploadMenu: { type: Boolean, default: true, }, withSmileSelector: { type: Boolean, default: true, }, withAudioInput: { type: Boolean, default: true, }, withAutoFocus: { type: Boolean, default: true, }, }, emits: ['mounted'], data(): JsonObject { return { text: '', textareaHeight: TextareaHeight.min, showMention: false, mentionQuery: '', showUploadPreviewPopup: false, previewPopupUploaderIds: [], previewPopupSourceFilesCount: 0, panelType: PanelType.none, panelContext: { messageId: 0, }, }; }, computed: { OutlineIcons: () => OutlineIcons, ICON_SIZE: () => ICON_SIZE, dialog(): ImModelChat { return this.$store.getters['chats/get'](this.dialogId, true); }, dialogInited(): boolean { return this.dialog.inited; }, replyMode(): boolean { return this.panelType === PanelType.reply; }, forwardMode(): boolean { return this.panelType === PanelType.forward; }, forwardEntityMode(): boolean { return this.panelType === PanelType.forwardEntity; }, editMode(): boolean { return this.panelType === PanelType.edit; }, marketMode(): boolean { return this.panelType === PanelType.market; }, isDisabled(): boolean { return this.text.trim() === '' && !this.editMode && !this.forwardMode && !this.forwardEntityMode; }, textareaPlaceholder(): string { if (!this.placeholder) { return this.loc('IM_TEXTAREA_PLACEHOLDER_V3'); } return this.placeholder; }, textareaStyle(): Object { let height = `${this.textareaHeight}px`; if (this.textareaHeight === 'auto') { height = 'auto'; } return { height, maxHeight: height, }; }, textareaMaxLength(): number { const settings = Extension.getSettings('im.v2.component.textarea'); return settings.get('maxLength'); }, isChannelType(): boolean { return ChannelManager.isChannel(this.dialogId); }, isEmptyText(): boolean { return this.text === ''; }, isAutoDeleteEnabled(): boolean { return this.$store.getters['chats/autoDelete/isEnabled'](this.dialog.chatId); }, marketIconColor(): string { if (this.marketMode) { return Color.accentBlue; } return Color.gray40; }, }, watch: { text(newValue) { this.adjustTextareaHeight(); this.getDraftManager().setDraftText(this.dialogId, newValue); if (Type.isStringFilled(newValue)) { this.getInputActionService().startAction(InputAction.writing); } }, }, created() { this.initResizeManager(); this.restoreTextareaHeight(); void this.restorePanel(); this.initSendingService(); EventEmitter.subscribe(EventType.textarea.insertMention, this.onInsertMention); EventEmitter.subscribe(EventType.textarea.insertText, this.onInsertText); EventEmitter.subscribe(EventType.textarea.editMessage, this.onEditMessage); EventEmitter.subscribe(EventType.textarea.replyMessage, this.onReplyMessage); EventEmitter.subscribe(EventType.textarea.forwardEntity, this.onForwardEntity); EventEmitter.subscribe(EventType.textarea.sendMessage, this.onSendMessage); EventEmitter.subscribe(EventType.textarea.insertForward, this.onInsertForward); EventEmitter.subscribe(EventType.textarea.openUploadPreview, this.onOpenUploadPreview); EventEmitter.subscribe(EventType.dialog.onMessageDeleted, this.onMessageDeleted); }, mounted() { void this.initMentionManager(); if (this.withAutoFocus) { this.focus(); } this.$emit('mounted'); }, beforeUnmount() { this.resizeManager.destroy(); this.unbindUploadingService(); EventEmitter.unsubscribe(EventType.textarea.insertMention, this.onInsertMention); EventEmitter.unsubscribe(EventType.textarea.insertText, this.onInsertText); EventEmitter.unsubscribe(EventType.textarea.editMessage, this.onEditMessage); EventEmitter.unsubscribe(EventType.textarea.replyMessage, this.onReplyMessage); EventEmitter.unsubscribe(EventType.textarea.forwardEntity, this.onForwardEntity); EventEmitter.unsubscribe(EventType.textarea.sendMessage, this.onSendMessage); EventEmitter.unsubscribe(EventType.textarea.insertForward, this.onInsertForward); EventEmitter.unsubscribe(EventType.textarea.openUploadPreview, this.onOpenUploadPreview); EventEmitter.unsubscribe(EventType.dialog.onMessageDeleted, this.onMessageDeleted); }, methods: { sendMessage() { this.text = this.text.trim(); if (this.isDisabled || !this.dialogInited) { return; } const text = this.mentionManager.replaceMentions(this.text); if (this.hasActiveMessageAction()) { this.handlePanelAction(text); this.closePanel(); } else { this.getSendingService().sendMessage({ text, dialogId: this.dialogId }); } this.getInputActionService().stopAction(InputAction.writing); this.clear(); this.getDraftManager().clearDraft(this.dialogId); SoundNotificationManager.getInstance().playOnce(SoundType.send); this.focus(); EventEmitter.emit(EventType.textarea.onAfterSendMessage); }, handlePanelAction(text: string) { if (this.editMode && text === '') { this.getMessageService().deleteMessages([this.panelContext.messageId]); } else if (this.editMode && text !== '') { this.getMessageService().editMessageText(this.panelContext.messageId, text); } else if (this.forwardMode) { void this.getSendingService().forwardMessages({ text, dialogId: this.dialogId, forwardIds: this.panelContext.messagesIds, }); } else if (this.forwardEntityMode) { console.error('sending forwarded entity message'); } else if (this.replyMode) { this.getSendingService().sendMessage({ text, dialogId: this.dialogId, replyId: this.panelContext.messageId, }); } }, clear() { this.text = ''; this.mentionManager?.clearMentionReplacements(); }, hasActiveMessageAction(): boolean { return MESSAGE_ACTION_PANELS.has(this.panelType); }, closePanel() { if (this.editMode) { this.clear(); } this.panelType = PanelType.none; this.panelContext = { messageId: 0, }; this.draftManager.setDraftPanel(this.dialogId, this.panelType, this.panelContext); }, openEditPanel(messageId: number) { if (!this.withEdit) { return; } const message: ImModelMessage = this.$store.getters['messages/getById'](messageId); if (message.isDeleted) { return; } this.panelType = PanelType.edit; this.panelContext.messageId = messageId; const mentions = this.mentionManager.extractMentions(message.text); this.mentionManager.setMentionReplacements(mentions); this.text = Parser.prepareEdit(message); this.focus(); this.draftManager.setDraftText(this.dialogId, this.text); this.draftManager.setDraftPanel(this.dialogId, this.panelType, this.panelContext); this.draftManager.setDraftMentions(this.dialogId, mentions); }, openReplyPanel(messageId: number) { if (this.editMode) { this.clear(); } this.panelType = PanelType.reply; this.panelContext.messageId = messageId; this.focus(); this.draftManager.setDraftPanel(this.dialogId, this.panelType, this.panelContext); }, openForwardPanel(messagesIds: number[]) { this.panelType = PanelType.forward; this.panelContext.messageId = 0; this.panelContext.messagesIds = messagesIds; this.clear(); this.focus(); this.draftManager.setDraftPanel(this.dialogId, this.panelType, this.panelContext); }, async openForwardEntityPanel(entityConfig: ForwardedEntityConfig) { this.panelType = PanelType.forwardEntity; this.panelContext.messageId = 0; this.panelContext.entityConfig = entityConfig; this.clear(); this.focus(); }, toggleMarketPanel() { if (this.marketMode) { this.panelType = PanelType.none; return; } this.panelType = PanelType.market; this.panelContext.messageId = 0; }, async adjustTextareaHeight() { this.textareaHeight = 'auto'; await this.$nextTick(); const newMaxPoint = Math.min(TextareaHeight.max, this.$refs.textarea.scrollHeight); if (this.resizedTextareaHeight) { this.textareaHeight = Math.max(newMaxPoint, this.resizedTextareaHeight); return; } this.textareaHeight = Math.max(newMaxPoint, TextareaHeight.min); }, saveTextareaHeight() { const WRITE_TO_STORAGE_TIMEOUT = 200; clearTimeout(this.saveTextareaTimeout); this.saveTextareaTimeout = setTimeout(() => { LocalStorageManager.getInstance().set(LocalStorageKey.textareaHeight, this.resizedTextareaHeight); }, WRITE_TO_STORAGE_TIMEOUT); }, restoreTextareaHeight() { const rawSavedHeight = LocalStorageManager.getInstance().get(LocalStorageKey.textareaHeight); const savedHeight = Number.parseInt(rawSavedHeight, 10); if (!savedHeight) { return; } this.resizedTextareaHeight = savedHeight; this.textareaHeight = savedHeight; }, checkMessageExists(messageId: number): boolean { return this.$store.getters['messages/isExists'](messageId); }, verifyPanelContext(panelContext: PanelContextWithMultipleIds): boolean { if (panelContext.messagesIds) { return panelContext.messagesIds.every((messageId) => this.checkMessageExists(messageId)); } return this.checkMessageExists(panelContext.messageId); }, async restorePanel() { const { text = '', panelType = PanelType.none, panelContext = { messageId: 0, }, } = await this.getDraftManager().getDraft(this.dialogId); const noPanel = this.panelType === PanelType.none; if (!noPanel && !this.verifyPanelContext(panelContext)) { return; } this.text = text; if (noPanel) { this.panelType = panelType; } this.panelContext = panelContext; }, async onKeyDown(event: KeyboardEvent) { Analytics.getInstance().onTypeMessage(this.dialog); if (this.showMention) { this.mentionManager.onActiveMentionKeyDown(event); return; } const exitActionCombination = Utils.key.isCombination(event, 'Escape'); if (this.hasActiveMessageAction() && exitActionCombination) { this.closePanel(); return; } const sendMessageCombination = isSendMessageCombination(event); const newLineCombination = isNewLineCombination(event); if (sendMessageCombination && !newLineCombination) { event.preventDefault(); this.sendMessage(); return; } if (newLineCombination) { this.handleNewLine(); return; } const tabCombination = Utils.key.isCombination(event, 'Tab'); if (tabCombination) { this.handleTab(event); return; } const decorationCombination = Utils.key.isExactCombination(event, ['Ctrl+b', 'Ctrl+i', 'Ctrl+u', 'Ctrl+s']); if (decorationCombination) { event.preventDefault(); this.text = Textarea.handleDecorationTag(this.$refs.textarea, event.code); return; } if (this.text === '' && Utils.key.isCombination(event, 'ArrowUp')) { this.handleLastOwnMessageEdit(event); return; } this.mentionManager.onKeyDown(event); }, handleNewLine() { this.text = Textarea.addNewLine(this.$refs.textarea); }, handleTab(event: KeyboardEvent) { event.preventDefault(); if (event.shiftKey) { this.text = Textarea.removeTab(this.$refs.textarea); return; } this.text = Textarea.addTab(this.$refs.textarea); }, handleLastOwnMessageEdit(event: KeyboardEvent) { event.preventDefault(); const lastOwnMessageId = this.$store.getters['messages/getLastOwnMessageId'](this.dialog.chatId); const isForward = this.$store.getters['messages/isForward'](lastOwnMessageId); if (lastOwnMessageId && !isForward) { this.openEditPanel(lastOwnMessageId); } }, onSendMessage(event: BaseEvent<{ text: string, dialogId: string }>) { const { text, dialogId } = event.getData(); if (this.dialogId !== dialogId) { return; } this.getSendingService().sendMessage({ text, dialogId: this.dialogId }); }, onResizeStart(event) { this.resizeManager.onResizeStart(event, this.textareaHeight); }, async onFileSelect({ event, sendAsFile }: Event) { const multiUploadingService: MultiUploadingService = this.getMultiUploadingService(); const multiUploadingResult: MultiUploadingResult = await multiUploadingService.uploadFromInput({ event, sendAsFile, dialogId: this.dialogId, autoUpload: false, }); this.showUploadPreviewPopup = true; this.previewPopupUploaderIds = multiUploadingResult.uploaderIds; this.previewPopupSourceFilesCount = multiUploadingResult.sourceFilesCount; }, onDiskFileSelect({ files }) { this.getUploadingService().uploadFileFromDisk(files, this.dialogId); }, onInsertMention(event: BaseEvent<InsertMentionEvent>) { const { mentionText, mentionReplacement, dialogId, isMentionSymbol = true } = event.getData(); let { textToReplace = '' } = event.getData(); if (this.dialogId !== dialogId) { return; } const mentions = this.mentionManager.addMentionReplacement(mentionText, mentionReplacement); this.draftManager.setDraftMentions(this.dialogId, mentions); const mentionSymbol = isMentionSymbol ? this.mentionManager.getMentionSymbol() : ''; textToReplace = `${mentionSymbol}${textToReplace}`; this.text = Textarea.insertMention(this.$refs.textarea, { textToInsert: mentionText, textToReplace, }); this.mentionManager.clearMentionSymbol(); }, onInsertText(event: BaseEvent<InsertTextEvent>) { const { dialogId } = event.getData(); if (this.dialogId !== dialogId) { return; } this.text = Textarea.insertText(this.$refs.textarea, event.getData()); }, onEditMessage(event: BaseEvent<{ messageId: number, dialogId: string }>) { const { messageId, dialogId } = event.getData(); if (this.dialogId !== dialogId) { return; } this.openEditPanel(messageId); }, onReplyMessage(event: BaseEvent<{ messageId: number, dialogId: string }>) { const { messageId, dialogId } = event.getData(); if (this.dialogId !== dialogId) { return; } this.openReplyPanel(messageId); }, onForwardEntity(event: BaseEvent<{ dialogId: string, entityConfig: ForwardedEntityConfig }>) { const { dialogId, entityConfig } = event.getData(); if (this.dialogId !== dialogId) { return; } this.openForwardEntityPanel(entityConfig); }, onInsertForward(event: BaseEvent<{ messagesIds: number[], dialogId: string }>) { const { messagesIds, dialogId } = event.getData(); if (this.dialogId !== dialogId) { return; } this.openForwardPanel(messagesIds); }, async onPaste(clipboardEvent: ClipboardEvent) { if (!this.withUploadMenu) { return; } const multiUploadingService: MultiUploadingService = this.getMultiUploadingService(); const multiUploadingResult: MultiUploadingResult = await multiUploadingService.uploadFromClipboard({ clipboardEvent, dialogId: this.dialogId, imagesOnly: false, autoUpload: false, }); if (!Type.isArrayFilled(multiUploadingResult.uploaderIds)) { return; } this.showUploadPreviewPopup = true; this.previewPopupUploaderIds = multiUploadingResult.uploaderIds; this.previewPopupSourceFilesCount = multiUploadingResult.sourceFilesCount; }, onOpenUploadPreview(event: BaseEvent) { const { multiUploadingResult } = event.getData(); this.showUploadPreviewPopup = true; this.previewPopupUploaderIds = multiUploadingResult.uploaderIds; this.previewPopupSourceFilesCount = multiUploadingResult.sourceFilesCount; }, onMarketIconClick() { this.toggleMarketPanel(); }, onMessageDeleted(event: BaseEvent<{ messageId: number }>) { const { messageId } = event.getData(); if (this.panelContext.messageId === messageId) { this.closePanel(); } if (this.panelContext.messagesIds && this.panelContext.messagesIds.includes(messageId)) { this.closePanel(); } }, initResizeManager() { this.resizeManager = new ResizeManager({ direction: ResizeDirection.up, maxHeight: TextareaHeight.max, minHeight: TextareaHeight.min, }); this.resizeManager.subscribe(ResizeManager.events.onHeightChange, ({ data: { newHeight } }) => { Logger.warn('Textarea: Resize height change', newHeight); this.textareaHeight = newHeight; }); this.resizeManager.subscribe(ResizeManager.events.onResizeStop, () => { Logger.warn('Textarea: Resize stop'); this.resizedTextareaHeight = this.textareaHeight; this.saveTextareaHeight(); }); }, initSendingService() { if (this.sendingService) { return; } this.sendingService = SendingService.getInstance(); }, async initMentionManager() { const { mentions = {}, } = await this.getDraftManager().getDraft(this.dialogId); this.mentionManager = new MentionManager(this.$refs.textarea); this.mentionManager.setMentionReplacements(mentions); this.mentionManager.subscribe(MentionManagerEvents.showMentionPopup, (event) => { const { mentionQuery } = event.getData(); this.showMentionPopup(mentionQuery); }); this.mentionManager.subscribe(MentionManagerEvents.hideMentionPopup, () => { this.closeMentionPopup(); }); }, getSendingService(): SendingService { return this.sendingService; }, getInputActionService(): InputSenderService { if (!this.inputSenderService) { this.inputSenderService = new InputSenderService(this.dialogId); } return this.inputSenderService; }, getDraftManager(): DraftManager { if (!this.draftManager) { this.draftManager = DraftManager.getInstance(); } return this.draftManager; }, getMessageService(): MessageService { if (!this.messageService) { this.messageService = new MessageService({ chatId: this.dialog.chatId }); } return this.messageService; }, getUploadingService(): UploadingService { if (!this.uploadingService) { this.initUploadingService(); } return this.uploadingService; }, initUploadingService() { this.uploadingService = UploadingService.getInstance(); this.startFileUploadAction = () => { this.getInputActionService().startAction(InputAction.sendingFile); }; this.stopFileUploadAction = () => { this.getInputActionService().stopAction(InputAction.sendingFile); }; this.uploadingService.subscribe(UploadingService.event.uploadStart, this.startFileUploadAction); this.uploadingService.subscribe(UploadingService.event.uploadComplete, this.stopFileUploadAction); this.uploadingService.subscribe(UploadingService.event.uploadCancel, this.stopFileUploadAction); this.uploadingService.subscribe(UploadingService.event.uploadError, this.stopFileUploadAction); }, unbindUploadingService() { if (!this.uploadingService) { return; } this.uploadingService.unsubscribe(UploadingService.event.uploadStart, this.startFileUploadAction); this.uploadingService.unsubscribe(UploadingService.event.uploadComplete, this.stopFileUploadAction); this.uploadingService.unsubscribe(UploadingService.event.uploadCancel, this.stopFileUploadAction); this.uploadingService.unsubscribe(UploadingService.event.uploadError, this.stopFileUploadAction); }, getMultiUploadingService(): MultiUploadingService { if (!this.multiUploadingService) { this.multiUploadingService = new MultiUploadingService(); } return this.multiUploadingService; }, onSendFilesFromPreviewPopup(event) { this.text = ''; const { text, uploaderIds } = event; const textWithMentions = this.mentionManager.replaceMentions(text); uploaderIds.forEach((uploaderId, index) => { this.getUploadingService().sendMessageWithFiles({ uploaderId, text: index === 0 ? textWithMentions : '', }); }); this.focus(); }, closeMentionPopup() { this.showMention = false; this.mentionQuery = ''; this.mentionManager.onMentionPopupClose(); }, showMentionPopup(mentionQuery: string) { this.mentionQuery = mentionQuery; this.showMention = true; }, focus(): void { this.$refs.textarea?.focus({ preventScroll: true }); }, loc(phraseCode: string): string { return this.$Bitrix.Loc.getMessage(phraseCode); }, onAudioInputStart() { if (this.isEmptyText) { return; } this.text += ' '; }, onAudioInputResult(inputText: string) { this.text += inputText; }, }, template: ` <div class="bx-im-send-panel__scope bx-im-send-panel__container --ui-context-content-light"> <div class="bx-im-textarea__container"> <div @mousedown="onResizeStart" class="bx-im-textarea__drag-handle"></div> <TextareaPanel :type="panelType" :context="panelContext" :dialogId="dialogId" @close="closePanel" /> <div class="bx-im-textarea__content" ref="textarea-content"> <div class="bx-im-textarea__left"> <UploadMenu v-if="withUploadMenu" :dialogId="dialogId" @fileSelect="onFileSelect" @diskFileSelect="onDiskFileSelect" /> <textarea v-model="text" :style="textareaStyle" :placeholder="textareaPlaceholder" :maxlength="textareaMaxLength" @keydown="onKeyDown" @paste="onPaste" class="bx-im-textarea__element" ref="textarea" rows="1" ></textarea> <AudioInput v-if="withAudioInput" @inputStart="onAudioInputStart" @inputResult="onAudioInputResult" /> </div> <div class="bx-im-textarea__right"> <div class="bx-im-textarea__action-panel"> <AutoDeleteSelector v-if="isAutoDeleteEnabled" :dialogId="dialogId" /> <BIcon v-if="withMarket" :name="OutlineIcons.APPS" :title="loc('IM_TEXTAREA_ICON_APPLICATION')" :size="ICON_SIZE" :color="marketIconColor" class="bx-im-textarea__icon" @click="onMarketIconClick" /> <SmileSelector v-if="withSmileSelector" :dialogId="dialogId" /> </div> </div> </div> </div> <SendButton :dialogId="dialogId" :editMode="editMode" :isDisabled="isDisabled" @click="sendMessage" /> <UploadPreviewPopup v-if="showUploadPreviewPopup" :dialogId="dialogId" :uploaderIds="previewPopupUploaderIds" :sourceFilesCount="previewPopupSourceFilesCount" :textareaValue="text" @close="showUploadPreviewPopup = false" @sendFiles="onSendFilesFromPreviewPopup" /> <MentionPopup v-if="showMention" :bindElement="$refs['textarea-content']" :dialogId="dialogId" :query="mentionQuery" @close="closeMentionPopup" /> </div> `, };