Browse Source

Merge branch 'master' into feat/light-theme

anoffvu 5 years ago
parent
commit
d409aa682c

+ 2 - 0
package.json

@@ -25,11 +25,13 @@
         "release-publishing": "run-s cljs:release-publishing gulp:build",
         "dev-release-app": "run-s cljs:dev-release-app gulp:build",
         "clean": "gulp clean",
+        "test": "run-s cljs:test cljs:run-test",
         "gulp:watch": "gulp watch",
         "gulp:build": "NODE_ENV=production gulp build",
         "cljs:watch": "clojure -M:cljs watch app publishing",
         "cljs:release": "clojure -M:cljs release app publishing",
         "cljs:test": "clojure -A:test compile test",
+        "cljs:run-test": "node static/tests.js",
         "cljs:watch-app": "clojure -M:cljs watch app",
         "cljs:release-app": "clojure -M:cljs release app",
         "cljs:release-publishing": "clojure -M:cljs release publishing",

+ 19 - 21
resources/css/common.css

@@ -246,25 +246,6 @@ li {
   white-space: pre-line;
 }
 
-.editor textarea {
-  border: none;
-  border-radius: 0;
-  background: transparent;
-  padding: 0;
-}
-
-.non-block-editor textarea, pre {
-  display: block;
-  padding: 0.5rem;
-  box-shadow: 0 0 0 1px rgba(0, 0, 0, .02);
-  border-radius: var(--border-radius-low);
-}
-
-.non-block-editor textarea {
-  background: #F6F8FA;
-  background: var(--ls-secondary-background-color);
-}
-
 pre {
   background: #F6F8FA;
   background: var(--ls-secondary-background-color);
@@ -1089,8 +1070,8 @@ button.context-menu-option {
 }
 
 .content img {
-  margin-top: 1rem;
-  margin-bottom: 1rem;
+  margin-top: .5rem;
+  margin-bottom: .5rem;
 }
 
 a.login {
@@ -1492,6 +1473,23 @@ a.tag:hover {
   padding: 5px;
 }
 
+// auto complete
+#ui__ac {
+  @apply rounded-md shadow-xs bg-base-3;
+}
+
+#ui__ac-inner {
+  max-height: 400px;
+  overflow-x: hidden;
+  overflow-y: auto;
+  position: relative;
+  -webkit-overflow-scrolling: touch;
+
+  > .menu-link {
+    padding: 6px 0;
+  }
+}
+
 /* endregion */
 
 /* Hide scrollbar for IE, Edge and Firefox */

+ 24 - 18
src/main/frontend/components/block.cljs

@@ -17,6 +17,7 @@
             [frontend.ui :as ui]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.dnd :as dnd]
+            [frontend.handler.ui :as ui-handler]
             [frontend.handler.repeated :as repeated]
             [goog.object :as gobj]
             [medley.core :as medley]
@@ -157,10 +158,8 @@
   (let [href (if (util/starts-with? href "http")
                href
                (get-file-absolute-path config href))]
-    [:img.rounded-sm.shadow-xl.mb-2.mt-2
-     {:class "object-contain object-center"
-      :loading "lazy"
-      :style {:max-height "24rem"}
+    [:img.rounded-sm.shadow-xl
+     {:loading "lazy"
       ;; :on-error (fn [])
       :src href
       :title (second (first label))}]))
@@ -241,7 +240,7 @@
 (declare page-reference)
 
 (defn page-cp
-  [{:keys [html-export? label children] :as config} page]
+  [{:keys [html-export? label children contents-page?] :as config} page]
   (when-let [page-name (:page/name page)]
     (let [original-page-name (get page :page/original-name page-name)
           original-page-name (if (date/valid-journal-title? original-page-name)
@@ -261,7 +260,11 @@
                          (state/get-current-repo)
                          (:db/id page-entity)
                          :page
-                         {:page page-entity}))))}
+                         {:page page-entity})))
+                    (when (and contents-page?
+                               (state/get-left-sidebar-open?))
+                      (ui-handler/close-left-sidebar!)))}
+
        (if (seq children)
          (for [child children]
            (if (= (first child) "Label")
@@ -277,16 +280,19 @@
 
 (defn page-reference
   [html-export? s config label]
-  [:span.page-reference
-   (if (string/ends-with? s ".excalidraw")
-     [:a.page-ref
-      {:href (rfe/href :draw nil {:file (string/replace s (str config/default-draw-directory "/") "")})
-       :on-click (fn [e] (util/stop e))}
-      [:span
-       (svg/excalidraw-logo)
-       (string/capitalize (draw/get-file-title s))]]
-     (page-cp (assoc config
-                     :label (mldoc/plain->text label)) {:page/name s}))])
+  (let [contents-page? (= "contents" (string/lower-case (str (:id config))))]
+    [:span.page-reference
+     (if (string/ends-with? s ".excalidraw")
+       [:a.page-ref
+        {:href (rfe/href :draw nil {:file (string/replace s (str config/default-draw-directory "/") "")})
+         :on-click (fn [e]
+                     (util/stop e))}
+        [:span
+         (svg/excalidraw-logo)
+         (string/capitalize (draw/get-file-title s))]]
+       (page-cp (assoc config
+                       :label (mldoc/plain->text label)
+                       :contents-page? contents-page?) {:page/name s}))]))
 
 (defn- latex-environment-content
   [name option content]
@@ -602,10 +608,10 @@
                                   nil)]
             (when-not (string/blank? youtube-id)
               [:iframe
-               {:allowfullscreen "allowfullscreen"
+               {:allow-full-screen "allowfullscreen"
                 :allow
                 "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
-                :frameborder "0"
+                :frame-border "0"
                 :src (str "https://www.youtube.com/embed/" youtube-id)
                 :height "315"
                 :width "560"}])))

+ 15 - 0
src/main/frontend/components/block.css

@@ -0,0 +1,15 @@
+.blocks-container {
+}
+
+.block-content {
+}
+
+.block-children {
+}
+
+.ls-block {
+}
+
+.block-content img {
+  width: 100%;
+}

+ 1 - 1
src/main/frontend/components/diff.cljs

