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/rospirotorg.ru/bitrix/js/ui/text-editor/src/plugins/link/ |
Upload File : |
/* eslint-disable no-underscore-dangle */ import { Browser, Event, Loc, Type, Validation } from 'main.core'; import type { BBCodeElementNode } from 'ui.bbcode.model'; import type { BaseEvent } from 'main.core.events'; import type { BBCodeConversion, BBCodeConversionFn, BBCodeExportConversion, BBCodeExportOutput, BBCodeImportConversion, } from '../../bbcode'; import { DIALOG_VISIBILITY_COMMAND, HIDE_DIALOG_COMMAND } from '../../commands'; import { UNFORMATTED } from '../../constants'; import { $adjustDialogPosition } from '../../helpers/adjust-dialog-position'; import { getSelectedNode } from '../../helpers/get-selected-node'; import Button from '../../toolbar/button'; import type { SchemeValidationOptions } from '../../types/scheme-validation-options'; import { TextEditorLexicalNode } from '../../types/text-editor-lexical-node'; import BasePlugin from '../base-plugin'; import { LinkEditor } from './link-editor'; import { sanitizeUrl } from '../../helpers/sanitize-url'; import { validateUrl } from '../../helpers/validate-url'; import { COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, PASTE_COMMAND, KEY_MODIFIER_COMMAND, $isTextNode, $isElementNode, $getSelection, $setSelection, $isRangeSelection, $insertNodes, $isRootOrShadowRoot, $createParagraphNode, $createTextNode, $getNodeByKey, createCommand, type LexicalNode, type RangeSelection, type NodeKey, type LexicalCommand, } from 'ui.lexical.core'; import { $wrapNodeInElement, $findMatchingParent, mergeRegister } from 'ui.lexical.utils'; import { LinkNode, TOGGLE_LINK_COMMAND, $toggleLink, $createLinkNode, $isLinkNode, $isAutoLinkNode, type LinkAttributes, type AutoLinkNode, } from 'ui.lexical.link'; import { type TextEditor } from '../../text-editor'; export const INSERT_LINK_DIALOG_COMMAND: LexicalCommand<string> = createCommand('INSERT_LINK_DIALOG_COMMAND'); export class LinkPlugin extends BasePlugin { #linkEditor: LinkEditor = null; #onEditorScroll: Function = this.#handleEditorScroll.bind(this); #lastSelection: RangeSelection = null; constructor(editor: TextEditor) { super(editor); this.#registerCommands(); this.#registerListeners(); this.#registerComponents(); } static getName(): string { return 'Link'; } static getNodes(editor: TextEditor): Array<Class<LexicalNode>> { return [LinkNode]; } importBBCode(): BBCodeImportConversion { return { url: (): BBCodeConversion => ({ conversion: (node: BBCodeElementNode): BBCodeConversionFn | null => { // [url]{url}[/url] // [url={url}]{text}[/url] let url = node.getValue(); if (!validateUrl(url)) { url = node.toPlainText(); if (!validateUrl(url)) { return { node: null }; } } return { node: $createLinkNode(sanitizeUrl(url), { target: '_blank' }), }; }, priority: 0, }), }; } exportBBCode(): BBCodeExportConversion { return { link: (lexicalNode: LinkNode): BBCodeExportOutput => { const url = lexicalNode.getURL(); const children = lexicalNode.getChildren(); const isSimpleText = ( children.length === 1 && $isTextNode(children[0]) && children[0].getFormat() === 0 ); const scheme = this.getEditor().getBBCodeScheme(); if (isSimpleText && children[0].getTextContent() === url) { return { node: scheme.createElement({ name: 'url' }), }; } return { node: scheme.createElement({ name: 'url', value: url }), }; }, }; } validateScheme(): SchemeValidationOptions | null { return { nodes: [{ nodeClass: LinkNode, }], bbcodeMap: { link: 'url', }, }; } #registerListeners(): void { this.cleanUpRegister( this.getEditor().registerEventListener(LinkNode, 'click', (event: Event, nodeKey: NodeKey) => { const linkNode: LinkNode = $getNodeByKey(nodeKey); if ($isLinkNode(linkNode)) { this.getEditor().dispatchCommand(INSERT_LINK_DIALOG_COMMAND, linkNode); } }), ); } #registerCommands(): void { this.cleanUpRegister( this.#registerToggleLinkCommand(), this.#registerInsertLinkCommand(), this.#registerKeyModifierCommand(), this.#registerPasteCommand(), ); } #registerToggleLinkCommand(): () => void { return this.getEditor().registerCommand( TOGGLE_LINK_COMMAND, (payload): boolean => { if (payload === null) { $toggleLink(payload); return true; } const selection: RangeSelection = $getSelection(); if (!$isRangeSelection(selection)) { return false; } let url = null; let originalUrl = null; let attributes = {}; if (Type.isStringFilled(payload)) { url = payload; } else if (Type.isPlainObject(payload)) { const { target, rel, title } = payload; attributes = { rel, target, title }; url = payload.url; originalUrl = payload.originalUrl || null; } if (Type.isStringFilled(url)) { if (!Type.isStringFilled(attributes.target)) { attributes.target = '_blank'; } if (validateUrl(url)) { if (selection.isCollapsed() && !this.#isLinkSelected(selection)) { this.#insertLink(selection, url, attributes, originalUrl); } else { $toggleLink(url, attributes); } return true; } return false; } return false; }, COMMAND_PRIORITY_LOW, ); } #registerInsertLinkCommand(): () => void { return mergeRegister( this.getEditor().registerCommand( INSERT_LINK_DIALOG_COMMAND, (payload): boolean => { const selection: RangeSelection = $getSelection(); if (!$isRangeSelection(selection) || !this.getEditor().isEditable()) { return false; } this.#lastSelection = selection.clone(); if (this.#linkEditor !== null) { this.#linkEditor.destroy(); } let lineNode = null; let linkUrl = null; if ($isLinkNode(payload)) { lineNode = payload; linkUrl = lineNode.getURL(); } else { const $isUnformatted = $findMatchingParent( selection.anchor.getNode(), (node: TextEditorLexicalNode) => { return (node.__flags & UNFORMATTED) !== 0; }, ); if ($isUnformatted) { return false; } const node = getSelectedNode(selection); const linkParent = $findMatchingParent(node, $isLinkNode); if (linkParent) { lineNode = linkParent; linkUrl = lineNode.getURL(); lineNode.select(); } else if ($isLinkNode(node)) { lineNode = node; linkUrl = lineNode.getURL(); lineNode.select(); } } this.getEditor().dispatchCommand(HIDE_DIALOG_COMMAND); this.#linkEditor = new LinkEditor({ linkUrl, autoLinkMode: $isAutoLinkNode(lineNode), // for an embedded popup: document.body -> this.getEditor().getScrollerContainer() targetContainer: document.body, events: { onSave: (event: BaseEvent) => { const linkEditor: LinkEditor = event.getTarget(); let url = linkEditor.getLinkUrl(); if (!Type.isStringFilled(url)) { linkEditor.hide(); return; } const protocol = Validation.isEmail(url) ? 'mailto:' : 'https://'; const originalUrl = url; if (!validateUrl(url)) { url = `${protocol}${url}`; linkEditor.setLinkUrl(url); } if (lineNode === null) { this.getEditor().update(() => { this.#restoreSelection(); this.getEditor().dispatchCommand(TOGGLE_LINK_COMMAND, { url, originalUrl, rel: null }); linkEditor.setEditMode(false); const currentSelection: RangeSelection = $getSelection(); if ($isRangeSelection(currentSelection)) { this.#lastSelection = currentSelection.clone(); } if (!$isRangeSelection(currentSelection) || currentSelection.isCollapsed()) { linkEditor.hide(); } this.#convertAutoLinkToLink(currentSelection); }); } else { this.getEditor().update(() => { lineNode.setURL(url); this.#convertAutoLinkToLink($getSelection()); linkEditor.setAutoLinkMode(false); }); linkEditor.setEditMode(false); } this.getEditor().resetHighlightSelection(); }, onCancel: (event: BaseEvent) => { const linkEditor: LinkEditor = event.getTarget(); linkEditor.hide(); }, onUnlink: (event: BaseEvent) => { if (lineNode === null) { this.getEditor().dispatchCommand(TOGGLE_LINK_COMMAND, null); } else { this.getEditor().update(() => { const children = lineNode.getChildren(); for (const child of children) { // eslint-disable-next-line @bitrix24/bitrix24-rules/no-native-dom-methods lineNode.insertBefore(child); } lineNode.remove(); }); } const linkEditor: LinkEditor = event.getTarget(); linkEditor.hide(); }, onShow: () => { if ($adjustDialogPosition(this.#linkEditor.getPopup(), this.getEditor())) { Event.bind(this.getEditor().getScrollerContainer(), 'scroll', this.#onEditorScroll); this.getEditor().highlightSelection(); } }, onClose: () => { this.#handleDialogDestroy(); }, onDestroy: () => { this.#handleDialogDestroy(); }, }, }); this.#linkEditor.show(); return true; }, COMMAND_PRIORITY_LOW, ), this.getEditor().registerCommand( HIDE_DIALOG_COMMAND, (): boolean => { if (this.#linkEditor !== null) { this.#linkEditor.destroy(); } return false; }, COMMAND_PRIORITY_LOW, ), this.getEditor().registerCommand( DIALOG_VISIBILITY_COMMAND, (): boolean => { return this.#linkEditor !== null && this.#linkEditor.isShown(); }, COMMAND_PRIORITY_LOW, ), ); } #restoreSelection(): boolean { const selection = $getSelection(); if (!$isRangeSelection(selection) && this.#lastSelection !== null) { $setSelection(this.#lastSelection); this.#lastSelection = null; return true; } return false; } #handleDialogDestroy(): void { this.#linkEditor = null; Event.unbind(this.getEditor().getScrollerContainer(), 'scroll', this.#onEditorScroll); this.getEditor().resetHighlightSelection(); this.getEditor().update(() => { if (!this.#restoreSelection()) { this.getEditor().focus(); } }); } #handleEditorScroll(): void { this.getEditor().update(() => { $adjustDialogPosition(this.#linkEditor.getPopup(), this.getEditor()); }); } #registerKeyModifierCommand(): () => void { return this.getEditor().registerCommand( KEY_MODIFIER_COMMAND, (payload) => { const event: KeyboardEvent = payload; const { code, ctrlKey, metaKey } = event; if (code === 'KeyK' && (ctrlKey || metaKey)) { event.preventDefault(); this.getEditor().dispatchCommand(INSERT_LINK_DIALOG_COMMAND); return true; } return false; }, COMMAND_PRIORITY_NORMAL, ); } #registerPasteCommand(): () => void { return this.getEditor().registerCommand( PASTE_COMMAND, (event) => { const selection: RangeSelection = $getSelection(); if ( !$isRangeSelection(selection) || selection.isCollapsed() || !(event instanceof ClipboardEvent) || event.clipboardData === null ) { return false; } const clipboardText = event.clipboardData.getData('text'); if (!validateUrl(clipboardText)) { return false; } // If we select nodes that are elements then avoid applying the link. if (!selection.getNodes().some((node) => $isElementNode(node))) { $toggleLink(clipboardText); event.preventDefault(); return true; } return false; }, COMMAND_PRIORITY_NORMAL, ); } #insertLink(selection: RangeSelection, url: string, attributes?: LinkAttributes, originalUrl?: string): void { const linkUrl = sanitizeUrl(url); const linkNode = $createLinkNode(linkUrl, attributes); linkNode.append($createTextNode(Type.isStringFilled(originalUrl) ? originalUrl : linkUrl)); const anchor = selection.anchor; if (anchor.type === 'text' && anchor.getNode().isSimpleText()) { const anchorNode = anchor.getNode(); const selectionOffset = anchor.offset; const splitNodes = anchorNode.splitText(selectionOffset); if (selectionOffset === 0) { // eslint-disable-next-line @bitrix24/bitrix24-rules/no-native-dom-methods splitNodes[0].insertBefore(linkNode); linkNode.select(); } else { splitNodes[0].insertAfter(linkNode); linkNode.select(); } } else { $insertNodes([linkNode]); if ($isRootOrShadowRoot(linkNode.getParentOrThrow())) { $wrapNodeInElement(linkNode, $createParagraphNode).selectEnd(); } } } #isLinkSelected(selection: RangeSelection): boolean { const node = getSelectedNode(selection); const parent = node.getParent(); return $isLinkNode(parent) || $isLinkNode(node); } #convertAutoLinkToLink(selection: RangeSelection): boolean { if ($isRangeSelection(selection)) { const parent: AutoLinkNode = getSelectedNode(selection).getParent(); if ($isAutoLinkNode(parent)) { const linkNode = $createLinkNode( parent.getURL(), { rel: parent.getRel(), target: Type.isStringFilled(parent.getTarget()) ? parent.getTarget() : '_blank', title: parent.getTitle(), }, ); parent.replace(linkNode, true); return true; } } return false; } #registerComponents(): void { this.getEditor().getComponentRegistry().register('link', (): Button => { const button: Button = new Button(); button.setContent('<span class="ui-icon-set --link-3"></span>'); button.setTooltip(Loc.getMessage('TEXT_EDITOR_BTN_LINK')); button.setBlockType('link'); button.disableInsideUnformatted(); button.setTooltip( Loc.getMessage('TEXT_EDITOR_BTN_LINK', { '#keystroke#': Browser.isMac() ? '⌘K' : 'Ctrl+K' }), ); button.subscribe('onClick', (): void => { if (this.#linkEditor !== null && this.#linkEditor.isShown()) { return; } this.getEditor().focus(() => { this.getEditor().dispatchCommand(INSERT_LINK_DIALOG_COMMAND); }); }); return button; }); } destroy(): void { super.destroy(); if (this.#linkEditor !== null) { this.#linkEditor.destroy(); } } }