Better commands experience

pull/645/head
Tienson Qin 2020-05-10 15:39:08 +08:00
parent 22c3f4779f
commit 6b5e42d762
5 changed files with 273 additions and 181 deletions

View File

@ -4,32 +4,42 @@
[clojure.string :as string]
[goog.dom :as gdom]))
(defonce *show-commands (atom false))
(defonce *slash-caret-pos (atom nil))
(defn ->page-reference
[page]
(util/format "[[%s]]" page))
(def link-steps [[:editor/input "[[][]]"]
[:editor/cursor-back 4]
(def link-steps [[:editor/input "/link"]
[:editor/show-input [{:id :link
:placeholder "Link"}
{:id :label
:placeholder "Label"}]]])
(defn ->marker
[marker]
[[:editor/clear-current-slash]
[:editor/set-marker marker]
[:editor/move-cursor-to-end]])
;; Credits to roamresearch.com
(defn commands-map
[]
(->>
(concat
[["TODO" "TODO"]
["DOING" "DOING"]
[["TODO" (->marker "TODO")]
["DOING" (->marker "DOING")]
["DONE" (->marker "DONE")]
["WAIT" (->marker "WAIT")]
["CANCELED" (->marker "CANCELED")]
["Tomorrow" (->page-reference (util/tomorrow))]
["Yesterday" (->page-reference (util/yesterday))]
["Today" (->page-reference (util/today))]
["Current Time" (util/get-current-time)]
["Date Picker" [[:editor/input "[[]]"]
[:editor/cursor-back 2]
[:editor/show-date-picker]]]
["Page Reference" [[:editor/input "[[]]"]
[:editor/cursor-back 2]
["Date Picker" [[:editor/show-date-picker]]]
["Page Reference" [[:editor/input "[[]]" {:backward-pos 2}]
;; [:editor/clear-current-slash]
[:editor/search-page]]]
["Link" link-steps]
;; same as link
@ -43,12 +53,18 @@
(util/remove-nils)
(util/distinct-by-last-wins first)))
(defonce *matched-commands (atom (commands-map)))
(defn restore-state
[restore-slash-caret-pos?]
(when restore-slash-caret-pos?
(reset! *slash-caret-pos nil))
(reset! *show-commands false)
(reset! *matched-commands (commands-map)))
(defn insert!
[id value
*slash-caret-pos
*show-commands
*matched-commands
& {:keys [last-pattern postfix-fn forward-pos]
{:keys [last-pattern postfix-fn backward-pos forward-pos]
:or {last-pattern "/"}}]
(let [edit-content (state/get-edit-content)
input (gdom/getElement id)
@ -56,20 +72,23 @@
prefix (subs edit-content 0 current-pos)
prefix (if (string/blank? last-pattern)
(str prefix " " value)
(str (string/trimr prefix) " " (string/triml value))
(util/replace-last last-pattern prefix value))
postfix (subs edit-content current-pos)
postfix (if postfix-fn (postfix-fn postfix) postfix)
new-value (str prefix postfix)]
(when *slash-caret-pos
(reset! *slash-caret-pos nil))
(when *show-commands
(reset! *show-commands nil))
(when *matched-commands
(reset! *matched-commands (commands-map)))
new-value (str (string/trimr prefix)
" "
(string/triml postfix))
new-pos (- (+ (count prefix)
(or forward-pos 0))
(or backward-pos 0))]
(swap! state/state assoc
:edit-content new-value
:editor/last-saved-cursor (+ (count prefix) (or forward-pos 0)))))
:editor/last-saved-cursor new-pos)
(util/move-cursor-to input
(if (or backward-pos forward-pos)
new-pos
(+ new-pos 1)))))
(defn get-matched-commands
[text]
@ -88,9 +107,9 @@
(defmulti handle-step first)
(defmethod handle-step :editor/input [[_ value]]
(defmethod handle-step :editor/input [[_ value option]]
(when-let [input-id (state/get-edit-input-id)]
(insert! input-id value nil nil nil)))
(insert! input-id value option)))
(defmethod handle-step :editor/cursor-back [[_ n]]
(when-let [input-id (state/get-edit-input-id)]
@ -102,6 +121,38 @@
(when-let [current-input (gdom/getElement input-id)]
(util/cursor-move-forward current-input n))))
(defmethod handle-step :editor/move-cursor-to-end [[_]]
(when-let [input-id (state/get-edit-input-id)]
(when-let [current-input (gdom/getElement input-id)]
(util/move-cursor-to-end current-input))))
(defmethod handle-step :editor/clear-current-slash [[_]]
(when-let [input-id (state/get-edit-input-id)]
(when-let [current-input (gdom/getElement input-id)]
(let [edit-content (state/get-edit-content)
current-pos (:pos (util/get-caret-pos current-input))
prefix (subs edit-content 0 current-pos)
prefix (util/replace-last "/" prefix "")
new-value (str prefix
(subs edit-content current-pos))]
(swap! state/state assoc
:edit-content new-value
:editor/last-saved-cursor (count prefix))))))
(def marker-pattern
#"(TODO|DOING|DONE|WAIT|CANCELED|STARTED|IN-PROGRESS)?\s?")
(defmethod handle-step :editor/set-marker [[_ marker]]
(when-let [input-id (state/get-edit-input-id)]
(when-let [current-input (gdom/getElement input-id)]
(let [edit-content (state/get-edit-content)
pos (count (re-find #"\*+\s" edit-content))
new-value (str (subs edit-content 0 pos)
(string/replace-first (subs edit-content pos)
marker-pattern
(str marker " ")))]
(state/set-edit-content! new-value)))))
(defmethod handle-step :editor/search-page [[_]]
(state/set-editor-show-page-search true))
@ -119,8 +170,6 @@
(prn "No handler for step: " type))
(defn handle-steps
[vector *show-commands *matched-commands]
(reset! *show-commands nil)
(reset! *matched-commands nil)
[vector]
(doseq [step vector]
(handle-step step)))

View File

@ -11,70 +11,107 @@
[goog.object :as gobj]
[goog.dom :as gdom]
[clojure.string :as string]
[frontend.commands :as commands]
[frontend.commands :as commands
:refer [*show-commands
*matched-commands
*slash-caret-pos]]
[medley.core :as medley]
[cljs-time.core :as t]
[cljs-time.coerce :as tc]
[cljs-drag-n-drop.core :as dnd]))
(defonce *show-commands (atom false))
(defonce *matched-commands (atom nil))
(defonce *slash-caret-pos (atom nil))
(defonce *should-delete? (atom false))
;; FIXME: should support multiple images concurrently uploading
(defonce *image-uploading? (atom false))
(defonce *image-uploading-process (atom 0))
(defn- insert-command!
[id command-output]
[id command-output {:keys [restore?]
:or {restore? true}
:as option}]
(cond
;; replace string
(string? command-output)
(commands/insert! id command-output *slash-caret-pos *show-commands *matched-commands)
(commands/insert! id command-output option)
;; steps
(vector? command-output)
(commands/handle-steps command-output *show-commands *matched-commands)
(commands/handle-steps command-output)
:else
nil))
nil)
(when restore?
(commands/restore-state restore?)))
(defn- upload-image
[id files uploading? drop?]
(image/upload
files
(fn [file file-name file-type]
(handler/request-presigned-url
file file-name file-type
uploading?
(fn [signed-url]
(insert-command! id
(util/format "[[%s][%s]]"
signed-url
file-name)
{:last-pattern (if drop? "" "/")})
(reset! *image-uploading-process 0))
(fn [e]
(let [process (* (/ (gobj/get e "loaded")
(gobj/get e "total"))
100)]
(reset! *image-uploading-process process)))))))
(rum/defc commands < rum/reactive
{:will-mount (fn [state]
(reset! *matched-commands (commands/commands-map))
state)}
[id]
(when (rum/react *show-commands)
(when (and (rum/react *show-commands)
(not (state/sub :editor/show-page-search?))
(not (state/sub :editor/show-input))
(not (state/sub :editor/show-date-picker?)))
(let [matched (rum/react *matched-commands)]
(ui/auto-complete
(map first matched)
(fn [chosen]
(insert-command! id (get (into {} matched) chosen)))))))
(let [restore-slash? (not (contains? #{"Page Reference"
"Link"
"Image Link"
"Date Picker"} chosen))]
(insert-command! id (get (into {} matched) chosen)
{:restore? restore-slash?})))))))
(rum/defc page-search < rum/reactive
[id]
(when (state/sub :editor/show-page-search?)
(let [{:keys [pos]} (rum/react *slash-caret-pos)
input (gdom/getElement id)
current-pos (:pos (util/get-caret-pos input))
edit-content (state/sub :edit-content)
q (subs edit-content (inc pos) current-pos)
matched-pages (when-not (string/blank? q)
(defn get-matched-pages
[q]
(let [pages (db/get-pages (state/get-current-repo))]
(filter
(fn [page]
(string/index-of
(string/lower-case page)
(string/lower-case q)))
pages)))]
pages)))
(rum/defc page-search < rum/reactive
[id]
(when (state/sub :editor/show-page-search?)
(let [pos (:editor/last-saved-cursor @state/state)
input (gdom/getElement id)]
(when input
(let [current-pos (:pos (util/get-caret-pos input))
edit-content (state/sub :edit-content)
q (subs edit-content pos current-pos)
matched-pages (when-not (string/blank? q)
(get-matched-pages q))]
(ui/auto-complete
matched-pages
(fn [chosen click?]
(commands/insert! id (str "[[" chosen)
*slash-caret-pos
*show-commands
*matched-commands
:last-pattern "[["
:forward-pos 2)
(insert-command! id
(util/format "[[%s]]" chosen)
{:last-pattern (str "[[" q)
:postfix-fn (fn [s] (util/replace-first "]]" s ""))})
(state/set-editor-show-page-search false))
:empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a page"]))))
:empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a page"]))))))
(rum/defc date-picker < rum/reactive
[id]
@ -86,12 +123,9 @@
(util/stop e)
(let [journal (util/journal-name (tc/to-date date))]
;; similar to page reference
(commands/insert! id (str "[[" journal)
*slash-caret-pos
*show-commands
*matched-commands
:last-pattern "[["
:forward-pos 2)
(insert-command! id
(util/format "[[%s]]" journal)
nil)
(state/set-editor-show-date-picker false)))})))
(rum/defcs input < rum/reactive
@ -228,7 +262,7 @@
(let [{:keys [top left pos]} (rum/react *slash-caret-pos)]
[:div.absolute.rounded-md.shadow-lg
{:style (merge
{:top (+ top 20)
{:top (+ top 24)
:left left}
(if set-default-width?
{:width 400}))}
@ -242,6 +276,24 @@
:exit 300}}
(absolute-modal cp set-default-width?)))
(rum/defc image-uploader < rum/reactive
[id]
[:<>
[:input
{:id "upload-file"
:type "file"
:on-change (fn [e]
(let [files (.-files (.-target e))]
(upload-image id files *image-uploading? false)))
:hidden true}]
(when-let [uploading? (rum/react *image-uploading?)]
(let [processing (rum/react *image-uploading-process)]
(transition-cp
[:div.flex.flex-row.align-center
[:span.lds-dual-ring.mr-2]
[:span {:style {:margin-top 2}}
(util/format "Uploading %s%" (util/format "%2d" processing))]])))])
(rum/defc box < rum/reactive
(mixins/event-mixin
(fn [state]
@ -281,8 +333,9 @@
(reset! *show-commands false))))
}
(fn [e key-code]
(swap! state/state assoc
:editor/last-saved-cursor nil)))
;; (swap! state/state assoc
;; :editor/last-saved-cursor nil)
))
(mixins/on-key-up
state
{
@ -297,13 +350,18 @@
(when (not= key-code 191) ; not /
(let [matched-commands (get-matched-commands input)]
(if (seq matched-commands)
(if (= key-code 9) ;tab
(do
(cond
(= key-code 9) ;tab
(do
(util/stop e)
(insert-command! input-id (last (first matched-commands))))
(insert-command! input-id (last (first matched-commands)) nil))
:else
(do
(reset! *matched-commands matched-commands)
(reset! *show-commands true)))
(reset! *show-commands true))
))
(reset! *show-commands false)))))))))
{:init (fn [state _props]
(let [[content {:keys [dummy?]}] (:rum/args state)]
@ -311,9 +369,7 @@
(state/set-edit-content!
(if dummy?
(string/triml content)
(string/trim content)))
(swap! state/state assoc
:editor/last-saved-cursor nil))
(string/trim content))))
state)
:did-mount (fn [state]
(let [[content opts id] (:rum/args state)]
@ -325,10 +381,7 @@
(dnd/subscribe!
input
:upload-images
{:drop (fn [e files]
(js/console.dir files)
(handler/upload-image
id files *slash-caret-pos *show-commands *matched-commands true))})))
{:drop (fn [e files] (upload-image id files *image-uploading? true))})))
state)
:will-unmount (fn [state]
(let [[content opts id] (:rum/args state)]
@ -336,14 +389,6 @@
(dnd/unsubscribe!
input
:upload-images)))
state)
:did-update (fn [state]
(when-let [saved-cursor (get @state/state :editor/last-saved-cursor)]
(let [[_content _opts id] (:rum/args state)
input (gdom/getElement id)]
(when input
(.focus input)
(util/move-cursor-to input saved-cursor))))
state)}
[content {:keys [on-hide dummy? node]
:or {dummy? false}} id]
@ -376,26 +421,19 @@
(transition-cp
(input id
(fn [{:keys [link label]} pos]
(when-not (and (string/blank? link)
(if (and (string/blank? link)
(string/blank? label))
(commands/insert! id
nil
(insert-command! id
(util/format "[[%s][%s]]"
(or link "")
(or label ""))
*slash-caret-pos
*show-commands
*matched-commands
:last-pattern "[["
:postfix-fn (fn [s]
(util/replace-first "][]]" s ""))))
(state/set-editor-show-input nil)))
{:last-pattern "/link"}))
(state/set-editor-show-input nil)
(when-let [saved-cursor (get @state/state :editor/last-saved-cursor)]
(when-let [input (gdom/getElement id)]
(.focus input)
(util/move-cursor-to input saved-cursor)))))
true)
[:input
{:id "upload-file"
:type "file"
:on-change (fn [e]
(let [files (.-files (.-target e))]
(handler/upload-image
id files *slash-caret-pos *show-commands *matched-commands false)))
:hidden true}]]))
(image-uploader id)]))