@@ -142,7 +142,7 @@
      (when-let [repo (state/get-current-repo)]
        (p/let [remote-latest-commit (common-handler/get-remote-ref repo)
                local-latest-commit (common-handler/get-ref repo)
-               result (git/get-local-diffs repo local-latest-commit remote-latest-commit)
+               result (git/get-diffs repo local-latest-commit remote-latest-commit)
                token (helper/get-github-token repo)]
          (reset! state/diffs result)
          (reset! remote-hash-id remote-latest-commit)

+ 157 - 167
src/main/frontend/components/editor.cljs

@@ -50,7 +50,7 @@
                                            (and
                                             (not (fn? command-steps))
                                             (not (contains? (set (map first command-steps)) :editor/input))
-                                            (not (contains? #{"Date Picker" "Template" "Deadline" "Scheduled"} chosen))))]
+                                            (not (contains? #{"Date Picker" "Template" "Deadline" "Scheduled" "Upload an image"} chosen))))]
                        (editor-handler/insert-command! id command-steps
                                                        format
                                                        {:restore? restore-slash?})))
@@ -268,8 +268,7 @@
                       {:keys [pos]} @*slash-caret-pos
                       command (:command (first input-option))]
                   (on-submit command @input-value pos))
-                (reset! input-value nil))))}
-      nil)))
+                (reset! input-value nil))))})))
   {:did-update
    (fn [state]
      (when-let [show-input (state/get-editor-show-input)]
@@ -360,8 +359,6 @@
            input-id id
            input (gdom/getElement input-id)
            repo (:block/repo block)]
-       ;; (.addEventListener input "paste" (fn [event]
-       ;;                                    (editor-handler/append-paste-doc! format event)))
        (mixins/on-key-down
         state
         {;; enter
@@ -415,160 +412,158 @@
                      (not (editor-handler/in-auto-complete? input)))
                 (editor-handler/on-up-down state e false)))
          ;; backspace
-         8 (fn [state e]
-             (let [node (gdom/getElement input-id)
-                   current-pos (:pos (util/get-caret-pos node))
-                   value (gobj/get node "value")
-                   deleted (and (> current-pos 0)
-                                (util/nth-safe value (dec current-pos)))
-                   selected-start (gobj/get node "selectionStart")
-                   selected-end (gobj/get node "selectionEnd")
-                   block-id (:block-id (first (:rum/args state)))
-                   page (state/get-current-page)]
-               (cond
-                 (not= selected-start selected-end)
-                 nil
-
-                 (and (zero? current-pos)
-                      ;; not the top block in a block page
-                      (not (and page
-                                (util/uuid-string? page)
-                                (= (medley/uuid page) block-id))))
-
-                 (editor-handler/delete-block! state repo e)
-
-                 (and (> current-pos 1)
-                      (= (util/nth-safe value (dec current-pos)) commands/slash))
-                 (do
-                   (reset! *slash-caret-pos nil)
-                   (reset! *show-commands false))
-
-                 (and (> current-pos 1)
-                      (= (util/nth-safe value (dec current-pos)) commands/angle-bracket))
-                 (do
-                   (reset! *angle-bracket-caret-pos nil)
-                   (reset! *show-block-commands false))
-
-                 ;; pair
-                 (and
-                  deleted
-                  (contains?
-                   (set (keys editor-handler/delete-map))
-                   deleted)
-                  (>= (count value) (inc current-pos))
-                  (= (util/nth-safe value current-pos)
-                     (get editor-handler/delete-map deleted)))
-
-                 (do
-                   (util/stop e)
-                   (commands/delete-pair! id)
-                   (cond
-                     (and (= deleted "[") (state/get-editor-show-page-search?))
-                     (state/set-editor-show-page-search! false)
-
-                     (and (= deleted "(") (state/get-editor-show-block-search?))
-                     (state/set-editor-show-block-search! false)
-
-                     :else
-                     nil))
-
-                 ;; deleting hashtag
-                 (and (= deleted "#") (state/get-editor-show-page-search-hashtag?))
-                 (state/set-editor-show-page-search-hashtag! false)
-
-                 :else
-                 nil)))
+         8  (fn [state e]
+              (let [node (gdom/getElement input-id)
+                    current-pos (:pos (util/get-caret-pos node))
+                    value (gobj/get node "value")
+                    deleted (and (> current-pos 0)
+                                 (util/nth-safe value (dec current-pos)))
+                    selected-start (gobj/get node "selectionStart")
+                    selected-end (gobj/get node "selectionEnd")
+                    block-id (:block-id (first (:rum/args state)))
+                    page (state/get-current-page)]
+                (cond
+                  (not= selected-start selected-end)
+                  nil
+
+                  (and (zero? current-pos)
+                       ;; not the top block in a block page
+                       (not (and page
+                                 (util/uuid-string? page)
+                                 (= (medley/uuid page) block-id))))
+                  (editor-handler/delete-block! state repo e)
+
+                  (and (> current-pos 1)
+                       (= (util/nth-safe value (dec current-pos)) commands/slash))
+                  (do
+                    (reset! *slash-caret-pos nil)
+                    (reset! *show-commands false))
+
+                  (and (> current-pos 1)
+                       (= (util/nth-safe value (dec current-pos)) commands/angle-bracket))
+                  (do
+                    (reset! *angle-bracket-caret-pos nil)
+                    (reset! *show-block-commands false))
+
+                  ;; pair
+                  (and
+                   deleted
+                   (contains?
+                    (set (keys editor-handler/delete-map))
+                    deleted)
+                   (>= (count value) (inc current-pos))
+                   (= (util/nth-safe value current-pos)
+                      (get editor-handler/delete-map deleted)))
+
+                  (do
+                    (util/stop e)
+                    (commands/delete-pair! id)
+                    (cond
+                      (and (= deleted "[") (state/get-editor-show-page-search?))
+                      (state/set-editor-show-page-search! false)
+
+                      (and (= deleted "(") (state/get-editor-show-block-search?))
+                      (state/set-editor-show-block-search! false)
+
+                      :else
+                      nil))
+
+                  ;; deleting hashtag
+                  (and (= deleted "#") (state/get-editor-show-page-search-hashtag?))
+                  (state/set-editor-show-page-search-hashtag! false)
+
+                  :else
+                  nil)))
          ;; tab
-         9 (fn [state e]
-             (let [input-id (state/get-edit-input-id)
-                   input (and input-id (gdom/getElement id))
-                   pos (and input (:pos (util/get-caret-pos input)))]
-               (when-not (state/get-editor-show-input)
-                 (util/stop e)
-                 (let [direction (if (gobj/get e "shiftKey") ; shift+tab move to left
-                                   :left
-                                   :right)]
-                   (p/let [_ (editor-handler/adjust-block-level! state direction)]
-                     (and input pos (js/setTimeout #(when-let [input (gdom/getElement input-id)]
-                                                      (util/move-cursor-to input pos))
-                                                   0)))))))}
-        (fn [e key-code]
-          (let [key (gobj/get e "key")
-                value (gobj/get input "value")
-                pos (:pos (util/get-caret-pos input))]
-            (cond
-              (or
-               (and (= key "#")
-                    (and
-                     (> pos 0)
-                     (= "#" (util/nth-safe value (dec pos)))))
-               (and (= key " ")
-                    (state/get-editor-show-page-search-hashtag?)))
-              (state/set-editor-show-page-search-hashtag! false)
-
-              (and
-               (not= key-code 8) ;; backspace
+         9  (fn [state e]
+              (let [input-id (state/get-edit-input-id)
+                    input (and input-id (gdom/getElement id))
+                    pos (and input (:pos (util/get-caret-pos input)))]
+                (when-not (state/get-editor-show-input)
+                  (util/stop e)
+                  (let [direction (if (gobj/get e "shiftKey") ; shift+tab move to left
+                                    :left
+                                    :right)]
+                    (p/let [_ (editor-handler/adjust-block-level! state direction)]
+                      (and input pos (js/setTimeout #(when-let [input (gdom/getElement input-id)]
+                                                       (util/move-cursor-to input pos))
+                                                    0)))))))}
+        {:not-matched-handler
+         (fn [e key-code]
+           (let [key (gobj/get e "key")
+                 value (gobj/get input "value")
+                 pos (:pos (util/get-caret-pos input))]
+             (cond
+               (or
+                (and (= key "#")
+                     (and
+                      (> pos 0)
+                      (= "#" (util/nth-safe value (dec pos)))))
+                (and (= key " ")
+                     (state/get-editor-show-page-search-hashtag?)))
+               (state/set-editor-show-page-search-hashtag! false)
+
                (or
                 (editor-handler/surround-by? input "#" " ")
                 (editor-handler/surround-by? input "#" :end)
-                (= key "#")))
-              (do
-                (commands/handle-step [:editor/search-page-hashtag])
-                (state/set-last-pos! (:pos (util/get-caret-pos input)))
-                (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
-
-              (and
-               (= key " ")
-               (state/get-editor-show-page-search-hashtag?))
-              (state/set-editor-show-page-search-hashtag! false)
-
-              (and
-               (contains? (set/difference (set (keys editor-handler/reversed-autopair-map))
-                                          #{"`"})
-                          key)
-               (= (editor-handler/get-current-input-char input) key))
-              (do
-                (util/stop e)
-                (util/cursor-move-forward input 1))
+                (= key "#"))
+               (do
+                 (commands/handle-step [:editor/search-page-hashtag])
+                 (state/set-last-pos! (:pos (util/get-caret-pos input)))
+                 (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
+
+               (and
+                (= key " ")
+                (state/get-editor-show-page-search-hashtag?))
+               (state/set-editor-show-page-search-hashtag! false)
+
+               (and
+                (contains? (set/difference (set (keys editor-handler/reversed-autopair-map))
+                                           #{"`"})
+                           key)
+                (= (editor-handler/get-current-input-char input) key))
+               (do
+                 (util/stop e)
+                 (util/cursor-move-forward input 1))
 
-              (contains? (set (keys editor-handler/autopair-map)) key)
-              (do
-                (util/stop e)
-                (editor-handler/autopair input-id key format nil)
-                (cond
-                  (editor-handler/surround-by? input "[[" "]]")
-                  (do
-                    (commands/handle-step [:editor/search-page])
-                    (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
-                  (editor-handler/surround-by? input "((" "))")
-                  (do
-                    (commands/handle-step [:editor/search-block :reference])
-                    (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
-                  :else
-                  nil))
-
-              (let [sym "$"]
-                (and (= key sym)
-                     (>= (count value) 1)
-                     (> pos 0)
-                     (= (nth value (dec pos)) sym)
-                     (if (> (count value) pos)
-                       (not= (nth value pos) sym)
-                       true)))
-              (commands/simple-insert! input-id "$$" {:backward-pos 2})
-
-              (let [sym "^"]
-                (and (= key sym)
-                     (>= (count value) 1)
-                     (> pos 0)
-                     (= (nth value (dec pos)) sym)
-                     (if (> (count value) pos)
-                       (not= (nth value pos) sym)
-                       true)))
-              (commands/simple-insert! input-id "^^" {:backward-pos 2})
-
-              :else
-              nil))))
+               (contains? (set (keys editor-handler/autopair-map)) key)
+               (do
+                 (util/stop e)
+                 (editor-handler/autopair input-id key format nil)
+                 (cond
+                   (editor-handler/surround-by? input "[[" "]]")
+                   (do
+                     (commands/handle-step [:editor/search-page])
+                     (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
+                   (editor-handler/surround-by? input "((" "))")
+                   (do
+                     (commands/handle-step [:editor/search-block :reference])
+                     (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
+                   :else
+                   nil))
+
+               (let [sym "$"]
+                 (and (= key sym)
+                      (>= (count value) 1)
+                      (> pos 0)
+                      (= (nth value (dec pos)) sym)
+                      (if (> (count value) pos)
+                        (not= (nth value pos) sym)
+                        true)))
+               (commands/simple-insert! input-id "$$" {:backward-pos 2})
+
+               (let [sym "^"]
+                 (and (= key sym)
+                      (>= (count value) 1)
+                      (> pos 0)
+                      (= (nth value (dec pos)) sym)
+                      (if (> (count value) pos)
+                        (not= (nth value pos) sym)
+                        true)))
+               (commands/simple-insert! input-id "^^" {:backward-pos 2})
+
+               :else
+               nil)))})
        (mixins/on-key-up
         state
         {}
@@ -676,7 +671,7 @@
                                 (not (string/blank? value))
                                 (not= (string/trim value) (string/trim content)))
                            (let [old-page-name (db/get-file-page path false)]
-                             (page-handler/rename-when-alter-title-propertiy! old-page-name path format content value)
+                             (page-handler/rename-when-alter-title-property! old-page-name path format content value)
                              (file/alter-file (state/get-current-repo) path (string/trim value)
                                               {:re-render-root? true}))))
                        (when-not (contains? #{:insert :indent-outdent} (state/get-editor-op))
@@ -685,24 +680,19 @@
   [state {:keys [on-hide dummy? node format block block-parent-id]
           :or {dummy? false}
           :as option} id config]
-  (let [content (state/sub [:editor/content id])]
-    [:div.editor {:style {:position "relative"
-                          :display "flex"
-                          :flex "1 1 0%"}
-                  :class (if block "block-editor" "non-block-editor")}
+  (let [content (state/get-edit-content)]
+    [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
      (when config/mobile? (mobile-bar state id))
-     (ui/textarea
+     (ui/ls-textarea
       {:id id
-       :value (or content "")
+       :cacheMeasurements true
+       :default-value (or content "")
        :minRows (if (state/enable-grammarly?) 2 1)
        :on-click (fn [_e]
                    (let [input (gdom/getElement id)
                          current-pos (:pos (util/get-caret-pos input))]
                      (state/set-edit-pos! current-pos)
                      (editor-handler/close-autocomplete-if-outside input)))
-       :on-key-down (fn [_e]
-                      (let [current-pos (:pos (util/get-caret-pos (gdom/getElement id)))]
-                        (state/set-edit-pos! current-pos)))
        :on-change (fn [e]
                     (let [value (util/evalue e)
                           current-pos (:pos (util/get-caret-pos (gdom/getElement id)))]
@@ -716,8 +706,8 @@
                           ;; TODO: is it cross-browser compatible?
                           (when (not= (gobj/get native-e "inputType") "insertFromPaste")
                             (when-let [matched-commands (seq (editor-handler/get-matched-commands input))]
-                             (reset! *slash-caret-pos (util/get-caret-pos input))
-                             (reset! *show-commands true)))
+                              (reset! *slash-caret-pos (util/get-caret-pos input))
+                              (reset! *show-commands true)))
                           "<"
                           (when-let [matched-commands (seq (editor-handler/get-matched-block-commands input))]
                             (reset! *angle-bracket-caret-pos (util/get-caret-pos input))

+ 28 - 0
src/main/frontend/components/editor.css

@@ -0,0 +1,28 @@
+.editor-wrapper {
+
+}
+
+.editor-inner {
+  position: relative;
+  display: flex;
+}
+
+.editor-inner textarea {
+  border: none;
+  border-radius: 0;
+  background: transparent;
+  padding: 0;
+}
+
+.non-block-editor textarea, pre {
+  display: block;
+  padding: 0.5rem;
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, .02);
+  border-radius: 4px;
+}
+
+.non-block-editor textarea {
+  background: #F6F8FA;
+  background: var(--ls-secondary-background-color);
+}
+

+ 3 - 5
src/main/frontend/components/sidebar.cljs

@@ -87,7 +87,7 @@
      :style {:background-color "#002b36"}}
     (if @open?
       [:div.absolute.top-0.right-0.-mr-14.p-1
-       [:button.flex.items-center.justify-center.h-12.w-12.rounded-full.focus:outline-none.focus:bg-gray-600
+       [:button#close-left-bar.flex.items-center.justify-center.h-12.w-12.rounded-full.focus:outline-none.focus:bg-gray-600
         {:on-click close-fn}
         [:svg.h-6.w-6.text-white
          {:viewBox "0 0 24 24", :fill "none", :stroke "currentColor"}
@@ -294,9 +294,7 @@
               (when-let [repo-url (state/get-current-repo)]
                 (when-not (state/get-edit-input-id)
                   (util/stop e)
-                  (state/set-modal! commit/add-commit-message)))))}
-      (fn [e key-code]
-        nil))))
+                  (state/set-modal! commit/add-commit-message)))))})))
   {:did-mount (fn [state]
                 (keyboards/bind-shortcuts!)
                 state)}
@@ -359,4 +357,4 @@
          ;;   :on-click (fn []
          ;;               (state/set-left-sidebar-open! (not (state/get-left-sidebar-open?))))}
          ;;  (if (state/sub :ui/left-sidebar-open?) "<" ">")]
-         )])))
+)])))

