1
0

command_palette.cljs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. (ns frontend.handler.command-palette
  2. "System-component-like ns for command palette's functionality"
  3. (:require [cljs.spec.alpha :as s]
  4. [frontend.modules.shortcut.data-helper :as shortcut-helper]
  5. [frontend.spec :as spec]
  6. [frontend.state :as state]
  7. [lambdaisland.glogi :as log]
  8. [frontend.storage :as storage]))
  9. (s/def :command/id keyword?)
  10. (s/def :command/desc string?)
  11. (s/def :command/action fn?)
  12. (s/def :command/shortcut string?)
  13. (s/def :command/tag vector?)
  14. (s/def :command/command
  15. (s/keys :req-un [:command/id :command/action]
  16. ;; :command/desc is optional for internal commands since view
  17. ;; checks translation ns first
  18. :opt-un [:command/desc :command/shortcut :command/tag]))
  19. (defn global-shortcut-commands []
  20. (->> [:shortcut.handler/editor-global
  21. :shortcut.handler/global-prevent-default
  22. :shortcut.handler/global-non-editing-only]
  23. (mapcat shortcut-helper/shortcuts->commands)))
  24. (defn get-commands []
  25. (->> (get @state/state :command-palette/commands)
  26. (sort-by :id)))
  27. (defn get-commands-unique []
  28. (reduce #(assoc %1 (:id %2) %2) {}
  29. (get @state/state :command-palette/commands)))
  30. (defn history
  31. ([] (or (storage/get "commands-history") []))
  32. ([vals] (storage/set "commands-history" vals)))
  33. (defn- assoc-invokes [cmds]
  34. (let [invokes (->> (history)
  35. (map :id)
  36. (frequencies))]
  37. (mapv (fn [{:keys [id] :as cmd}]
  38. (if (contains? invokes id)
  39. (assoc cmd :invokes-count (get invokes id))
  40. cmd))
  41. cmds)))
  42. (defn add-history [{:keys [id]}]
  43. (storage/set "commands-history" (conj (history) {:id id :timestamp (.getTime (js/Date.))})))
  44. (defn invoke-command [{:keys [action] :as cmd}]
  45. (add-history cmd)
  46. (state/set-state! :ui/command-palette-open? false)
  47. (state/close-modal!)
  48. (action))
  49. (defn top-commands [limit]
  50. (->> (get-commands)
  51. (assoc-invokes)
  52. (sort-by :invokes-count)
  53. (reverse)
  54. (take limit)))
  55. (defn register
  56. "Register a global command searchable by command palette.
  57. `id` is defined as a global unique namespaced key :scope/command-name
  58. `action` must be a zero arity function
  59. Example:
  60. ```clojure
  61. (register
  62. {:id :document/open-logseq-doc
  63. :desc \"Document: open Logseq documents\"
  64. :action (fn [] (js/window.open \"https://docs.logseq.com/\"))})
  65. ```
  66. To add i18n support, prefix `id` with command and put that item in dict.
  67. Example: {:zh-CN {:command.document/open-logseq-doc \"打开文档\"}}"
  68. [{:keys [id] :as command}]
  69. (spec/validate :command/command command)
  70. (let [cmds (get-commands)]
  71. (if (some (fn [existing-cmd] (= (:id existing-cmd) id)) cmds)
  72. (log/error :command/register {:msg "Failed to register command. Command with same id already exist"
  73. :id id})
  74. (state/set-state! :command-palette/commands (conj cmds command)))))
  75. (defn unregister
  76. [id]
  77. (let [id (keyword id)
  78. cmds (get-commands-unique)]
  79. (when (contains? cmds id)
  80. (state/set-state! :command-palette/commands (vals (dissoc cmds id)))
  81. ;; clear history
  82. (history (filter #(not= (:id %) id) (history))))))
  83. (defn register-global-shortcut-commands []
  84. (let [cmds (global-shortcut-commands)]
  85. (doseq [cmd cmds] (register cmd))))
  86. (comment
  87. ;; register custom command example
  88. (register
  89. {:id :document/open-logseq-doc
  90. :desc "Document: open Logseq documents"
  91. :action (fn [] (js/window.open "https://docs.logseq.com/"))}))