import type { BlokModules } from '../../../../types-internal/blok-modules'; import { Dom as $ } from '../../../dom'; import { SelectionUtils } from '../../../selection/index'; import type { InlineToolAdapter } from '../../../tools/inline'; import type { SelectionValidationResult } from './types'; /** * InlineSelectionValidator determines if inline toolbar should be shown based on selection. * * Responsibilities: * - Check if selection is valid (not null, not collapsed, has content) * - Check if target element is compatible (not IMG/INPUT) * - Check if current block has inline tools available * - Check if target is contenteditable or in read-only mode with valid selection */ export class InlineSelectionValidator { /** * Getter function to access Blok modules dynamically */ private getBlok: () => BlokModules; constructor(getBlok: () => BlokModules) { this.getBlok = getBlok; } /** * Check if inline toolbar can be shown * @param options - validation options * @param options.allowCollapsed - when true, a collapsed (empty) selection is * permitted. Used by popover-entry tools (Link, Equation) triggered via their * keyboard shortcut, which open an input at the caret instead of formatting a * range — all other validity checks (block present, tools available, not a * code block, contenteditable target) still apply. */ public canShow(options: { allowCollapsed?: boolean } = {}): SelectionValidationResult { /** * readOnly: { hideControls: true } — pure document view, no inline toolbar. * Checked first: the contenteditable check below matches contenteditable="false" * targets too, so later gates would be bypassed. */ if (this.getBlok().ReadOnly.isControlsHidden) { return { allowed: false, reason: 'Editor controls are hidden in read-only mode' }; } /** * Tags conflicts with window.selection function. * Ex. IMG tag returns null (Firefox) or Redactors wrapper (Chrome) */ const tagsConflictsWithSelection = ['IMG', 'INPUT']; const currentSelection = this.resolveSelection(); const selectedText = SelectionUtils.text; // old browsers if (!currentSelection || !currentSelection.anchorNode) { return { allowed: false, reason: 'Selection is null or has no anchor node' }; } // empty selection if (!options.allowCollapsed && (currentSelection.isCollapsed || selectedText.length < 1)) { return { allowed: false, reason: 'Selection is collapsed or empty' }; } const target = !$.isElement(currentSelection.anchorNode) ? currentSelection.anchorNode.parentElement : currentSelection.anchorNode; if (target === null) { return { allowed: false, reason: 'Target element is null' }; } if (tagsConflictsWithSelection.includes(target.tagName)) { return { allowed: false, reason: `Target tag ${target.tagName} conflicts with selection` }; } // Code block (`
`) is plain-text
    // content. Inline formatting would inject HTML into the plaintext-only
    // region and break syntax highlighting, so suppress the toolbar there.
    const focusElement = this.nodeToElement(currentSelection.focusNode);

    if (target.closest('pre') !== null || focusElement?.closest('pre')) {
      return { allowed: false, reason: 'Selection is inside a code block' };
    }

    const anchorElement = $.isElement(currentSelection.anchorNode)
      ? currentSelection.anchorNode as HTMLElement
      : currentSelection.anchorNode.parentElement;
    const { BlockManager } = this.getBlok();
    const blockFromAnchor = anchorElement
      ? BlockManager.getBlock(anchorElement)
      : null;
    const currentBlock = blockFromAnchor ?? BlockManager.currentBlock;

    if (currentBlock === null || currentBlock === undefined) {
      return { allowed: false, reason: 'Current block is null or undefined' };
    }

    const toolsAvailable = this.getTools();
    const isAtLeastOneToolAvailable = toolsAvailable.some((tool) => currentBlock.tool.inlineTools.has(tool.name));

    if (!isAtLeastOneToolAvailable) {
      return { allowed: false, reason: 'No inline tools available for current block' };
    }

    const contenteditableSelector = '[contenteditable]';
    const contenteditableTarget = target.closest(contenteditableSelector);

    if (contenteditableTarget !== null) {
      return { allowed: true };
    }

    const blockHolder = currentBlock.holder;
    const holderContenteditable = blockHolder &&
      (
        blockHolder.matches(contenteditableSelector)
          ? blockHolder
          : blockHolder.closest(contenteditableSelector)
      );

    if (holderContenteditable) {
      return { allowed: true };
    }

    const { ReadOnly } = this.getBlok();

    if (ReadOnly.isEnabled) {
      return SelectionUtils.isSelectionAtBlok(currentSelection)
        ? { allowed: true }
        : { allowed: false, reason: 'Read-only mode and selection not at Blok' };
    }

    return { allowed: false, reason: 'Target is not contenteditable' };
  }

  /**
   * Returns tools that are available for current block
   */
  private getTools(): InlineToolAdapter[] {
    const { BlockManager, ReadOnly } = this.getBlok();
    const currentBlock = BlockManager.currentBlock
      ?? (() => {
        const selection = this.resolveSelection();
        const anchorNode = selection?.anchorNode;

        if (!anchorNode) {
          return null;
        }

        const anchorElement = $.isElement(anchorNode) ? anchorNode as HTMLElement : anchorNode.parentElement;

        if (!anchorElement) {
          return null;
        }

        return BlockManager.getBlock(anchorElement);
      })();

    if (!currentBlock) {
      return [];
    }

    return Array.from(currentBlock.tool.inlineTools.values()).filter((tool) => {
      return !(ReadOnly.isEnabled && !tool.isReadOnlySupported);
    });
  }

  /**
   * Resolves the current selection, handling test mocks
   */
  private resolveSelection(): Selection | null {
    const selectionOverride = (SelectionUtils as unknown as { selection?: Selection | null }).selection;

    if (selectionOverride !== undefined) {
      return selectionOverride;
    }

    const instanceOverride = (SelectionUtils as unknown as { instance?: Selection | null }).instance;

    if (instanceOverride !== undefined) {
      return instanceOverride;
    }

    return SelectionUtils.get();
  }

  private nodeToElement(node: Node | null | undefined): Element | null {
    if (!node) {
      return null;
    }

    return $.isElement(node) ? node : node.parentElement;
  }
}