+ 1 - 0
src/main/frontend/date.cljs

@@ -33,6 +33,7 @@
      "yyyy/MM/dd"
      "yyyy-MM-dd"
      "yyyy_MM_dd"
+     "yyyyMMdd"
      "yyyy年MM月dd日"}
    (state/get-date-formatter)))
 

+ 1 - 1
src/main/frontend/db.cljs

@@ -1843,7 +1843,7 @@
   [page-name]
   (:page/journal? (entity [:page/name page-name])))
 
-(defn mark-repo-as-cloned
+(defn mark-repo-as-cloned!
   [repo-url]
   (transact!
    [{:repo/url repo-url

+ 6 - 2
src/main/frontend/extensions/sci.cljs

@@ -11,11 +11,15 @@
 
 (defn call-fn
   [f & args]
-  (apply f args)
+  (try
+    (apply f args)
+    (catch js/Error e
+      (println "Call fn: failed: " {:args args})
+      (js/console.error e)))
   ;; (-> (apply f (bean/->js args))
   ;;     (->js)
   ;;     (bean/->clj))
-  )
+)
 
 (defn eval-result
   [code]

+ 8 - 0
src/main/frontend/fs.cljs

@@ -6,6 +6,14 @@
   (when (and dir js/window.pfs)
     (js/window.pfs.mkdir dir)))
 
+(defn mkdir-if-not-exists
+  [dir]
+  (when (and dir js/window.pfs)
+    (util/p-handle
+     (js/window.pfs.stat dir)
+     (fn [_stat])
+     (fn [_error] (js/window.pfs.mkdir dir)))))
+
 (defn readdir
   [dir]
   (when (and dir js/window.pfs)

+ 0 - 4
src/main/frontend/git.cljs

@@ -182,10 +182,6 @@
              (first commons)
              (find-common-base repo-url local-parent remote-parent local-commits remote-commits))))))))
 
