{"version":3,"sources":["../../src/react/TabbableEffects.tsx","../../src/lib/BaseTabbablePlugin.ts","../../src/lib/findTabDestination.ts","../../src/react/TabbablePlugin.tsx"],"sourcesContent":["import React from 'react';\n\nimport { PathApi } from '@udecode/plate';\nimport { useEditorReadOnly, useEditorRef } from '@udecode/plate/react';\nimport { tabbable } from 'tabbable';\n\nimport type { TabbableEntry } from '../lib/types';\n\nimport { BaseTabbablePlugin } from '../lib/BaseTabbablePlugin';\nimport { findTabDestination } from '../lib/findTabDestination';\n\nexport function TabbableEffects() {\n  const editor = useEditorRef();\n  const readOnly = useEditorReadOnly();\n\n  React.useEffect(() => {\n    if (readOnly) return;\n\n    const { globalEventListener, insertTabbableEntries, isTabbable, query } =\n      editor.getOptions(BaseTabbablePlugin);\n\n    const editorDOMNode = editor.api.toDOMNode(editor);\n\n    if (!editorDOMNode) return;\n\n    const handler = (event: KeyboardEvent) => {\n      // Check if the keydown is a tab key that should be handled\n      if (event.key !== 'Tab' || event.defaultPrevented || !query?.(event)) {\n        return;\n      }\n\n      /**\n       * Get the list of additional tabbable entries specified in the plugin\n       * options\n       */\n      const insertedTabbableEntries = insertTabbableEntries?.(\n        event\n      ) as TabbableEntry[];\n\n      /**\n       * Global event listener only. Do not handle the tab event if the keydown\n       * was sent to an element other than the editor or one of the additional\n       * tabbable elements.\n       */\n      if (\n        globalEventListener &&\n        event.target &&\n        ![\n          editorDOMNode,\n          ...insertedTabbableEntries.map(({ domNode }) => domNode),\n        ].some((container) => container.contains(event.target as Node))\n      ) {\n        return;\n      }\n\n      // Get all tabbable DOM nodes in the editor\n      const tabbableDOMNodes = tabbable(editorDOMNode) as HTMLElement[];\n\n      /**\n       * Construct a tabbable entry for each tabbable Slate node, filtered by\n       * the `isTabbable` option (defaulting to only void nodes).\n       */\n      const defaultTabbableEntries = tabbableDOMNodes\n        .map((domNode) => {\n          const slateNode = editor.api.toSlateNode(domNode);\n\n          if (!slateNode) return;\n\n          return {\n            domNode,\n            path: editor.api.findPath(slateNode),\n            slateNode,\n          } as TabbableEntry;\n        })\n        .filter((entry) => entry && isTabbable?.(entry)) as TabbableEntry[];\n\n      /**\n       * The list of all tabbable entries. Sorting by path ensures a consistent\n       * tab order.\n       */\n      const tabbableEntries = [\n        ...insertedTabbableEntries,\n        ...defaultTabbableEntries,\n      ].sort((a, b) => PathApi.compare(a.path, b.path));\n\n      /**\n       * TODO: Refactor everything ABOVE this line into a util function and test\n       * separately\n       */\n\n      // Check if any tabbable entry is the active element\n      const { activeElement } = document;\n      const activeTabbableEntry =\n        (activeElement &&\n          tabbableEntries.find((entry) => entry.domNode === activeElement)) ??\n        null;\n\n      // Find the next Slate node or DOM node to focus\n      const tabDestination = findTabDestination(editor, {\n        activeTabbableEntry,\n        direction: event.shiftKey ? 'backward' : 'forward',\n        tabbableEntries,\n      });\n\n      if (tabDestination) {\n        event.preventDefault();\n\n        switch (tabDestination.type) {\n          case 'dom-node': {\n            tabDestination.domNode.focus();\n\n            break;\n          }\n          case 'path': {\n            editor.tf.focus({\n              at: {\n                anchor: { offset: 0, path: tabDestination.path },\n                focus: { offset: 0, path: tabDestination.path },\n              },\n            });\n\n            break;\n          }\n        }\n\n        return;\n      }\n\n      /**\n       * There was no tab destination, so let the browser handle the tab event.\n       * We don't want the browser to focus anything that could have been\n       * focused by us, so we make make all tabbable DOM nodes in the editor\n       * unfocusable. This ensures that the focus exits the editor cleanly.\n       */\n      tabbableDOMNodes.forEach((domNode) => {\n        const oldTabIndex = domNode.getAttribute('tabindex');\n        domNode.setAttribute('tabindex', '-1');\n\n        setTimeout(() => {\n          if (oldTabIndex) {\n            domNode.setAttribute('tabindex', oldTabIndex);\n          } else {\n            domNode.removeAttribute('tabindex');\n          }\n        }, 0);\n      });\n    };\n\n    const eventListenerNode = globalEventListener\n      ? document.body\n      : editorDOMNode;\n\n    eventListenerNode.addEventListener('keydown', handler, true);\n\n    return () =>\n      eventListenerNode.removeEventListener('keydown', handler, true);\n  }, [readOnly, editor]);\n\n  return null;\n}\n","import { type PluginConfig, createTSlatePlugin, KEYS } from '@udecode/plate';\n\nimport type { TabbableEntry } from './types';\n\nexport type TabblableConfig = PluginConfig<\n  'tabbable',\n  {\n    /**\n     * When true, the plugin will add its event listener to the document instead\n     * of the editor, allowing it to capture events from outside the editor.\n     *\n     * @default: false\n     */\n    globalEventListener?: boolean;\n    /**\n     * Add additional tabbables to the list of tabbables. Useful for adding\n     * tabbables that are not contained within the editor. Ignores\n     * `isTabbable`.\n     *\n     * @default: () => []\n     */\n    insertTabbableEntries?: (event: KeyboardEvent) => TabbableEntry[];\n    /**\n     * Determine whether an element should be included in the tabbable list.\n     *\n     * @default: (editor, tabbableEntry) => editor.api.isVoid(tabbableEntry.slateNode)\n     */\n    isTabbable?: (entry: TabbableEntry) => boolean;\n    /**\n     * Dynamically enable or disable the plugin.\n     *\n     * @default: () => true\n     */\n    query?: (event: KeyboardEvent) => boolean;\n  }\n>;\n\nexport const BaseTabbablePlugin = createTSlatePlugin<TabblableConfig>({\n  key: KEYS.tabbable,\n  options: {\n    globalEventListener: false,\n    insertTabbableEntries: () => [],\n    query: () => true,\n  },\n}).extend(({ editor }) => ({\n  options: {\n    isTabbable: (tabbableEntry) => editor.api.isVoid(tabbableEntry.slateNode),\n  },\n}));\n","import { type SlateEditor, PathApi } from '@udecode/plate';\n\nimport type { TabbableEntry, TabDestination } from './types';\n\nexport interface FindTabDestinationOptions {\n  activeTabbableEntry: TabbableEntry | null;\n  direction: 'backward' | 'forward';\n  tabbableEntries: TabbableEntry[];\n}\n\nexport const findTabDestination = (\n  editor: SlateEditor,\n  { activeTabbableEntry, direction, tabbableEntries }: FindTabDestinationOptions\n): TabDestination | null => {\n  // Case 1: A tabbable entry was active before tab was pressed\n  if (activeTabbableEntry) {\n    // Find the next tabbable entry after the active one\n    const activeTabbableEntryIndex =\n      tabbableEntries.indexOf(activeTabbableEntry);\n    const nextTabbableEntryIndex =\n      activeTabbableEntryIndex + (direction === 'forward' ? 1 : -1);\n    const nextTabbableEntry = tabbableEntries[nextTabbableEntryIndex];\n\n    /**\n     * If the next tabbable entry originated from the same path as the active\n     * tabbable entry, focus it.\n     *\n     * Examples of when this is true:\n     *\n     * - We're inside a void node and there is an additional tabbable inside the\n     *   same void node.\n     * - We're inside a popover containing multiple tabbable elements all anchored\n     *   to the same slate node, and there is an additional tabbable inside the\n     *   same popover.\n     *\n     * Examples of when this is false:\n     *\n     * - We're inside a void node and the next tabbable is outside the void node.\n     * - We're in the last tabbable element of a popover.\n     * - There is no next tabbable element.\n     */\n    if (\n      nextTabbableEntry &&\n      PathApi.equals(activeTabbableEntry.path, nextTabbableEntry.path)\n    ) {\n      return {\n        domNode: nextTabbableEntry.domNode,\n        type: 'dom-node',\n      };\n    }\n    /**\n     * Otherwise, return the focus to the editor. If we're moving forward, focus\n     * the first point after the active tabbable's path. If we're moving\n     * backward, focus the point of the active tabbable's path. TODO: Let a\n     * tabbable entry specify custom before and after points.\n     */\n    if (direction === 'forward') {\n      const pointAfter = editor.api.after(activeTabbableEntry.path);\n\n      if (!pointAfter) return null;\n\n      return {\n        path: pointAfter.path,\n        type: 'path',\n      };\n    }\n\n    return {\n      path: editor.api.point(activeTabbableEntry.path)!.path,\n      type: 'path',\n    };\n  }\n\n  // Case 2: No tabbable entry was active before tab was pressed\n\n  const selectionPath = editor.selection?.anchor?.path || [];\n\n  // Find the first tabbable entry after the selection\n  const nextTabbableEntry =\n    direction === 'forward'\n      ? tabbableEntries.find(\n          (entry) => !PathApi.isBefore(entry.path, selectionPath)\n        )\n      : [...tabbableEntries]\n          .reverse()\n          .find((entry) => PathApi.isBefore(entry.path, selectionPath));\n\n  // If it exists, focus it\n  if (nextTabbableEntry) {\n    return {\n      domNode: nextTabbableEntry.domNode,\n      type: 'dom-node',\n    };\n  }\n\n  // Otherwise, use the default behaviour\n  return null;\n};\n","import { toPlatePlugin } from '@udecode/plate/react';\n\nimport { BaseTabbablePlugin } from '../lib/BaseTabbablePlugin';\nimport { TabbableEffects } from './TabbableEffects';\n\nexport const TabbablePlugin = toPlatePlugin(BaseTabbablePlugin, {\n  render: { afterEditable: TabbableEffects },\n});\n"],"mappings":";AAAA,OAAO,WAAW;AAElB,SAAS,WAAAA,gBAAe;AACxB,SAAS,mBAAmB,oBAAoB;AAChD,SAAS,gBAAgB;;;ACJzB,SAA4B,oBAAoB,YAAY;AAqCrD,IAAM,qBAAqB,mBAAoC;AAAA,EACpE,KAAK,KAAK;AAAA,EACV,SAAS;AAAA,IACP,qBAAqB;AAAA,IACrB,uBAAuB,MAAM,CAAC;AAAA,IAC9B,OAAO,MAAM;AAAA,EACf;AACF,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,OAAO;AAAA,EACzB,SAAS;AAAA,IACP,YAAY,CAAC,kBAAkB,OAAO,IAAI,OAAO,cAAc,SAAS;AAAA,EAC1E;AACF,EAAE;;;AChDF,SAA2B,eAAe;AAUnC,IAAM,qBAAqB,CAChC,QACA,EAAE,qBAAqB,WAAW,gBAAgB,MACxB;AAE1B,MAAI,qBAAqB;AAEvB,UAAM,2BACJ,gBAAgB,QAAQ,mBAAmB;AAC7C,UAAM,yBACJ,4BAA4B,cAAc,YAAY,IAAI;AAC5D,UAAMC,qBAAoB,gBAAgB,sBAAsB;AAoBhE,QACEA,sBACA,QAAQ,OAAO,oBAAoB,MAAMA,mBAAkB,IAAI,GAC/D;AACA,aAAO;AAAA,QACL,SAASA,mBAAkB;AAAA,QAC3B,MAAM;AAAA,MACR;AAAA,IACF;AAOA,QAAI,cAAc,WAAW;AAC3B,YAAM,aAAa,OAAO,IAAI,MAAM,oBAAoB,IAAI;AAE5D,UAAI,CAAC,WAAY,QAAO;AAExB,aAAO;AAAA,QACL,MAAM,WAAW;AAAA,QACjB,MAAM;AAAA,MACR;AAAA,IACF;AAEA,WAAO;AAAA,MACL,MAAM,OAAO,IAAI,MAAM,oBAAoB,IAAI,EAAG;AAAA,MAClD,MAAM;AAAA,IACR;AAAA,EACF;AAIA,QAAM,gBAAgB,OAAO,WAAW,QAAQ,QAAQ,CAAC;AAGzD,QAAM,oBACJ,cAAc,YACV,gBAAgB;AAAA,IACd,CAAC,UAAU,CAAC,QAAQ,SAAS,MAAM,MAAM,aAAa;AAAA,EACxD,IACA,CAAC,GAAG,eAAe,EAChB,QAAQ,EACR,KAAK,CAAC,UAAU,QAAQ,SAAS,MAAM,MAAM,aAAa,CAAC;AAGpE,MAAI,mBAAmB;AACrB,WAAO;AAAA,MACL,SAAS,kBAAkB;AAAA,MAC3B,MAAM;AAAA,IACR;AAAA,EACF;AAGA,SAAO;AACT;;;AFtFO,SAAS,kBAAkB;AAChC,QAAM,SAAS,aAAa;AAC5B,QAAM,WAAW,kBAAkB;AAEnC,QAAM,UAAU,MAAM;AACpB,QAAI,SAAU;AAEd,UAAM,EAAE,qBAAqB,uBAAuB,YAAY,MAAM,IACpE,OAAO,WAAW,kBAAkB;AAEtC,UAAM,gBAAgB,OAAO,IAAI,UAAU,MAAM;AAEjD,QAAI,CAAC,cAAe;AAEpB,UAAM,UAAU,CAAC,UAAyB;AAExC,UAAI,MAAM,QAAQ,SAAS,MAAM,oBAAoB,CAAC,QAAQ,KAAK,GAAG;AACpE;AAAA,MACF;AAMA,YAAM,0BAA0B;AAAA,QAC9B;AAAA,MACF;AAOA,UACE,uBACA,MAAM,UACN,CAAC;AAAA,QACC;AAAA,QACA,GAAG,wBAAwB,IAAI,CAAC,EAAE,QAAQ,MAAM,OAAO;AAAA,MACzD,EAAE,KAAK,CAAC,cAAc,UAAU,SAAS,MAAM,MAAc,CAAC,GAC9D;AACA;AAAA,MACF;AAGA,YAAM,mBAAmB,SAAS,aAAa;AAM/C,YAAM,yBAAyB,iBAC5B,IAAI,CAAC,YAAY;AAChB,cAAM,YAAY,OAAO,IAAI,YAAY,OAAO;AAEhD,YAAI,CAAC,UAAW;AAEhB,eAAO;AAAA,UACL;AAAA,UACA,MAAM,OAAO,IAAI,SAAS,SAAS;AAAA,UACnC;AAAA,QACF;AAAA,MACF,CAAC,EACA,OAAO,CAAC,UAAU,SAAS,aAAa,KAAK,CAAC;AAMjD,YAAM,kBAAkB;AAAA,QACtB,GAAG;AAAA,QACH,GAAG;AAAA,MACL,EAAE,KAAK,CAAC,GAAG,MAAMC,SAAQ,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC;AAQhD,YAAM,EAAE,cAAc,IAAI;AAC1B,YAAM,uBACH,iBACC,gBAAgB,KAAK,CAAC,UAAU,MAAM,YAAY,aAAa,MACjE;AAGF,YAAM,iBAAiB,mBAAmB,QAAQ;AAAA,QAChD;AAAA,QACA,WAAW,MAAM,WAAW,aAAa;AAAA,QACzC;AAAA,MACF,CAAC;AAED,UAAI,gBAAgB;AAClB,cAAM,eAAe;AAErB,gBAAQ,eAAe,MAAM;AAAA,UAC3B,KAAK,YAAY;AACf,2BAAe,QAAQ,MAAM;AAE7B;AAAA,UACF;AAAA,UACA,KAAK,QAAQ;AACX,mBAAO,GAAG,MAAM;AAAA,cACd,IAAI;AAAA,gBACF,QAAQ,EAAE,QAAQ,GAAG,MAAM,eAAe,KAAK;AAAA,gBAC/C,OAAO,EAAE,QAAQ,GAAG,MAAM,eAAe,KAAK;AAAA,cAChD;AAAA,YACF,CAAC;AAED;AAAA,UACF;AAAA,QACF;AAEA;AAAA,MACF;AAQA,uBAAiB,QAAQ,CAAC,YAAY;AACpC,cAAM,cAAc,QAAQ,aAAa,UAAU;AACnD,gBAAQ,aAAa,YAAY,IAAI;AAErC,mBAAW,MAAM;AACf,cAAI,aAAa;AACf,oBAAQ,aAAa,YAAY,WAAW;AAAA,UAC9C,OAAO;AACL,oBAAQ,gBAAgB,UAAU;AAAA,UACpC;AAAA,QACF,GAAG,CAAC;AAAA,MACN,CAAC;AAAA,IACH;AAEA,UAAM,oBAAoB,sBACtB,SAAS,OACT;AAEJ,sBAAkB,iBAAiB,WAAW,SAAS,IAAI;AAE3D,WAAO,MACL,kBAAkB,oBAAoB,WAAW,SAAS,IAAI;AAAA,EAClE,GAAG,CAAC,UAAU,MAAM,CAAC;AAErB,SAAO;AACT;;;AG/JA,SAAS,qBAAqB;AAKvB,IAAM,iBAAiB,cAAc,oBAAoB;AAAA,EAC9D,QAAQ,EAAE,eAAe,gBAAgB;AAC3C,CAAC;","names":["PathApi","nextTabbableEntry","PathApi"]}