View File

@ -415,7 +415,7 @@
(let [agenda? (= (:id config) "agenda")
checkbox (heading-checkbox t
(str "mr-1 cursor"))
marker (if (contains? #{"DOING" "IN-PROGRESS" "WAIT"} marker)
marker-cp (if (contains? #{"DOING" "IN-PROGRESS" "WAIT"} marker)
[:span {:class (str "task-status " (string/lower-case marker))
:style {:margin-right 3.5}}
(string/upper-case marker)])
@ -433,17 +433,18 @@
name]])
tags)))
heading-part (when level
(let [element (keyword (str "h" level))
level-str [:a.initial-color {:href (str "/page/" uuid)}
(str (apply str (repeat level "*")) " ")]]
(let [element (if (<= level 6)
(keyword (str "h" level))
:div)]
(->elem element
(merge
{:id anchor}
(when marker
{:class (string/lower-case marker)}))
(remove-nils
(concat
[
;; (when-not agenda? level-str)
checkbox
marker
[checkbox
marker-cp
priority]
(map-inline title)
[tags])))))]

View File

@ -560,12 +560,15 @@
(state/update-state! :journals-length inc))))
(defn request-presigned-url
[file filename mime-type url-handler]
[file filename mime-type uploading? url-handler on-processing]
(cond
(> (gobj/get file "size") (* 20 1024 1024))
(show-notification! [:p "Sorry, we don't support any file that's larger than 20MB."] :error)
(> (gobj/get file "size") (* 12 1024 1024))
(show-notification! [:p "Sorry, we don't support any file that's larger than 12MB."] :error)
:else
(do
(reset! uploading? true)
;; start uploading?
(util/post (str config/api "presigned_url")
{:filename filename
:mime-type mime-type}
@ -578,20 +581,27 @@
(util/post (str config/api "signed_url")
{:s3-object-key s3-object-key}
(fn [{:keys [signed-url]}]
(reset! uploading? false)
(if signed-url
(do
(url-handler signed-url))
(prn "Something error, can't get a valid signed url.")))
(fn [error]
(reset! uploading? false)
(prn "Something error, can't get a valid signed url."))))
(fn [error]
(reset! uploading? false)
(prn "upload failed.")
(js/console.dir error)))
(js/console.dir error))
(fn [e]
(on-processing e)))
;; TODO: notification, or re-try
(prn "failed to get any presigned url, resp: " resp)))
(do
(reset! uploading? false)
(prn "failed to get any presigned url, resp: " resp))))
(fn [_error]
;; (prn "Get token failed, error: " error)
))))
(reset! uploading? false))))))
(defn set-me-if-exists!
[]
@ -879,23 +889,6 @@
(clone-and-pull repo)))))
(watch-config!)))
(defn upload-image
[id files *slash-caret-pos *show-commands *matched-commands drop?]
(image/upload
files
(fn [file file-name file-type]
(request-presigned-url
file file-name file-type
(fn [signed-url]
(commands/insert! id
(util/format "[[%s][%s]]"
signed-url
file-name)
*slash-caret-pos
*show-commands
*matched-commands
:last-pattern (if drop? "" "/")))))))
(comment
(defn debug-latest-commits

View File

@ -142,12 +142,17 @@
(on-failed resp))))))))
(defn upload
[url file on-ok on-failed]
(-> (js/fetch url (clj->js {:method "put"
:body file}))
(.then #(if (.-ok %)
(on-ok %)
(on-failed %)))))
[url file on-ok on-failed on-progress]
(let [xhr (js/XMLHttpRequest.)]
(.open xhr "put" url)
(gobj/set xhr "onload" on-ok)
(gobj/set xhr "onerror" on-failed)
(when (and (gobj/get xhr "upload")
on-progress)
(gobj/set (gobj/get xhr "upload")
"onprogress"
on-progress))
(.send xhr file)))
(defn post
[url body on-ok on-failed]
@ -449,8 +454,9 @@
(defn replace-last [pattern s new-value]
(when-let [last-index (string/last-index-of s pattern)]
(str (subs s 0 last-index)
new-value)))
(str (string/trimr (subs s 0 last-index))
" "
(string/triml new-value))))
(defn cursor-move-back [input n]
(let [{:keys [pos]} (get-caret-pos input)]
@ -466,6 +472,11 @@
(set! (.-selectionStart input) n)
(set! (.-selectionEnd input) n))
(defn move-cursor-to-end
[input]
(let [pos (count (gobj/get input "value"))]
(move-cursor-to input pos)))
;; copied from re_com
(defn deref-or-value
"Takes a value or an atom