-(defn get-local-diffs
-  [repo-url remote-id local-id]
-  (get-diffs repo-url remote-id local-id))
-
 (defn read-blob
   [repo-url oid path]
   (js/window.workerThread.readBlob (util/get-repo-dir repo-url)

+ 1 - 1
src/main/frontend/handler.cljs

@@ -134,7 +134,7 @@
 (defn set-save-before-unload! []
   (.addEventListener js/window "beforeunload"
                      (fn [e]
-                       (when (state/repos-need-to-be-stored?)
+                       (when (and (not config/dev?) (state/repos-need-to-be-stored?))
                          (let [notification-id (atom nil)]
                            (let [id (notification/show!
                                      [:div

+ 1 - 1
src/main/frontend/handler/dnd.cljs

@@ -511,7 +511,7 @@
           :else
           (move-block-in-different-repos target-block-repo to-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes))
 
-        (when (state/git-auto-push?)
+        (when (state/get-git-auto-push?)
           (doseq [repo (->> #{target-block-repo to-block-repo}
                             (remove nil?))]
             (repo-handler/push repo nil)))))))

+ 1 - 1
src/main/frontend/handler/editor.cljs

@@ -1403,7 +1403,7 @@
                                       file-name)
                          format
                          {:last-pattern (if drop? "" commands/slash)
-                          :restore? false})
+                          :restore? true})
 
         (reset! *image-uploading-process 0))
       (fn [e]

+ 7 - 6
src/main/frontend/handler/page.cljs

@@ -320,13 +320,14 @@
         (notification/show! "Page already exists!" :error)
         (when-let [page (db/entity [:page/name (string/lower-case old-name)])]
           (let [old-original-name (:page/original-name page)
-                file (:page/file page)]
+                file (:page/file page)
+                journal? (:page/journal? page)]
             (d/transact! (db/get-conn repo false)
                          [{:db/id (:db/id page)
                            :page/name (string/lower-case new-name)
                            :page/original-name new-name}])
 
-            (when file
+            (when (and file (not journal?))
               (rename-file! file new-name
                             (fn []
                               (page-add-properties! (string/lower-case new-name) {:title new-name}))))
@@ -357,7 +358,7 @@
 
           (ui-handler/re-render-root!))))))
 
-(defn rename-when-alter-title-propertiy!
+(defn rename-when-alter-title-property!
   [page path format original-content content]
   (when (and page (contains? config/mldoc-support-formats format))
     (let [old-name page
@@ -410,9 +411,9 @@
                        page)
           (let [journal? (date/valid-journal-title? page)
                 ref-file-path (str (get-directory journal?)
-                          "/"
-                          (get-file-name journal? page)
-                          ".org")]
+                                   "/"
+                                   (get-file-name journal? page)
+                                   ".org")]
             (create! page {:redirect? false})
             (util/format "[[file:%s][%s]]"
                          (util/get-relative-path edit-block-file-path ref-file-path)

+ 36 - 64
src/main/frontend/handler/repo.cljs

@@ -11,7 +11,6 @@
             [frontend.date :as date]
             [frontend.config :as config]
             [frontend.format :as format]
-            [goog.object :as gobj]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.git :as git-handler]
             [frontend.handler.file :as file-handler]
@@ -45,37 +44,23 @@
    :error
    false))
 
-(defn journal-file-changed?
-  [repo-url diffs]
-  (contains? (set (map :path diffs))
-             (db/get-current-journal-path)))
-
 (defn create-config-file-if-not-exists
   [repo-url]
   (spec/validate :repos/url repo-url)
   (let [repo-dir (util/get-repo-dir repo-url)
         app-dir config/app-name
         dir (str repo-dir "/" app-dir)]
-    (p/let [_ (-> (fs/mkdir dir)
-                  (p/catch (fn [_e])))]
+    (p/let [_ (fs/mkdir-if-not-exists dir)]
       (let [default-content config/config-default-content]
         (p/let [file-exists? (fs/create-if-not-exists repo-dir (str app-dir "/" config/config-file) default-content)]
           (let [path (str app-dir "/" config/config-file)
                 old-content (when file-exists?
                               (db/get-file repo-url path))
-                content (or
-                         (and old-content
-                              (string/replace old-content "heading" "block"))
-                         default-content)]
+                content (or old-content default-content)]
             (db/reset-file! repo-url path content)
             (db/reset-config! repo-url content)
             (when-not (= content old-content)
               (git-handler/git-add repo-url path))))
-        ;; (p/let [file-exists? (fs/create-if-not-exists repo-dir (str app-dir "/" config/metadata-file) default-content)]
-        ;;   (let [path (str app-dir "/" config/metadata-file)]
-        ;;     (when-not file-exists?
-        ;;       (db/reset-file! repo-url path "{:tx-data []}")
-        ;;       (git-handler/git-add repo-url path))))
 ))))
 
 (defn create-contents-file
@@ -83,13 +68,10 @@
   (spec/validate :repos/url repo-url)
   (let [repo-dir (util/get-repo-dir repo-url)
         format (state/get-preferred-format)
-        path (str "pages/contents." (if (= (name format) "markdown")
-                                      "md"
-                                      (name format)))
+        path (str "pages/contents." (config/get-file-extension format))
         file-path (str "/" path)
         default-content (util/default-content-with-title format "contents")]
-    (p/let [_ (-> (fs/mkdir (str repo-dir "/pages"))
-                  (p/catch (fn [_e])))
+    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/pages"))
             file-exists? (fs/create-if-not-exists repo-dir file-path default-content)]
       (when-not file-exists?
         (db/reset-file! repo-url path default-content)
@@ -102,8 +84,7 @@
         path (str config/app-name "/" config/custom-css-file)
         file-path (str "/" path)
         default-content ""]
-    (p/let [_ (-> (fs/mkdir (str repo-dir "/" config/app-name))
-                  (p/catch (fn [_e])))
+    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
             file-exists? (fs/create-if-not-exists repo-dir file-path default-content)]
       (when-not file-exists?
         (db/reset-file! repo-url path default-content)
@@ -115,8 +96,7 @@
   (let [repo-dir (util/get-repo-dir repo-url)
         path (str (config/get-pages-directory) "/how_to_make_dummy_notes.md")
         file-path (str "/" path)]
-    (p/let [_ (-> (fs/mkdir (str repo-dir "/" (config/get-pages-directory)))
-                  (p/catch (fn [_e])))
+    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-pages-directory)))
             _file-exists? (fs/create-if-not-exists repo-dir file-path content)]
       (db/reset-file! repo-url path content))))
 
@@ -150,8 +130,7 @@
          empty-blocks? (empty? (db/get-page-blocks-no-cache repo-url (string/lower-case title)))]
      (when (or empty-blocks?
                (not page-exists?))
-       (p/let [_ (-> (fs/mkdir (str repo-dir "/" config/default-journals-directory))
-                     (p/catch (fn [_e])))
+       (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/default-journals-directory))
                file-exists? (fs/create-if-not-exists repo-dir file-path content)]
          (when-not file-exists?
            (db/reset-file! repo-url path content)
@@ -161,7 +140,7 @@
 (defn create-default-files!
   [repo-url]
   (spec/validate :repos/url repo-url)
-  (when-let [name (get-in @state/state [:me :name])]
+  (when (state/logged?)
     (create-config-file-if-not-exists repo-url)
     (create-today-journal-if-not-exists repo-url)
     (create-contents-file repo-url)
@@ -250,6 +229,7 @@
   (when (seq files)
     (file-handler/alter-files repo files)))
 
+; FIXME: Unused
 (defn persist-repo-metadata!
   [repo]
   (spec/validate :repos/url repo)
@@ -273,7 +253,7 @@
   [repo-url]
   (p/let [remote-latest-commit (common-handler/get-remote-ref repo-url)
           local-latest-commit (common-handler/get-ref repo-url)]
-    (git/get-local-diffs repo-url local-latest-commit remote-latest-commit)))
+    (git/get-diffs repo-url local-latest-commit remote-latest-commit)))
 
 (defn pull
   [repo-url {:keys [force-pull? show-diff? try-times]
@@ -409,7 +389,7 @@
 (defn push-if-auto-enabled!
   [repo]
   (spec/validate :repos/url repo)
-  (when (state/git-auto-push?)
+  (when (state/get-git-auto-push? repo)
     (push repo nil)))
 
 (defn pull-current-repo
@@ -417,7 +397,7 @@
   (when-let [repo (state/get-current-repo)]
     (pull repo {:force-pull? true})))
 
-(defn clone
+(defn- clone
   [repo-url]
   (spec/validate :repos/url repo-url)
   (p/let [token (helper/get-github-token repo-url)]
@@ -427,10 +407,9 @@
          (state/set-cloning! true)
          (git/clone repo-url token))
        (fn [result]
-         (state/set-git-clone-repo! "")
          (state/set-current-repo! repo-url)
-         (db/start-db-conn! (:me @state/state) repo-url)
-         (db/mark-repo-as-cloned repo-url))
+         (db/start-db-conn! (state/get-me) repo-url)
+         (db/mark-repo-as-cloned! repo-url))
        (fn [e]
          (println "Clone failed, error: ")
          (js/console.error e)
@@ -478,21 +457,20 @@
   []
   (if js/window.pfs
     (let [repo config/local-repo]
-      (p/let [result (-> (fs/mkdir (str "/" repo))
-                         (p/catch (fn [_e] nil)))
-              _ (state/set-current-repo! repo)
-              _ (db/start-db-conn! nil repo)
-              _ (when-not config/publishing?
-                  (let [dummy-notes (get-in dicts/dicts [:en :tutorial/dummy-notes])]
-                    (create-dummy-notes-page repo dummy-notes)))
-              _ (when-not config/publishing?
-                  (let [tutorial (get-in dicts/dicts [:en :tutorial/text])
-                        tutorial (string/replace-first tutorial "$today" (date/today))]
-                    (create-today-journal-if-not-exists repo tutorial)))
-              _ (create-config-file-if-not-exists repo)
-              _ (create-contents-file repo)
-              _ (create-custom-theme repo)]
-        (state/set-db-restoring! false)))
+      (p/do! (fs/mkdir-if-not-exists (str "/" repo))
+             (state/set-current-repo! repo)
+             (db/start-db-conn! nil repo)
+             (when-not config/publishing?
+               (let [dummy-notes (get-in dicts/dicts [:en :tutorial/dummy-notes])]
+                 (create-dummy-notes-page repo dummy-notes)))
+             (when-not config/publishing?
+               (let [tutorial (get-in dicts/dicts [:en :tutorial/text])
+                     tutorial (string/replace-first tutorial "$today" (date/today))]
+                 (create-today-journal-if-not-exists repo tutorial)))
+             (create-config-file-if-not-exists repo)
+             (create-contents-file repo)
+             (create-custom-theme repo)
+             (state/set-db-restoring! false)))
     (js/setTimeout setup-local-repo-if-not-exists! 100)))
 
 (defn periodically-pull
@@ -506,14 +484,8 @@
 
 (defn periodically-push-tasks
   [repo-url]
-  (spec/validate :repos/url repo-url)
-  (let [push (fn []
-               (when (and (not (false? (:git-auto-push (state/get-config repo-url))))
-                          ;; (not config/dev?)
-)
-                 (push repo-url nil)))]
-    (js/setInterval push
-                    (* (config/git-push-secs) 1000))))
+  (js/setInterval #(push-if-auto-enabled! repo-url)
+                  (* (config/git-push-secs) 1000)))
 
 (defn periodically-pull-and-push
   [repo-url {:keys [pull-now?]
@@ -537,12 +509,12 @@
                (println "Something wrong!")
                (js/console.dir error))))
 
-(defn clone-and-pull
+(defn- clone-and-pull
   [repo-url]
   (spec/validate :repos/url repo-url)
   (->
    (p/let [_ (clone repo-url)
-           _ (git-handler/git-set-username-email! repo-url (:me @state/state))]
+           _ (git-handler/git-set-username-email! repo-url (state/get-me))]
      (load-db-and-journals! repo-url nil true)
      (periodically-pull-and-push repo-url {:pull-now? false})
      ;; (periodically-persist-app-metadata repo-url)
@@ -576,10 +548,10 @@
   (spec/validate :repos/repo repo)
   (db/remove-conn! url)
   (db/clear-query-state!)
-  (-> (p/let [_ (db/remove-db! url)
-              _ (db/remove-files-db! url)
-              _ (fs/rmdir (util/get-repo-dir url))]
-        (clone-and-pull url))
+  (-> (p/do! (db/remove-db! url)
+             (db/remove-files-db! url)
+             (fs/rmdir (util/get-repo-dir url))
+             (clone-and-pull url))
       (p/catch (fn [error]
                  (prn "Delete repo failed, error: " error)))))
 

+ 4 - 21
src/main/frontend/handler/ui.cljs

@@ -8,27 +8,10 @@
             [frontend.util :as util :refer-macros [profile]]))
 
 ;; sidebars
-(defn hide-left-sidebar
+(defn close-left-sidebar!
   []
-  (dom/add-class! (dom/by-id "menu")
-                  "md:block")
-  (dom/remove-class! (dom/by-id "left-sidebar")
-                     "enter")
-  (dom/remove-class! (dom/by-id "search")
-                     "sidebar-open")
-  (dom/remove-class! (dom/by-id "main")
-                     "sidebar-open"))
-
-(defn show-left-sidebar
-  []
-  (dom/remove-class! (dom/by-id "menu")
-                     "md:block")
-  (dom/add-class! (dom/by-id "left-sidebar")
-                  "enter")
-  (dom/add-class! (dom/by-id "search")
-                  "sidebar-open")
-  (dom/add-class! (dom/by-id "main")
-                  "sidebar-open"))
+  (when-let [elem (gdom/getElement "close-left-bar")]
+    (.click elem)))
 
 (defn hide-right-sidebar
   []
@@ -92,5 +75,5 @@
                     (state/get-custom-css-link)
                     (db/get-custom-css)
                     ;; (state/get-custom-css-link)
-                    )]
+)]
     (util/add-style! style)))

+ 1 - 2
src/main/frontend/helper.cljs

@@ -36,9 +36,8 @@
       (if (and (map? token-state)
                (string? expires_at))
         (let [expires-at (tf/parse (tf/formatters :date-time-no-ms) expires_at)
-              request-time-gap (t/minutes 1)
               now (t/now)
-              expired? (t/after? now (t/minus expires-at request-time-gap))]
+              expired? (t/after? now expires-at)]
           {:exist? true
            :expired? expired?
            :token token})

+ 13 - 8
src/main/frontend/mixins.cljs

@@ -102,14 +102,19 @@
                 (when all-handler (all-handler e key-code)))))))
 
 (defn on-key-down
-  [state keycode-map all-handler]
-  (let [node (rum/dom-node state)]
-    (listen state js/window "keydown"
-            (fn [e]
-              (let [key-code (.-keyCode e)]
-                (when-let [f (get keycode-map key-code)]
-                  (f state e))
-                (when all-handler (all-handler e key-code)))))))
+  ([state keycode-map]
+   (on-key-down state keycode-map {}))
+  ([state keycode-map {:keys [not-matched-handler all-handler]}]
+   (let [node (rum/dom-node state)]
+     (listen state js/window "keydown"
+             (fn [e]
+               (let [key-code (.-keyCode e)]
+                 (if-let [f (get keycode-map key-code)]
+                   (f state e)
+                   (when (and not-matched-handler (fn? not-matched-handler))
+                     (not-matched-handler e key-code)))
+                 (when (and all-handler (fn? all-handler))
+                   (all-handler e key-code))))))))
 
 (defn event-mixin
   ([attach-listeners]

+ 24 - 15
src/main/frontend/state.cljs

@@ -8,7 +8,9 @@
             [goog.dom :as gdom]
             [dommy.core :as dom]
             [cljs.core.async :as async]
-            [lambdaisland.glogi :as log]))
+            [lambdaisland.glogi :as log]
+            [cljs-time.core :as t]
+            [cljs-time.format :as tf]))
 
 (defonce ^:private state
   (atom
@@ -32,7 +34,6 @@
     ;; repo -> {:last-stored-at :last-modified-at}
     :repo/persist-status {}
     :me nil
-    :git/clone-repo (or (storage/get :git/clone-repo) "")
     :git/current-repo (storage/get :git/current-repo)
     :git/status {}
     :format/loading {}
@@ -286,7 +287,7 @@
   [input-id value]
   (when input-id
     (when-let [input (gdom/getElement input-id)]
-      (gobj/set input "value" value))
+      (util/set-change-value input value))
     (update-state! :editor/content (fn [m]
                                      (assoc m input-id value)))
     ;; followers
@@ -495,11 +496,6 @@
          :custom-context-menu/show? false
          :custom-context-menu/links nil))
 
