inline-editor.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import { createStore } from "solid-js/store"
  2. import { onCleanup, Show, type Accessor } from "solid-js"
  3. import { InlineInput } from "@opencode-ai/ui/inline-input"
  4. export function createInlineEditorController() {
  5. // This controller intentionally supports one active inline editor at a time.
  6. const [editor, setEditor] = createStore({
  7. active: "" as string,
  8. value: "",
  9. })
  10. const editorOpen = (id: string) => editor.active === id
  11. const editorValue = () => editor.value
  12. const openEditor = (id: string, value: string) => {
  13. if (!id) return
  14. setEditor({ active: id, value })
  15. }
  16. const closeEditor = () => setEditor({ active: "", value: "" })
  17. const saveEditor = (callback: (next: string) => void) => {
  18. const next = editor.value.trim()
  19. if (!next) {
  20. closeEditor()
  21. return
  22. }
  23. closeEditor()
  24. callback(next)
  25. }
  26. const editorKeyDown = (event: KeyboardEvent, callback: (next: string) => void) => {
  27. if (event.key === "Enter") {
  28. event.preventDefault()
  29. saveEditor(callback)
  30. return
  31. }
  32. if (event.key !== "Escape") return
  33. event.preventDefault()
  34. closeEditor()
  35. }
  36. const InlineEditor = (props: {
  37. id: string
  38. value: Accessor<string>
  39. onSave: (next: string) => void
  40. class?: string
  41. displayClass?: string
  42. editing?: boolean
  43. stopPropagation?: boolean
  44. openOnDblClick?: boolean
  45. }) => {
  46. let frame: number | undefined
  47. onCleanup(() => {
  48. if (frame === undefined) return
  49. cancelAnimationFrame(frame)
  50. })
  51. const isEditing = () => props.editing ?? editorOpen(props.id)
  52. const stopEvents = () => props.stopPropagation ?? false
  53. const allowDblClick = () => props.openOnDblClick ?? true
  54. const stopPropagation = (event: Event) => {
  55. if (!stopEvents()) return
  56. event.stopPropagation()
  57. }
  58. const handleDblClick = (event: MouseEvent) => {
  59. if (!allowDblClick()) return
  60. stopPropagation(event)
  61. openEditor(props.id, props.value())
  62. }
  63. return (
  64. <Show
  65. when={isEditing()}
  66. fallback={
  67. <span
  68. class={props.displayClass ?? props.class}
  69. onDblClick={handleDblClick}
  70. onPointerDown={stopPropagation}
  71. onMouseDown={stopPropagation}
  72. onClick={stopPropagation}
  73. onTouchStart={stopPropagation}
  74. >
  75. {props.value()}
  76. </span>
  77. }
  78. >
  79. <InlineInput
  80. ref={(el) => {
  81. if (frame !== undefined) cancelAnimationFrame(frame)
  82. frame = requestAnimationFrame(() => {
  83. frame = undefined
  84. if (!el.isConnected) return
  85. el.focus()
  86. })
  87. }}
  88. value={editorValue()}
  89. class={props.class}
  90. onInput={(event) => setEditor("value", event.currentTarget.value)}
  91. onKeyDown={(event) => {
  92. event.stopPropagation()
  93. editorKeyDown(event, props.onSave)
  94. }}
  95. onBlur={closeEditor}
  96. onPointerDown={stopPropagation}
  97. onClick={stopPropagation}
  98. onDblClick={stopPropagation}
  99. onMouseDown={stopPropagation}
  100. onMouseUp={stopPropagation}
  101. onTouchStart={stopPropagation}
  102. />
  103. </Show>
  104. )
  105. }
  106. return {
  107. editor,
  108. editorOpen,
  109. editorValue,
  110. openEditor,
  111. closeEditor,
  112. saveEditor,
  113. editorKeyDown,
  114. setEditor,
  115. InlineEditor,
  116. }
  117. }