diff --git a/resources/css/common.css b/resources/css/common.css index cbf07d2364..4baaf9ad5d 100644 --- a/resources/css/common.css +++ b/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; } diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 2009a23680..62b88d80f3 100644 --- a/src/main/frontend/components/block.cljs +++ b/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]] diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index d0b74d9eec..2e5734444d 100644 --- a/src/main/frontend/components/block.css +++ b/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 { diff --git a/src/main/frontend/db/model.cljs b/src/main/frontend/db/model.cljs index 390a86995a..d1311dde84 100644 --- a/src/main/frontend/db/model.cljs +++ b/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]))] diff --git a/src/main/frontend/dicts.cljs b/src/main/frontend/dicts.cljs index 52c4bab279..1701a547b3 100644 --- a/src/main/frontend/dicts.cljs +++ b/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 "聚焦" diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index 26c12d76da..726a2f5f97 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/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) diff --git a/src/main/frontend/ui.cljs b/src/main/frontend/ui.cljs index 022399b67c..15f194a4e3 100644 --- a/src/main/frontend/ui.cljs +++ b/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)) diff --git a/src/main/frontend/ui.css b/src/main/frontend/ui.css index fa5acb8e5c..e6c59702e6 100644 --- a/src/main/frontend/ui.css +++ b/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;