-(defn set-git-clone-repo!
-  [repo]
-  (set-state! :git/clone-repo repo)
-  (storage/set :git/clone-repo repo))
-
 (defn set-github-token!
   [repo token-result]
   (when token-result
@@ -519,9 +515,15 @@
       (when (seq repos)
         (let [set-token-f
               (fn [{:keys [installation_id] :as repo}]
-                (let [{:keys [token expires_at] :as m} (get tokens installation_id)]
-                  (if (and token expires_at)
-                    (merge repo {:token token :expires_at expires_at})
+                (let [{:keys [token] :as m} (get tokens installation_id)]
+                  (if (string? token)
+                    ;; Github API returns a expires_at key which is a timestamp (expires after 60 minutes at present),
+                    ;; however, user's system time may be inaccurate. Here, based on the client system time, we use
+                    ;; 40-minutes interval to deal with some critical conditions, for e.g. http request time consume.
+                    (let [formatter (tf/formatters :date-time-no-ms)
+                          expires-at (->> (t/plus (t/now) (t/minutes 40))
+                                          (tf/unparse formatter))]
+                      (merge repo {:token token :expires_at expires-at}))
                     (do (log/error :token/cannot-set-token {:repo-m repo :token-m m}) repo))))
               repos (mapv set-token-f repos)]
           (swap! state assoc-in [:me :repos] repos))))))
@@ -712,9 +714,14 @@
   []
   (:me @state))
 
