Browse Source

feat: support delete image link of block

charlie 4 years ago
parent
commit
ee529b5b9d

+ 0 - 5
resources/css/common.css

@@ -578,11 +578,6 @@ h1.title {
   padding: -1px;
 }
 
-.content img {
-  margin-top: 0.5rem;
-  margin-bottom: 0.5rem;
-}
-
 span.timestamp {
   margin: 0 0.25rem;
 }

+ 46 - 9
src/main/frontend/components/block.cljs

@@ -156,6 +156,41 @@
                 parts (remove #(string/blank? %) parts)]
             (string/join "/" (reverse parts))))))))
 
+(rum/defc asset-container
+  [text child]
+  (rum/with-context [[t] i18n/*tongue-context*]
+    (let [get-block-id #(and % (.getAttribute (.closest % "[blockid]") "blockid"))
+          repo (state/get-current-repo)
+          local-repo? (config/local-db? repo)
+          ctl-handlers {:delete
+                        (fn [e]
+                          (let [target (.-target e)
+                                block-id (get-block-id target)
+                                confirm-fn (ui/make-confirm-modal
+                                            {:title         (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image)))
+                                             :sub-title     :asset/physical-delete
+                                             :sub-checkbox? local-repo?
+                                             :on-confirm    (fn [e {:keys [close-fn sub-selected]}]
+                                                              (close-fn)
+                                                              (editor-handler/delete-asset-of-block!
+                                                               {:block-id block-id
+                                                                :force-local (and sub-selected (get sub-selected 0))
+                                                                :repo repo
+                                                                :href text}))})]
+                            (state/set-modal! confirm-fn)
+                            (util/stop e)))}]
+      [:div.asset-container
+       {:on-click (fn [e]
+                    (let [target (.-target e)]
+                      (some (fn [k]
+                              (let [selector (str "." (symbol k))
+                                    el (.closest target selector)]
+                                (when el
+                                  (apply (k ctl-handlers) [e])
+                                  true))) [:delete])))}
+       [[:span.ctl [:a.delete {:title "delete"} svg/trash-sm]]
+        child]])))
+
 (rum/defcs asset-link < rum/reactive
   (rum/local nil ::src)
   [state href label]
@@ -167,10 +202,11 @@
       (p/then (editor-handler/make-asset-url href) #(reset! src %)))
 
     (when @src
-      [:img
-       {:loading "lazy"
-        :src     @src
-        :title   title}])))
+      (asset-container href
+                       [:img
+                        {:loading "lazy"
+                         :src     @src
+                         :title   title}]))))
 
 ;; TODO: safe encoding asciis
 ;; TODO: image link to another link
@@ -181,11 +217,12 @@
     (let [href (if (util/starts-with? href "http")
                  href
                  (get-file-absolute-path config href))]
-      [:img.rounded-sm.shadow-xl
-       {:loading "lazy"
-        ;; :on-error (fn [])
-        :src     href
-        :title   (second (first label))}])))
+      (asset-container href
+                       [:img.rounded-sm.shadow-xl
+                        {:loading "lazy"
+          ;; :on-error (fn [])
+                         :src     href
+                         :title   (second (first label))}]))))
 
 (defn repetition-to-string
   [[[kind] [duration] n]]

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

@@ -25,6 +25,51 @@
       width: 9px;
     }
   }
+
+  .asset-container {
+    display: inline-block;
+    position: relative;
+    margin-top: .5rem;
+    margin-bottom: .5rem;
+
+    .ctl {
+      position: absolute;
+      top: 0;
+      right: 0;
+      padding: 5px;
+      display: none;
+
+      > a {
+        padding: 3px;
+        border-radius: 4px;
+        opacity: .4;
+        user-select: none;
+
+        &.delete {
+          svg {
+            color: red;
+
+            opacity: .6;
+            font-weight: normal;
+          }
+        }
+
+        &:hover {
+          opacity: .8;
+        }
+
+        &:active {
+          opacity: 1;
+        }
+      }
+    }
+
+    &:hover {
+      .ctl {
+        display: flex;
+      }
+    }
+  }
 }
 
 .open-block-ref-link {

+ 4 - 0
src/main/frontend/db/model.cljs

@@ -272,6 +272,10 @@
   [id]
   (db-utils/entity [:block/uuid (if (uuid? id) id (uuid id))]))
 
+(defn query-block-by-uuid
+  [id]
+  (db-utils/pull [:block/uuid (if (uuid? id) id (uuid id))]))
+
 (defn get-page-format
   [page-name]
   (when-let [file (:page/file (db-utils/entity [:page/name page-name]))]

+ 6 - 0
src/main/frontend/dicts.cljs

@@ -266,6 +266,9 @@ title: How to take dummy notes?
         :draw/delete "Delete"
         :draw/more-options "More options"
         :draw/back-to-logseq "Back to logseq"
+        :text/image "Image"
+        :asset/confirm-delete "Are you sure you want to delete this {1}?"
+        :asset/physical-delete "force remove physical file"
         :content/copy "Copy"
         :content/cut "Cut"
         :content/make-todos "Make {1}s"
@@ -619,6 +622,9 @@ title: How to take dummy notes?
            :help/create-new-block "创建块"
            :help/new-line-in-block "块中新建行"
            :help/select-nfs-browser "请选择支持nfs的浏览来使用logseq本地文件夹功能, 如最新的Chrome浏览器."
+           :text/image "图片"
+           :asset/confirm-delete "确定要删除{1}吗?"
+           :asset/physical-delete "同时删除本地文件"
            :undo "撤销"
            :redo "重做"
            :help/zoom-in "聚焦"

+ 23 - 3
src/main/frontend/handler/editor.cljs

@@ -1,6 +1,7 @@
 (ns frontend.handler.editor
   (:require [frontend.state :as state]
             [frontend.db.model :as db-model]
+            [frontend.db.utils :as db-utils]
             [frontend.handler.common :as common-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.git :as git-handler]
@@ -1419,8 +1420,10 @@
                              opts))))
 
 (defn save-block!
-  ([repo uuid content]
-   (let [block (db-model/get-block-by-uuid uuid)
+  ([repo block-or-uuid content]
+   (let [block (if (or (uuid? block-or-uuid)
+                       (string? block-or-uuid))
+                 (db-model/query-block-by-uuid block-or-uuid) block-or-uuid)
          format (:block/format block)]
      (save-block! {:block block :repo repo :format format} content)))
   ([{:keys [format block repo dummy?] :as state} value]
@@ -1556,7 +1559,7 @@
         (p/then (fs/write-file repo dir filename (.stream file))
                 #(p/resolved [filename file])))))))
 
-(def *assets-url-cache (atom {}))
+(defonce *assets-url-cache (atom {}))
 
 (defn make-asset-url
   [path]                                                    ;; path start with "/assets" or compatible for "../assets"
@@ -1573,6 +1576,23 @@
             (swap! *assets-url-cache assoc (keyword handle-path) url)
             url))))))
 
+(defn- replace-asset-link-with-href
+  [format content href replacement]
+  (let [right-part-holder "&§&"]
+    (and content
+         (-> content                                        ;; FIXME: match strategy
+             (.replace (str "](" href ")") right-part-holder)
+             (.replace (js/RegExp. (str "!\\[[^\\]]*" right-part-holder)) replacement)))))
+
+(defn delete-asset-of-block!
+  [{:keys [repo href block-id force-local] :as opts}]
+  (let [block (db-model/query-block-by-uuid block-id)
+        _ (or block (throw (str block-id " not exists")))
+        format (:block/format block)
+        text (:block/content block)
+        content (replace-asset-link-with-href format text href "")]
+    (save-block! repo block content)))
+
 (defn upload-image
   [id files format uploading? drop-or-paste?]
   (let [repo (state/get-current-repo)

+ 79 - 32
src/main/frontend/ui.cljs

@@ -12,7 +12,8 @@
             [goog.object :as gobj]
             [goog.dom :as gdom]
             [medley.core :as medley]
-            [frontend.ui.date-picker]))
+            [frontend.ui.date-picker]
+            [frontend.context.i18n :as i18n]))
 
 (defonce transition-group (r/adapt-class TransitionGroup))
 (defonce css-transition (r/adapt-class CSSTransition))
@@ -59,7 +60,7 @@
        :or   {z-index 999}
        :as   opts}]]
   (let [{:keys [open? toggle-fn]} state
-        modal-content             (modal-content-fn state)]
+        modal-content (modal-content-fn state)]
     [:div.ml-1.relative {:style {:z-index z-index}}
      (content-fn state)
      (css-transition
@@ -91,8 +92,8 @@
               child [:div
                      {:style {:display "flex" :flex-direction "row"}}
                      [:div {:style {:margin-right "8px"}} title]
-                     ;; [:div {:style {:position "absolute" :right "8px"}}
-                     ;;  icon]
+                      ;; [:div {:style {:position "absolute" :right "8px"}}
+                      ;;  icon]
 ]]
           (rum/with-key
             (menu-link new-options child)
@@ -101,18 +102,18 @@
 
 (defn button
   [text & {:keys [background on-click href]
-           :as option}]
+           :as   option}]
   (let [class "inline-flex.items-center.px-3.py-2.border.border-transparent.text-sm.leading-4.font-medium.rounded-md.text-white.bg-indigo-600.hover:bg-indigo-700.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.active:bg-indigo-700.transition.ease-in-out.duration-150.mt-1"
         class (if background (string/replace class "indigo" background) class)]
     (if href
       [:a.button (merge
-                  {:type "button"
+                  {:type  "button"
                    :class (util/hiccup->class class)}
                   (dissoc option :background))
        text]
       [:button
        (merge
-        {:type "button"
+        {:type  "button"
          :class (util/hiccup->class class)}
         (dissoc option :background))
        text])))
@@ -127,19 +128,19 @@
              [:svg.h-6.w-6.text-green-400
               {:stroke "currentColor", :viewBox "0 0 24 24", :fill "none"}
               [:path
-               {:d "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
-                :stroke-width "2"
+               {:d               "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
+                :stroke-width    "2"
                 :stroke-linejoin "round"
-                :stroke-linecap "round"}]]]
+                :stroke-linecap  "round"}]]]
             :warning
             ["text-gray-900"
              [:svg.h-6.w-6.text-yellow-500
               {:stroke "currentColor", :viewBox "0 0 24 24", :fill "none"}
               [:path
-               {:d "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
-                :stroke-width "2"
+               {:d               "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+                :stroke-width    "2"
                 :stroke-linejoin "round"
-                :stroke-linecap "round"}]]]
+                :stroke-linecap  "round"}]]]
 
             ["text-red-500"
              [:svg.h-6.w-6.text-red-500
@@ -154,7 +155,7 @@
                                  (= state "exited"))
                            -1
                            99)
-                :top "3.2em"}}
+                :top     "3.2em"}}
        [:div.max-w-sm.w-full.shadow-lg.rounded-lg.pointer-events-auto.notification-area
         {:class (case state
                   "entering" "transition ease-out duration-300 transform opacity-0 translate-y-2 sm:translate-x-0"
@@ -192,7 +193,7 @@
                          v (second el)]
                      (css-transition
                       {:timeout 100
-                       :key (name k)}
+                       :key     (name k)}
                       (fn [state]
                         (notification-content state (:content v) (:status v) k)))))
                  contents)))))
@@ -297,7 +298,7 @@
   (mixins/event-mixin attach-listeners)
   "Render an infinite list."
   [state body {:keys [on-load on-top-reached]
-               :as opts}]
+               :as   opts}]
   body)
 
 (rum/defcs auto-complete <
@@ -324,7 +325,7 @@
                       element-top (gobj/get element "offsetTop")
                       scroll-top (- (gobj/get element "offsetTop") 360)]
                   (set! (.-scrollTop ac-inner) scroll-top)))))
-       ;; down
+         ;; down
        40 (fn [state e]
             (let [current-idx (get state ::current-idx)
                   matched (first (:rum/args state))]
@@ -339,7 +340,7 @@
                       scroll-top (- (gobj/get element "offsetTop") 360)]
                   (set! (.-scrollTop ac-inner) scroll-top)))))
 
-       ;; enter
+         ;; enter
        13 (fn [state e]
             (util/stop e)
             (let [[matched {:keys [on-chosen on-enter]}] (:rum/args state)]
@@ -365,7 +366,7 @@
              {:id       (str "ac-" idx)
               :class    (when (= @current-idx idx)
                           "chosen")
-                ;; :tab-index -1
+               ;; :tab-index -1
               :on-click (fn [e]
                           (.preventDefault e)
                           (if (and (gobj/get e "shiftKey") on-shift-chosen)
@@ -383,9 +384,9 @@
   [:a {:on-click on-click}
    [:span.relative.inline-block.flex-shrink-0.h-6.w-11.border-2.border-transparent.rounded-full.cursor-pointer.transition-colors.ease-in-out.duration-200.focus:outline-none.focus:shadow-outline
     {:aria-checked "false", :tab-index "0", :role "checkbox"
-     :class (if on? "bg-indigo-600" "bg-gray-200")}
+     :class        (if on? "bg-indigo-600" "bg-gray-200")}
     [:span.inline-block.h-5.w-5.rounded-full.bg-white.shadow.transform.transition.ease-in-out.duration-200
-     {:class (if on? "translate-x-5" "translate-x-0")
+     {:class       (if on? "translate-x-5" "translate-x-0")
       :aria-hidden "true"}]]])
 
 (defn tooltip
@@ -422,15 +423,15 @@
    [:div.absolute.top-0.right-0.pt-4.pr-4
     [:button.text-gray-400.hover:text-gray-500.focus:outline-none.focus:text-gray-500.transition.ease-in-out.duration-150
      {:aria-label "Close"
-      :type "button"
-      :on-click close-fn}
+      :type       "button"
+      :on-click   close-fn}
      [:svg.h-6.w-6
       {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
       [:path
-       {:d "M6 18L18 6M6 6l12 12"
-        :stroke-width "2"
+       {:d               "M6 18L18 6M6 6l12 12"
+        :stroke-width    "2"
         :stroke-linejoin "round"
-        :stroke-linecap "round"}]]]]
+        :stroke-linecap  "round"}]]]]
 
    (panel-content close-fn)])
 
@@ -451,6 +452,52 @@
       (fn [state]
         (modal-panel modal-panel-content state close-fn)))]))
 
+(defn make-confirm-modal
+  [{:keys [tag title sub-title sub-checkbox? on-cancel on-confirm] :as opts}]
+  (fn [close-fn]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      (let [*sub-checkbox-selected (and sub-checkbox? (atom []))]
+        [:div.ui__confirm-modal
+         {:class (str "is-" tag)}
+         [:div.sm:flex.sm:items-start
+          [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-red-100.sm:mx-0.sm:h-10.sm:w-10
+           [:svg.h-6.w-6.text-red-600
+            {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
+            [:path
+             {:d
+              "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
+              :stroke-width    "2"
+              :stroke-linejoin "round"
+              :stroke-linecap  "round"}]]]
+          [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
+           [:h2.headline.text-lg.leading-6.font-medium.text-gray-900
+            (if (keyword? title) (t title) title)]
+           [:label.sublabel
+            (when sub-checkbox?
+              (checkbox
+               {:default-value false
+                :on-change     (fn [e]
+                                 (let [checked (.. e -target -checked)]
+                                   (reset! *sub-checkbox-selected [checked])))}))
+            [:h3.subline.text-gray-400
+             (if (keyword? sub-title)
+               (t sub-title)
+               sub-title)]]]]
+
+         [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
+          [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
+           [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+            {:type     "button"
+             :on-click #(and (fn? on-confirm)
+                             (on-confirm % {:close-fn close-fn
+                                            :sub-selected (and *sub-checkbox-selected @*sub-checkbox-selected)}))}
+            (t :yes)]]
+          [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
+           [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+            {:type     "button"
+             :on-click (comp on-cancel close-fn)}
+            (t :cancel)]]]]))))
+
 (defn loading
   [content]
   [:div.flex.flex-row.items-center
@@ -471,12 +518,12 @@
     [:div.flex.flex-col
      [:div.content
       [:div.flex-1.flex-row.foldable-title {:on-mouse-over #(reset! control? true)
-                                            :on-mouse-out #(reset! control? false)}
+                                            :on-mouse-out  #(reset! control? false)}
        [:div.flex.flex-row.items-center
         [:a.block-control.opacity-50.hover:opacity-100.mr-2
-         {:style {:width 14
-                  :height 16
-                  :margin-left -24}
+         {:style    {:width       14
+                     :height      16
+                     :margin-left -24}
           :on-click (fn [e]
                       (util/stop e)
                       (swap! collapsed? not))}
@@ -534,13 +581,13 @@
 (rum/defc select
   [options on-change]
   [:select.mt-1.form-select.block.w-full.px-3.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5.ml-4
-   {:style {:padding "0 0 0 12px"}
+   {:style     {:padding "0 0 0 12px"}
     :on-change (fn [e]
                  (let [value (util/evalue e)]
                    (on-change value)))}
    (for [{:keys [label value selected]} options]
      [:option (cond->
-               {:key label
+               {:key   label
                 :value (or value label)}
                 selected
                 (assoc :selected selected))

+ 13 - 0
src/main/frontend/ui.css

@@ -30,6 +30,19 @@
   }
 }
 
+.ui__confirm-modal {
+  .sublabel {
+    display: flex;
+    padding: 2px 0;
+    align-items: center;
+    font-size: 14px;
+
+    input[type=checkbox] {
+      margin-right: 4px;
+    }
+  }
+}
+
 .dropdown-wrapper {
   background-color: var(--ls-primary-background-color, #fff);
   min-width: 12rem;