+(defn get-name
+  []
+  (:name (get-me)))
+
 (defn logged?
+  "Whether the user has logged in."
   []
-  (some? (:name (get-me))))
+  (some? (get-name)))
 
 (defn set-draw!
   [value]
@@ -844,9 +851,11 @@
     (let [shortcuts (or (:shortcuts value) {})]
       (storage/set (str repo-url "-shortcuts") shortcuts))))
 
-(defn git-auto-push?
-  []
-  (true? (:git-auto-push (get-config (get-current-repo)))))
+(defn get-git-auto-push?
+  ([]
+   (get-git-auto-push? (get-current-repo)))
+  ([repo]
+   (true? (:git-auto-push (get-config repo)))))
 
 (defn set-changed-files!
   [repo changed-files]

+ 25 - 18
src/main/frontend/ui.cljs

@@ -17,6 +17,22 @@
 (defonce transition-group (r/adapt-class TransitionGroup))
 (defonce css-transition (r/adapt-class CSSTransition))
 (defonce textarea (r/adapt-class (gobj/get TextareaAutosize "default")))
+
+(rum/defc ls-textarea [{:keys [on-change] :as -props}]
+  (let [composition? (atom false)
+        set-composition? #(reset! composition? %)
+        on-composition (fn [e]
+                         (case e.type
+                           "compositionend" (do (set-composition? false) (on-change e))
+                           (set-composition? true)))
+        props (assoc -props
+                     :on-change (fn [e] (when-not @composition?
+                                          (on-change e)))
+                     :on-composition-start on-composition
+                     :on-composition-update on-composition
+                     :on-composition-end on-composition)]
+    (textarea props)))
+
 (rum/defc dropdown-content-wrapper [state content class]
   (let [class (or class
                   (util/hiccup->class "origin-top-right.absolute.right-0.mt-2.rounded-md.shadow-lg"))]
@@ -292,7 +308,7 @@
                 :else
                 nil)
               (when-let [element (gdom/getElement (str "ac-" @current-idx))]
-                (let [ac-inner (gdom/getElement "ac-inner")
+                (let [ac-inner (gdom/getElement "ui__ac-inner")
                       element-top (gobj/get element "offsetTop")
                       scroll-top (- (gobj/get element "offsetTop") 360)]
                   (set! (.-scrollTop ac-inner) scroll-top)))))
@@ -306,7 +322,7 @@
                   (reset! current-idx 0)
                   (swap! current-idx inc)))
               (when-let [element (gdom/getElement (str "ac-" @current-idx))]
-                (let [ac-inner (gdom/getElement "ac-inner")
+                (let [ac-inner (gdom/getElement "ui__ac-inner")
                       element-top (gobj/get element "offsetTop")
                       scroll-top (- (gobj/get element "offsetTop") 360)]
                   (set! (.-scrollTop ac-inner) scroll-top)))))
@@ -320,8 +336,7 @@
                          (> (count matched)
                             @current-idx))
                   (on-chosen (nth matched @current-idx) false)
-                  (and on-enter (on-enter state))))))}
-      nil)))
+                  (and on-enter (on-enter state))))))})))
   [state matched {:keys [on-chosen
                          on-shift-chosen
                          on-enter
@@ -329,24 +344,16 @@
                          item-render
                          class]}]
   (let [current-idx (get state ::current-idx)]
-    [:div.rounded-md.shadow-xs.bg-base-3 {:class class}
+    [:div#ui__ac {:class class}
      (if (seq matched)
-       [:div#ac-inner
-        {:style {:max-height 400
-                 :overflow "hidden"
-                 :overflow-x "hidden"
-                 :overflow-y "auto"
-                 :position "relative"
-                 "-webkit-overflow-scrolling" "touch"}}
+       [:div#ui__ac-inner
         (for [[idx item] (medley/indexed matched)]
           (rum/with-key
             (menu-link
-             {:id (str "ac-" idx)
-              :style {:padding-top 6
-                      :padding-bottom 6}
-              :class (when (= @current-idx idx)
-                       "chosen")
-              ;; :tab-index -1
+             {:id       (str "ac-" idx)
+              :class    (when (= @current-idx idx)
+                          "chosen")
+                ;; :tab-index -1
               :on-click (fn [e]
                           (util/stop e)
                           (if (and (gobj/get e "shiftKey") on-shift-chosen)

+ 17 - 17
src/main/frontend/ui/date_picker.cljs

@@ -183,30 +183,30 @@
    (fn [state]
      (let [{:keys [on-change on-switch deadline-or-schedule?]} (last (:rum/args state))]
        (mixins/on-key-down
-       state
-       {;; enter, current day
-        13 (fn [state e]
-             (when on-change
-               (when-not deadline-or-schedule?
-                 (on-change e @*internal-model))))
+        state
+        {;; enter, current day
+         13 (fn [state e]
+              (when on-change
+                (when-not deadline-or-schedule?
+                  (on-change e @*internal-model))))
 
         ;; left, previous day
-        37 (fn [state e]
-             (swap! *internal-model inc-date -1))
+         37 (fn [state e]
+              (swap! *internal-model inc-date -1))
 
         ;; right, next day
-        39 (fn [state e]
-             (swap! *internal-model inc-date 1))
+         39 (fn [state e]
+              (swap! *internal-model inc-date 1))
 
         ;; up, one week ago
-        38 (fn [state e]
-             (swap! *internal-model inc-week -1))
+         38 (fn [state e]
+              (swap! *internal-model inc-week -1))
         ;; down, next week
-        40 (fn [state e]
-             (swap! *internal-model inc-week 1))}
-       (fn [e key-code]
-         (when (contains? #{13 37 38 39 40} key-code)
-           (util/stop e)))))))
+         40 (fn [state e]
+              (swap! *internal-model inc-week 1))}
+        {:all-handler (fn [e key-code]
+                        (when (contains? #{13 37 38 39 40} key-code)
+                          (util/stop e)))}))))
   {:init (fn [state]
            (reset! *internal-model (first (:rum/args state)))
            state)}

+ 9 - 4
src/main/frontend/util.cljs

@@ -38,6 +38,11 @@
   [event]
   (gobj/getValueByKeys event "target" "value"))
 
+(defn set-change-value
+  "compatible change event for React"
+  [node value]
+  (utils/triggerInputChange node value))
+
 (defn p-handle
   ([p ok-handler]
    (p-handle p ok-handler (fn [error]
@@ -253,7 +258,7 @@
   (try
     (bean/->clj ((gobj/get caret-pos "position") input))
     (catch js/Error e
-      nil)))
+      (js/console.error e))))
 
 (defn minimize-html
   [s]
@@ -434,12 +439,12 @@
 
 (defn textarea-cursor-first-row?
   [input line-height]
-  (< (:top (get-caret-pos input)) line-height))
+  (<= (:top (get-caret-pos input)) line-height))
 
 (defn textarea-cursor-end-row?
   [input line-height]
-  (> (+ (:top (get-caret-pos input)) line-height)
-     (get-textarea-height input)))
+  (>= (+ (:top (get-caret-pos input)) line-height)
+      (get-textarea-height input)))
 
 (defn safe-split-first [pattern s]
   (if-let [first-index (string/index-of s pattern)]

+ 57 - 34
src/main/frontend/utils.js

@@ -1,74 +1,97 @@
+if (typeof window === 'undefined') {
+  global.window = {}
+}
+
 // Copy from https://github.com/primetwig/react-nestable/blob/dacea9dc191399a3520f5dc7623f5edebc83e7b7/dist/utils.js
-export var closest = function closest(target, selector) {
+export var closest = function closest (target, selector) {
   // closest(e.target, '.field')
   while (target) {
-    if (target.matches && target.matches(selector)) return target;
-    target = target.parentNode;
+    if (target.matches && target.matches(selector)) return target
+    target = target.parentNode
   }
-  return null;
-};
+  return null
+}
 
-export var getOffsetRect = function getOffsetRect(elem) {
+export var getOffsetRect = function getOffsetRect (elem) {
   // (1)
-  var box = elem.getBoundingClientRect();
+  var box = elem.getBoundingClientRect()
 
-  var body = document.body;
-  var docElem = document.documentElement;
+  var body = document.body
+  var docElem = document.documentElement
 
   // (2)
-  var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
-  var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
+  var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop
+  var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft
 
   // (3)
-  var clientTop = docElem.clientTop || body.clientTop || 0;
-  var clientLeft = docElem.clientLeft || body.clientLeft || 0;
+  var clientTop = docElem.clientTop || body.clientTop || 0
+  var clientLeft = docElem.clientLeft || body.clientLeft || 0
 
   // (4)
-  var top = box.top + scrollTop - clientTop;
-  var left = box.left + scrollLeft - clientLeft;
+  var top = box.top + scrollTop - clientTop
+  var left = box.left + scrollLeft - clientLeft
 
-  return { top: Math.round(top), left: Math.round(left) };
-};
+  return { top: Math.round(top), left: Math.round(left) }
+}
 
 // jquery focus
-export var focus = function( elem ) {
+export var focus = function (elem) {
   return elem === document.activeElement &&
     document.hasFocus() &&
-    !!( elem.type || elem.href || ~elem.tabIndex );
+    !!(elem.type || elem.href || ~elem.tabIndex)
 }
 
 // copied from https://stackoverflow.com/a/32180863
 export var timeConversion = function (millisec) {
-  var seconds = (millisec / 1000).toFixed(0);
-  var minutes = (millisec / (1000 * 60)).toFixed(0);
-  var hours = (millisec / (1000 * 60 * 60)).toFixed(1);
-  var days = (millisec / (1000 * 60 * 60 * 24)).toFixed(1);
+  var seconds = (millisec / 1000).toFixed(0)
+  var minutes = (millisec / (1000 * 60)).toFixed(0)
+  var hours = (millisec / (1000 * 60 * 60)).toFixed(1)
+  var days = (millisec / (1000 * 60 * 60 * 24)).toFixed(1)
 
   if (seconds < 60) {
-    return seconds + "s";
+    return seconds + 's'
   } else if (minutes < 60) {
-    return minutes + "m";
+    return minutes + 'm'
   } else if (hours < 24) {
-    return hours + "h";
+    return hours + 'h'
   } else {
-    return days + "d"
+    return days + 'd'
   }
 }
 
-export var getSelectionText = function() {
-  const selection = (window.getSelection() || '').toString().trim();
+export var getSelectionText = function () {
+  const selection = (window.getSelection() || '').toString().trim()
   if (selection) {
-    return selection;
+    return selection
   }
 
   // Firefox fix
-  const activeElement = window.document.activeElement;
+  const activeElement = window.document.activeElement
   if (activeElement) {
     if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') {
-      const el = activeElement;
-      return el.value.slice(el.selectionStart || 0, el.selectionEnd || 0);
+      const el = activeElement
+      return el.value.slice(el.selectionStart || 0, el.selectionEnd || 0)
     }
   }
 
-  return '';
+  return ''
+}
+
+const inputTypes = [
+  window.HTMLInputElement,
+  window.HTMLSelectElement,
+  window.HTMLTextAreaElement,
+]
+
+export const triggerInputChange = (node, value = '', name = 'change') => {
+
+  // only process the change on elements we know have a value setter in their constructor
+  if (inputTypes.indexOf(node.__proto__.constructor) > -1) {
+
+    const setValue = Object.getOwnPropertyDescriptor(node.__proto__, 'value').set
+    const event = new Event('change', { bubbles: true })
+
+    setValue.call(node, value)
+    node.dispatchEvent(event)
+  }
 }