diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a876a0c07e..0cf4fd42c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -179,7 +179,7 @@ jobs: run: xvfb-run -- yarn e2e-test env: CI: true - DEBUG: "pw:test" + DEBUG: "pw:api" - name: Save test artifacts if: ${{ failure() }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b438fa031d..dab84a14a1 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -13,4 +13,4 @@ This Code of Conduct applies within all community spaces, and also applies when - Please respect each other. Do not dismiss, abuse, harass, attack, insult, or discriminate against others. - Likewise, any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome. - Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer. -- If you feel being harassed or made uncomfortable by a community member, please report the incident(s) either by contacting the moderators on the [Discord](https://discord.gg/KpN4eHY) channel, Tienson Qin (@tiensonqin) on GitHub, or the official [Twitter](https://twitter.com/logseq). We will work with you to resolve the issue promptly. +- If you feel being harassed or made uncomfortable by a community member, please report the incident(s) either by contacting the moderators on the [Forum](https://discuss.logseq.com), [Discord](https://discord.gg/KpN4eHY) channel, Tienson Qin (@tiensonqin) on GitHub, or the official [Twitter](https://twitter.com/logseq). We will work with you to resolve the issue promptly. diff --git a/README.md b/README.md index 6f0d3d76af..1b465876b5 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![latest release version](https://img.shields.io/github/v/release/logseq/logseq)](https://github.com/logseq/logseq/releases) [![License](https://img.shields.io/github/license/logseq/logseq?color=blue)](https://github.com/logseq/logseq/blob/master/LICENSE.md) [![Twitter follow](https://img.shields.io/badge/follow-%40logseq-blue.svg?style=flat&logo=twitter)](https://twitter.com/logseq) +[![forum](https://img.shields.io/badge/forum-Logseq-blue.svg?style=flat&logo=discourse)](https://discuss.logseq.com) [![discord](https://img.shields.io/discord/725182569297215569?label=discord&logo=Discord&color=blue)](https://discord.gg/KpN4eHY) [![total](https://opencollective.com/logseq/tiers/badge.svg?color=blue)](https://opencollective.com/logseq) @@ -68,7 +69,8 @@ Logseq is also made possible by the following projects: - Our blog: https://logseq.com/blog - Please be sure to visit our [About page](https://logseq.com/blog/about) for the latest updates of the app - Twitter: https://twitter.com/logseq -- Discord: https://discord.gg/KpN4eHY - Where we answer questions, discuss workflows and share tips +- Forum: https://discuss.logseq.com - Where we answer questions, discuss workflows and share tips +- Discord: https://discord.gg/KpN4eHY - 中文 Discord:https://discord.gg/xYqcrXWymg - Github: https://github.com/logseq/logseq - everyone is encouraged to report issues! diff --git a/android/app/build.gradle b/android/app/build.gradle index c688f30e97..f42ac4b0cd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.logseq.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 29 - versionName "0.7.5" + versionCode 30 + versionName "0.7.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/deps/graph-parser/src/logseq/graph_parser/block.cljs b/deps/graph-parser/src/logseq/graph_parser/block.cljs index a6c91a156f..b98aa8e55e 100644 --- a/deps/graph-parser/src/logseq/graph_parser/block.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/block.cljs @@ -139,52 +139,78 @@ (vector? block) (= "Timestamp" (first block)))) -;; TODO: we should move this to mldoc -(defn extract-properties - [format properties user-config] - (when (seq properties) - (let [properties (seq properties) - page-refs (->> +(defn- get-page-ref-names-from-properties + [format properties] + (let [page-refs (->> properties (remove (fn [[k _]] (contains? #{:background-color :background_color} (keyword k)))) (map last) (map (fn [v] - (when (and (string? v) - (not (gp-mldoc/link? format v))) + (cond + (and (string? v) + (not (gp-mldoc/link? format v))) (let [v (string/trim v) result (text/split-page-refs-without-brackets v {:un-brackets? false})] (if (coll? result) (map text/page-ref-un-brackets! result) - []))))) - (apply concat) - (remove string/blank?)) + [])) + + (coll? v) + (map (fn [s] + (when-not (and (string? v) + (gp-mldoc/link? format v)) + (text/page-ref-un-brackets! s))) v) + + :else + nil))) + (apply concat)) + property-keys-page-refs (some->> properties + (map (comp name first)) + (remove string/blank?) + (distinct))] + (->> (concat page-refs property-keys-page-refs) + (remove string/blank?) + distinct))) + +(defn- invalid-property-key? + [s] + (string/includes? s "`")) + +(defn extract-properties + [format properties user-config] + (when (seq properties) + (let [properties (seq properties) + properties (into {} properties) + page-refs (get-page-ref-names-from-properties format properties) properties (->> properties (map (fn [[k v]] (let [k (-> (string/lower-case (name k)) (string/replace " " "-") - (string/replace "_" "-")) - k (if (contains? #{"custom_id" "custom-id"} k) - "id" - k) - v (if (coll? v) - (remove string/blank? v) - (cond - (string/blank? v) - nil - (and (= (keyword k) :file-path) - (string/starts-with? v "file:")) - v - :else - (text/parse-property format k v user-config))) - k (keyword k) - v (if (and - (string? v) - (contains? #{:alias :aliases :tags} k)) - (set [v]) - v) - v (if (coll? v) (set v) v)] - [k v]))) + (string/replace "_" "-") + (string/replace #"[\"|^|(|)|{|}]+" ""))] + (when-not (invalid-property-key? k) + (let [k (if (contains? #{"custom_id" "custom-id"} k) + "id" + k) + v (if (coll? v) + (remove string/blank? v) + (cond + (string/blank? v) + nil + (and (= (keyword k) :file-path) + (string/starts-with? v "file:")) + v + :else + (text/parse-property format k v user-config))) + k (keyword k) + v (if (and + (string? v) + (contains? #{:alias :aliases :tags} k)) + (set [v]) + v) + v (if (coll? v) (set v) v)] + [k v]))))) (remove #(nil? (second %))))] {:properties (into {} properties) :properties-order (map first properties) @@ -437,17 +463,8 @@ (d/squuid))) (defn get-page-refs-from-properties - [properties db date-formatter] - (let [page-refs (mapcat (fn [v] (cond - (coll? v) - v - - (text/page-ref? v) - [(text/page-ref-un-brackets! v)] - - :else - nil)) (vals properties)) - page-refs (remove string/blank? page-refs)] + [format properties db date-formatter] + (let [page-refs (get-page-ref-names-from-properties format properties)] (map (fn [page] (page-name->map page true db true date-formatter)) page-refs))) (defn- with-page-block-refs @@ -461,6 +478,7 @@ (defn- with-pre-block-if-exists [blocks body pre-block-properties encoded-content {:keys [supported-formats db date-formatter]}] (let [first-block (first blocks) + format (or (:block/format first-block) :markdown) first-block-start-pos (get-in first-block [:block/meta :start_pos]) ;; Add pre-block @@ -471,7 +489,7 @@ (let [content (utf8/substring encoded-content 0 first-block-start-pos) {:keys [properties properties-order]} pre-block-properties id (get-custom-id-or-new-id {:properties properties}) - property-refs (->> (get-page-refs-from-properties properties db date-formatter) + property-refs (->> (get-page-refs-from-properties format properties db date-formatter) (map :block/original-name)) block {:uuid id :content content diff --git a/deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs b/deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs index 4d8eecb6cb..3ef6f05edc 100644 --- a/deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs @@ -142,7 +142,7 @@ ;; only increase over time as the docs graph rarely has deletions (testing "Counts" (is (= 211 (count files)) "Correct file count") - (is (= 40945 (count (d/datoms db :eavt))) "Correct datoms count") + (is (= 44212 (count (d/datoms db :eavt))) "Correct datoms count") (is (= 3600 (ffirst diff --git a/deps/graph-parser/src/logseq/graph_parser/util.cljs b/deps/graph-parser/src/logseq/graph_parser/util.cljs index be6da7c422..8fc1a56d08 100644 --- a/deps/graph-parser/src/logseq/graph_parser/util.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/util.cljs @@ -116,7 +116,7 @@ (remove-boundary-slashes) (path-normalize))] (if replace-slash? - (string/replace page #"/" "%2A") + (string/replace page #"/" "%2F") page)))) (defn page-name-sanity-lc diff --git a/deps/graph-parser/test/logseq/graph_parser/block_test.cljs b/deps/graph-parser/test/logseq/graph_parser/block_test.cljs index 2cf87d4cea..074b1b55db 100644 --- a/deps/graph-parser/test/logseq/graph_parser/block_test.cljs +++ b/deps/graph-parser/test/logseq/graph_parser/block_test.cljs @@ -25,12 +25,12 @@ [["file-path" "file:///home/x, y.pdf"]] {:file-path "file:///home/x, y.pdf"}) (are [x y] (= (vec (:page-refs (gp-block/extract-properties :markdown x {}))) y) - [["year" "1000"]] [] - [["year" "\"1000\""]] [] - [["foo" "[[bar]] test"]] ["bar" "test"] - [["foo" "[[bar]] test [[baz]]"]] ["bar" "test" "baz"] - [["foo" "[[bar]] test [[baz]] [[nested [[baz]]]]"]] ["bar" "test" "baz" "nested [[baz]]"] - [["foo" "#bar, #baz"]] ["bar" "baz"] - [["foo" "[[nested [[page]]]], test"]] ["nested [[page]]" "test"])) + [["year" "1000"]] ["year"] + [["year" "\"1000\""]] ["year"] + [["foo" "[[bar]] test"]] ["bar" "test" "foo"] + [["foo" "[[bar]] test [[baz]]"]] ["bar" "test" "baz" "foo"] + [["foo" "[[bar]] test [[baz]] [[nested [[baz]]]]"]] ["bar" "test" "baz" "nested [[baz]]" "foo"] + [["foo" "#bar, #baz"]] ["bar" "baz" "foo"] + [["foo" "[[nested [[page]]]], test"]] ["nested [[page]]" "test" "foo"])) #_(cljs.test/run-tests) diff --git a/docs/develop-logseq.md b/docs/develop-logseq.md index 48d609993b..e248431fad 100644 --- a/docs/develop-logseq.md +++ b/docs/develop-logseq.md @@ -24,12 +24,14 @@ yarn watch Then open the browser . -### Production +### Production Build ```bash yarn release ``` +The released files will be at `resources/` directory. + ## Desktop app development ### Development @@ -37,23 +39,39 @@ yarn release 1. Install npm packages for building the desktop app ``` bash -yarn install && cd static && yarn install && cd .. +yarn install ``` + 2. Compile to JavaScript and open the dev app ```bash yarn watch -# Wait until watch is finished building and then in a different shell -# If you have opened desktop logseq, you should close it. Otherwise, the following command will fail. +# Wait until watch reports `Build Completed.` for `:electron` and `:app`. +# Then, run the following command in a different shell. +# If you have opened desktop logseq, you should close it. Otherwise, this command will fail. yarn dev-electron-app ``` Alternatively, run `bb dev:electron-start` to do this step with one command. To download bb, see https://github.com/babashka/babashka#installation. -### Production +3. (Optional) Update dependencies if your are updating from an old branch + +```bash +# pull new changes +git pull + +cd static && yarn install && cd .. +``` + +Here `static/` is generated by `yarn watch` command. + +### Production Build + Build a release: ```bash yarn release-electron ``` + +The final released binaries or installers will be at `static/out/`. diff --git a/e2e-tests/basic.spec.ts b/e2e-tests/basic.spec.ts index 802263b168..747d463398 100644 --- a/e2e-tests/basic.spec.ts +++ b/e2e-tests/basic.spec.ts @@ -203,6 +203,8 @@ test('auto completion and auto pair', async ({ page, block }) => { await block.mustType('type (', { toBe: 'type ()' }) await block.mustType('(', { toBe: 'type (())' }) + await block.escapeEditing() // escape any popup from `(())` + // [[ #3251 await block.clickNext() diff --git a/e2e-tests/page-search.spec.ts b/e2e-tests/page-search.spec.ts index bf87175a6b..2de9a02e57 100644 --- a/e2e-tests/page-search.spec.ts +++ b/e2e-tests/page-search.spec.ts @@ -157,6 +157,6 @@ async function alias_test(page: Page, page_name: string, search_kws: string[]) { // TODO: search clicking (alias property) } -test('page diacritic alias', async ({ page }) => { +test.skip('page diacritic alias', async ({ page }) => { await alias_test(page, "ü", ["ü", "ü", "Ü"]) }) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 0f21e53069..79764229b7 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -542,7 +542,7 @@ INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.7.5; + MARKETING_VERSION = 0.7.6; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -568,7 +568,7 @@ INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.7.5; + MARKETING_VERSION = 0.7.6; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -593,7 +593,7 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 0.7.5; + MARKETING_VERSION = 0.7.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController; @@ -620,7 +620,7 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 0.7.5; + MARKETING_VERSION = 0.7.6; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ios/App/App/FileContainer.swift b/ios/App/App/FileContainer.swift index 3260a13823..f52c3b28c2 100644 --- a/ios/App/App/FileContainer.swift +++ b/ios/App/App/FileContainer.swift @@ -21,8 +21,13 @@ public class FileContainer: CAPPlugin, UIDocumentPickerDelegate { @objc func ensureDocuments(_ call: CAPPluginCall) { - validateDocuments(at: self.iCloudContainerUrl!) - validateDocuments(at: self.localContainerUrl!) + if self.iCloudContainerUrl != nil { + validateDocuments(at: self.iCloudContainerUrl!) + } + + if self.localContainerUrl != nil { + validateDocuments(at: self.localContainerUrl!) + } call.resolve(["path": [self.iCloudContainerUrl?.path as Any, self.localContainerUrl?.path as Any]]) diff --git a/libs/src/LSPlugin.ts b/libs/src/LSPlugin.ts index 79df97694c..66dc6d6c9b 100644 --- a/libs/src/LSPlugin.ts +++ b/libs/src/LSPlugin.ts @@ -196,6 +196,7 @@ export interface PageEntity { children?: Array format?: 'markdown' | 'org' journalDay?: number + updatedAt?: number } export type BlockIdentity = BlockUUID | Pick @@ -622,7 +623,7 @@ export interface IEditorProxy extends Record { renamePage: (oldName: string, newName: string) => Promise - getAllPages: (repo?: string) => Promise + getAllPages: (repo?: string) => Promise prependBlockInPage: ( page: PageIdentity, diff --git a/package.json b/package.json index fc270bfd3a..ab37775b9f 100644 --- a/package.json +++ b/package.json @@ -115,11 +115,11 @@ "react-grid-layout": "0.16.6", "react-icon-base": "^2.1.2", "react-icons": "2.2.7", + "react-intersection-observer": "^9.3.5", "react-resize-context": "3.0.0", "react-textarea-autosize": "8.3.3", "react-tippy": "1.4.0", "react-transition-group": "4.3.0", - "react-visibility-sensor": "^5.1.1", "reakit": "0.11.1", "remove-accents": "0.4.2", "send-intent": "3.0.11", diff --git a/resources/package.json b/resources/package.json index 8e755df573..b038767888 100644 --- a/resources/package.json +++ b/resources/package.json @@ -1,6 +1,6 @@ { "name": "Logseq", - "version": "0.7.5", + "version": "0.7.6", "main": "electron.js", "author": "Logseq", "license": "AGPL-3.0", diff --git a/src/main/frontend/commands.cljs b/src/main/frontend/commands.cljs index ec36da107e..a0d4bfa2bb 100644 --- a/src/main/frontend/commands.cljs +++ b/src/main/frontend/commands.cljs @@ -22,11 +22,8 @@ ;; TODO: move to frontend.handler.editor.commands -(defonce *show-commands (atom false)) -(defonce *slash-caret-pos (atom nil)) -(defonce *show-block-commands (atom false)) (defonce angle-bracket "<") -(defonce *angle-bracket-caret-pos (atom nil)) +(defonce colon ":") (defonce *current-command (atom nil)) (def query-doc @@ -302,16 +299,20 @@ (defonce *matched-block-commands (atom (block-commands-map))) -(defn restore-state - [restore-slash-caret-pos?] - (when restore-slash-caret-pos? - (reset! *slash-caret-pos nil)) - (reset! *show-commands false) - (reset! *angle-bracket-caret-pos nil) - (reset! *show-block-commands false) - (reset! *matched-commands @*initial-commands) +(defn reinit-matched-commands! + [] + (reset! *matched-commands @*initial-commands)) + +(defn reinit-matched-block-commands! + [] (reset! *matched-block-commands (block-commands-map))) +(defn restore-state + [] + (state/clear-editor-action!) + (reinit-matched-commands!) + (reinit-matched-block-commands!)) + (defn insert! [id value {:keys [last-pattern postfix-fn backward-pos forward-pos end-pattern backward-truncate-number] @@ -327,19 +328,22 @@ (+ current-pos i))) current-pos) orig-prefix (subs edit-content 0 current-pos) - space? (when (and last-pattern orig-prefix) - (let [s (when-let [last-index (string/last-index-of orig-prefix last-pattern)] - (gp-util/safe-subs orig-prefix 0 last-index))] - (not - (or - (and s - (string/ends-with? s "(") - (or (string/starts-with? last-pattern "((") - (string/starts-with? last-pattern "[["))) - (and s (string/starts-with? s "{{embed")))))) - space? (if (and space? (string/starts-with? last-pattern "#[[")) - false - space?) + space? (let [space? (when (and last-pattern orig-prefix) + (let [s (when-let [last-index (string/last-index-of orig-prefix last-pattern)] + (gp-util/safe-subs orig-prefix 0 last-index))] + (not + (or + (and s + (string/ends-with? s "(") + (or (string/starts-with? last-pattern "((") + (string/starts-with? last-pattern "[["))) + (and s (string/starts-with? s "{{embed")) + (and last-pattern + (or (string/ends-with? last-pattern "::") + (string/starts-with? last-pattern "::")))))))] + (if (and space? (string/starts-with? last-pattern "#[[")) + false + space?)) prefix (cond (and backward-truncate-number (integer? backward-truncate-number)) (str (gp-util/safe-subs orig-prefix 0 (- (count orig-prefix) backward-truncate-number)) @@ -366,12 +370,13 @@ (str prefix postfix)) new-pos (- (count prefix) (or backward-pos 0))] - (state/set-block-content-and-last-pos! id new-value new-pos) - (cursor/move-cursor-to input - (if (and (or backward-pos forward-pos) - (not= end-pattern "]]")) - new-pos - (inc new-pos)))))) + (when-not (string/blank? new-value) + (state/set-block-content-and-last-pos! id new-value new-pos) + (cursor/move-cursor-to input + (if (and (or backward-pos forward-pos) + (not= end-pattern "]]")) + new-pos + (inc new-pos))))))) (defn simple-insert! [id value @@ -464,13 +469,13 @@ (let [type (:type option) input (gdom/getElement input-id) beginning-of-line? (or (cursor/beginning-of-line? input) - (= 1 (:pos @*angle-bracket-caret-pos))) + (= 1 (:pos (:pos (state/get-editor-action-data))))) value (if (and (contains? #{"block" "properties"} type) (not beginning-of-line?)) (str "\n" value) value)] (insert! input-id value option) - (reset! *show-commands false)))) + (state/clear-editor-action!)))) (defmethod handle-step :editor/cursor-back [[_ n]] (when-let [input-id (state/get-edit-input-id)] @@ -537,7 +542,7 @@ (when-let [input-id (state/get-edit-input-id)] (when-let [current-input (gdom/getElement input-id)] (let [edit-content (gobj/get current-input "value") - slash-pos (:pos @*slash-caret-pos) + slash-pos (:pos (:pos (state/get-editor-action-data))) [re-pattern new-line-re-pattern] (if (= :org format) [#"\*+\s" #"\n\*+\s"] [#"#+\s" #"\n#+\s"]) @@ -595,22 +600,22 @@ (state/set-edit-content! input-id new-value))))) (defmethod handle-step :editor/search-page [[_]] - (state/set-editor-show-page-search! true)) + (state/set-editor-action! :page-search)) (defmethod handle-step :editor/search-page-hashtag [[_]] - (state/set-editor-show-page-search-hashtag! true)) + (state/set-editor-action! :page-search-hashtag)) (defmethod handle-step :editor/search-block [[_ _type]] - (state/set-editor-show-block-search! true)) + (state/set-editor-action! :block-search)) (defmethod handle-step :editor/search-template [[_]] - (state/set-editor-show-template-search! true)) + (state/set-editor-action! :template-search)) (defmethod handle-step :editor/show-input [[_ option]] (state/set-editor-show-input! option)) (defmethod handle-step :editor/show-zotero [[_]] - (state/set-editor-show-zotero! true)) + (state/set-editor-action! :zotero)) (defn insert-youtube-timestamp [] @@ -632,8 +637,8 @@ (string/blank? value))) (do (notification/show! [:div "Please add some content first."] :warning) - (restore-state false)) - (state/set-editor-show-date-picker! true))) + (restore-state)) + (state/set-editor-action! :datepicker))) (defmethod handle-step :editor/click-hidden-file-input [[_ _input-id]] (when-let [input-file (gdom/getElement "upload-file")] diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 9bd3b22af6..bafe190ab9 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -421,7 +421,9 @@ [config page-name-in-block page-name redirect-page-name page-entity contents-page? children html-export? label] (let [tag? (:tag? config)] [:a - {:class (if tag? "tag" "page-ref") + {:class (cond-> (if tag? "tag" "page-ref") + (:property? config) + (str " page-property-key")) :data-ref page-name :on-mouse-down (fn [e] @@ -1065,7 +1067,7 @@ ;;;; Macro component render functions (defn- macro-query-cp [config arguments] - [:div.dsl-query + [:div.dsl-query.overflow-x-hidden.pr-3.sm:pr-0 (let [query (->> (string/join ", " arguments) (string/trim))] (when-not (string/blank? query) @@ -1805,7 +1807,7 @@ [config block k v] (let [date (and (= k :date) (date/get-locale-string (str v)))] [:div - [:span.page-property-key.font-medium (name k)] + (page-cp (assoc config :property? true) {:block/name (subs (str k) 1)}) [:span.mr-1 ":"] (cond (int? v) @@ -1894,12 +1896,12 @@ (do (reset! show? false) (reset! commands/*current-command nil) - (state/set-editor-show-date-picker! false) + (state/clear-editor-action!) (state/set-timestamp-block! nil)) (do (reset! show? true) (reset! commands/*current-command typ) - (state/set-editor-show-date-picker! true) + (state/set-editor-action! :datepicker) (state/set-timestamp-block! {:block block :typ typ :show? show?}))))} @@ -1923,7 +1925,8 @@ (util/details-or-summary? target) (and (util/sup? target) (dom/has-class? target "fn")) - (dom/has-class? target "image-resize"))) + (dom/has-class? target "image-resize") + (dom/closest target "a"))) (defn- block-content-on-mouse-down [e block block-id _content edit-input-id] @@ -2057,7 +2060,7 @@ [:<> [:div.flex.flex-row.justify-between.block-content-inner - [:div.flex-1 + [:div.flex-1.w-full (cond (seq title) (build-block-title config block) @@ -2168,7 +2171,10 @@ (ui/block-error "Block Render Error:" {:content (:block/content block) :section-attrs - {:on-click #(state/set-editing! edit-input-id (:block/content block) block "")}}) + {:on-click #(do + (editor-handler/clear-selection!) + (editor-handler/unhighlight-blocks!) + (state/set-editing! edit-input-id (:block/content block) block ""))}}) (block-content config block edit-input-id block-id slide?))] [:div.flex.flex-row.items-center (when (and (:embed? config) @@ -2551,10 +2557,7 @@ custom-query? (boolean (:custom-query? config))] (if (and ref? (not custom-query?) (not (:ref-query-child? config))) (ui/lazy-visible - (fn [] - (block-container-inner state repo config block)) - nil - {}) + (fn [] (block-container-inner state repo config block))) (block-container-inner state repo config block)))) (defn divide-lists @@ -2892,9 +2895,7 @@ (ui/catch-error (ui/block-error "Query Error:" {:content (:query q)}) (ui/lazy-visible - (fn [] (custom-query* config q)) - nil - {}))) + (fn [] (custom-query* config q))))) (defn admonition [config type result] (when-let [icon (case (string/lower-case (name type)) diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index ff1e4cf84a..199bce580c 100644 --- a/src/main/frontend/components/block.css +++ b/src/main/frontend/components/block.css @@ -188,7 +188,7 @@ .block-left-menu { background-color: var(--ls-secondary-background-color); background: linear-gradient(90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%); - + .commands-button { overflow: hidden; max-width: 40px; @@ -613,9 +613,14 @@ a.cloze-revealed { } .page-property-key { + @apply font-medium; color: var(--ls-secondary-text-color); } +.page-property-key:hover { + background-color: var(--ls-selection-background-color); +} + .block-parents a { color: var(--ls-primary-text-color); } diff --git a/src/main/frontend/components/datetime.cljs b/src/main/frontend/components/datetime.cljs index f7bdef5d7e..d9072ea0a7 100644 --- a/src/main/frontend/components/datetime.cljs +++ b/src/main/frontend/components/datetime.cljs @@ -105,8 +105,7 @@ (when show? (reset! show? false))) (clear-timestamp!) - (state/set-editor-show-date-picker! false) - (commands/restore-state false)) + (commands/restore-state)) (rum/defc time-repeater < rum/reactive (mixins/event-mixin @@ -147,7 +146,7 @@ (contains? #{"deadline" "scheduled"} (string/lower-case current-command))) date (state/sub :date-picker/date)] - (when (state/sub :editor/show-date-picker?) + (when (= :datepicker (state/sub :editor/action)) [:div#date-time-picker.flex.flex-row {:on-click (fn [e] (util/stop e)) :on-mouse-down (fn [e] (.stopPropagation e))} (ui/datepicker @@ -164,7 +163,7 @@ (util/format "[[%s]]" journal) format nil) - (state/set-editor-show-date-picker! false) + (state/clear-editor-action!) (reset! commands/*current-command nil))))}) (when deadline-or-schedule? (time-repeater))]))) diff --git a/src/main/frontend/components/editor.cljs b/src/main/frontend/components/editor.cljs index a731cb17af..77776d0880 100644 --- a/src/main/frontend/components/editor.cljs +++ b/src/main/frontend/components/editor.cljs @@ -2,11 +2,12 @@ (:require [clojure.string :as string] [goog.string :as gstring] [frontend.commands :as commands - :refer [*angle-bracket-caret-pos *first-command-group *matched-block-commands *matched-commands *show-block-commands *show-commands *slash-caret-pos]] + :refer [*first-command-group *matched-block-commands *matched-commands]] [frontend.components.block :as block] [frontend.components.datetime :as datetime-comp] [frontend.components.search :as search] [frontend.components.svg :as svg] + [frontend.context.i18n :refer [t]] [frontend.db :as db] [frontend.db.model :as db-model] [frontend.extensions.zotero :as zotero] @@ -29,8 +30,8 @@ (rum/defc commands < rum/reactive [id format] - (let [matched (util/react *matched-commands)] - (when (util/react *show-commands) + (when (= :commands (state/sub :editor/action)) + (let [matched (util/react *matched-commands)] (ui/auto-complete matched {:get-group-name @@ -82,7 +83,7 @@ (rum/defc block-commands < rum/reactive [id format] - (when (util/react *show-block-commands) + (when (= :block-commands (state/get-editor-action)) (let [matched (util/react *matched-block-commands)] (ui/auto-complete (map first matched) @@ -99,59 +100,63 @@ {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)} "Embedded page searching popup" [id format] - (when (state/sub :editor/show-page-search?) - (let [pos (state/get-editor-last-pos) - input (gdom/getElement id)] - (when input - (let [current-pos (cursor/pos input) - edit-content (or (state/sub [:editor/content id]) "") - sidebar? (in-sidebar? input) - q (or - @editor-handler/*selected-text - (when (state/sub :editor/show-page-search-hashtag?) - (gp-util/safe-subs edit-content pos current-pos)) - (when (> (count edit-content) current-pos) - (gp-util/safe-subs edit-content pos current-pos)) - "") - matched-pages (when-not (string/blank? q) - (editor-handler/get-matched-pages q)) - matched-pages (cond - (contains? (set (map util/page-name-sanity-lc matched-pages)) (util/page-name-sanity-lc (string/trim q))) ;; if there's a page name fully matched - matched-pages + (let [action (state/sub :editor/action)] + (when (contains? #{:page-search :page-search-hashtag} action) + (let [pos (state/get-editor-last-pos) + input (gdom/getElement id)] + (when input + (let [current-pos (cursor/pos input) + edit-content (or (state/sub [:editor/content id]) "") + sidebar? (in-sidebar? input) + q (or + @editor-handler/*selected-text + (when (= action :page-search-hashtag) + (gp-util/safe-subs edit-content pos current-pos)) + (when (> (count edit-content) current-pos) + (gp-util/safe-subs edit-content pos current-pos)) + "") + matched-pages (when-not (string/blank? q) + (editor-handler/get-matched-pages q)) + matched-pages (cond + (contains? (set (map util/page-name-sanity-lc matched-pages)) + (util/page-name-sanity-lc (string/trim q))) ;; if there's a page name fully matched + (sort-by (fn [m] + [(count m) m]) + matched-pages) - (string/blank? q) - nil + (string/blank? q) + nil - (empty? matched-pages) - (cons (str "New page: " q) matched-pages) + (empty? matched-pages) + (cons (str (t :new-page) ": " q) matched-pages) - ;; reorder, shortest and starts-with first. - :else - (let [matched-pages (remove nil? matched-pages) - matched-pages (sort-by - (fn [m] - [(not (gstring/caseInsensitiveStartsWith m q)) (count m)]) - matched-pages)] - (if (gstring/caseInsensitiveStartsWith (first matched-pages) q) - (cons (first matched-pages) - (cons (str "New page: " q) (rest matched-pages))) - (cons (str "New page: " q) matched-pages))))] - (ui/auto-complete - matched-pages - {:on-chosen (page-handler/on-chosen-handler input id q pos format) - :on-enter #(page-handler/page-not-exists-handler input id q current-pos) - :item-render (fn [page-name chosen?] - [:div.preview-trigger-wrapper - (block/page-preview-trigger - {:children [:div (search/highlight-exact-query page-name q)] - :open? chosen? - :manual? true - :fixed-position? true - :tippy-distance 24 - :tippy-position (if sidebar? "left" "right")} - page-name)]) - :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 "Search for a page"] - :class "black"})))))) + ;; reorder, shortest and starts-with first. + :else + (let [matched-pages (remove nil? matched-pages) + matched-pages (sort-by + (fn [m] + [(not (gstring/caseInsensitiveStartsWith m q)) (count m) m]) + matched-pages)] + (if (gstring/caseInsensitiveStartsWith (first matched-pages) q) + (cons (first matched-pages) + (cons (str (t :new-page) ": " q) (rest matched-pages))) + (cons (str (t :new-page) ": " q) matched-pages))))] + (ui/auto-complete + matched-pages + {:on-chosen (page-handler/on-chosen-handler input id q pos format) + :on-enter #(page-handler/page-not-exists-handler input id q current-pos) + :item-render (fn [page-name chosen?] + [:div.preview-trigger-wrapper + (block/page-preview-trigger + {:children [:div (search/highlight-exact-query page-name q)] + :open? chosen? + :manual? true + :fixed-position? true + :tippy-distance 24 + :tippy-position (if sidebar? "left" "right")} + page-name)]) + :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 "Search for a page"] + :class "black"}))))))) (rum/defcs block-search-auto-complete < rum/reactive {:init (fn [state] @@ -164,24 +169,25 @@ (reset! result matched-blocks))) state)} [state _edit-block input id q format] - (let [result (rum/react (get state ::result)) + (let [result (->> (rum/react (get state ::result)) + (remove (fn [b] (string/blank? (:block/content (db-model/query-block-by-uuid (:block/uuid b))))))) chosen-handler (editor-handler/block-on-chosen-handler input id q format) non-exist-block-handler (editor-handler/block-non-exist-handler input)] - (when result - (ui/auto-complete - result - {:on-chosen chosen-handler - :on-enter non-exist-block-handler - :empty-placeholder [:div.text-gray-500.pl-4.pr-4 "Search for a block"] - :item-render (fn [{:block/keys [page uuid]}] ;; content returned from search engine is normalized - (let [page (or (:block/original-name page) - (:block/name page)) - repo (state/sub :git/current-repo) - format (db/get-page-format page) - block (db-model/query-block-by-uuid uuid) - content (:block/content block)] - [:.py-2 (search/block-search-result-item repo uuid format content q :block)])) - :class "black"})))) + (ui/auto-complete + result + {:on-chosen chosen-handler + :on-enter non-exist-block-handler + :empty-placeholder [:div.text-gray-500.pl-4.pr-4 "Search for a block"] + :item-render (fn [{:block/keys [page uuid]}] ;; content returned from search engine is normalized + (let [page (or (:block/original-name page) + (:block/name page)) + repo (state/sub :git/current-repo) + format (db/get-page-format page) + block (db-model/query-block-by-uuid uuid) + content (:block/content block)] + (when-not (string/blank? content) + [:.py-2 (search/block-search-result-item repo uuid format content q :block)]))) + :class "black"}))) (rum/defcs block-search < rum/reactive {:will-unmount (fn [state] @@ -189,7 +195,7 @@ (state/clear-search-result!) state)} [state id _format] - (when (state/sub :editor/show-block-search?) + (when (= :block-search (state/sub :editor/action)) (let [pos (state/get-editor-last-pos) input (gdom/getElement id) [id format] (:rum/args state) @@ -206,27 +212,73 @@ (rum/defc template-search < rum/reactive {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)} [id _format] - (when (state/sub :editor/show-template-search?) - (let [pos (state/get-editor-last-pos) - input (gdom/getElement id)] - (when input - (let [current-pos (cursor/pos input) - edit-content (state/sub [:editor/content id]) - q (or - (when (>= (count edit-content) current-pos) - (subs edit-content pos current-pos)) - "") - matched-templates (editor-handler/get-matched-templates q) - non-exist-handler (fn [_state] - (state/set-editor-show-template-search! false))] - (ui/auto-complete - matched-templates - {:on-chosen (editor-handler/template-on-chosen-handler id) - :on-enter non-exist-handler - :empty-placeholder [:div.text-gray-500.px-4.py-2.text-sm "Search for a template"] - :item-render (fn [[template _block-db-id]] - template) - :class "black"})))))) + (let [pos (state/get-editor-last-pos) + input (gdom/getElement id)] + (when input + (let [current-pos (cursor/pos input) + edit-content (state/sub [:editor/content id]) + q (or + (when (>= (count edit-content) current-pos) + (subs edit-content pos current-pos)) + "") + matched-templates (editor-handler/get-matched-templates q) + non-exist-handler (fn [_state] + (state/clear-editor-action!))] + (ui/auto-complete + matched-templates + {:on-chosen (editor-handler/template-on-chosen-handler id) + :on-enter non-exist-handler + :empty-placeholder [:div.text-gray-500.px-4.py-2.text-sm "Search for a template"] + :item-render (fn [[template _block-db-id]] + template) + :class "black"}))))) + +(rum/defc property-search < rum/reactive + {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)} + [id] + (let [input (gdom/getElement id)] + (when input + (let [q (or (:searching-property (editor-handler/get-searching-property input)) + "") + matched-properties (editor-handler/get-matched-properties q) + q-property (string/replace (string/lower-case q) #"\s+" "-") + non-exist-handler (fn [_state] + ((editor-handler/property-on-chosen-handler id q-property) nil))] + (ui/auto-complete + matched-properties + {:on-chosen (editor-handler/property-on-chosen-handler id q-property) + :on-enter non-exist-handler + :empty-placeholder [:div.px-4.py-2.text-sm (str "Create a new property: " q-property)] + :header [:div.px-4.py-2.text-sm.font-medium "Matched properties: "] + :item-render (fn [property] property) + :class "black"}))))) + +(rum/defc property-value-search < rum/reactive + {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)} + [id] + (let [property (:property (state/get-editor-action-data)) + input (gdom/getElement id)] + (when (and input + (not (string/blank? property))) + (let [current-pos (cursor/pos input) + edit-content (state/sub [:editor/content id]) + start-idx (string/last-index-of (subs edit-content 0 current-pos) "::") + q (or + (when (>= current-pos (+ start-idx 2)) + (subs edit-content (+ start-idx 2) current-pos)) + "") + q (string/triml q) + matched-values (editor-handler/get-matched-property-values property q) + non-exist-handler (fn [_state] + ((editor-handler/property-value-on-chosen-handler id q) nil))] + (ui/auto-complete + matched-values + {:on-chosen (editor-handler/property-value-on-chosen-handler id q) + :on-enter non-exist-handler + :empty-placeholder [:div.px-4.py-2.text-sm (str "Create a new property value: " q)] + :header [:div.px-4.py-2.text-sm.font-medium "Matched property values: "] + :item-render (fn [property-value] property-value) + :class "black"}))))) (rum/defcs input < rum/reactive (rum/local {} ::input-value) @@ -237,44 +289,44 @@ {;; enter 13 (fn [state e] (let [input-value (get state ::input-value) - input-option (get @state/state :editor/show-input)] + input-option (:options (state/get-editor-show-input))] (when (seq @input-value) ;; no new line input (util/stop e) (let [[_id on-submit] (:rum/args state) - {:keys [pos]} @*slash-caret-pos command (:command (first input-option))] - (on-submit command @input-value pos)) + (on-submit command @input-value)) (reset! input-value nil))))}))) [state _id on-submit] - (when-let [input-option (state/sub :editor/show-input)] - (let [{:keys [pos]} (util/react *slash-caret-pos) - input-value (get state ::input-value)] - (when (seq input-option) - (let [command (:command (first input-option))] - [:div.p-2.rounded-md.shadow-lg - (for [{:keys [id placeholder type autoFocus] :as input-item} input-option] - [:div.my-3 {:key id} - [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5 - (merge - (cond-> - {:key (str "modal-input-" (name id)) - :id (str "modal-input-" (name id)) - :type (or type "text") - :on-change (fn [e] - (swap! input-value assoc id (util/evalue e))) - :auto-complete (if (util/chrome?) "chrome-off" "off")} - placeholder - (assoc :placeholder placeholder) - autoFocus - (assoc :auto-focus true)) - (dissoc input-item :id))]]) - (ui/button - "Submit" - :on-click - (fn [e] - (util/stop e) - (on-submit command @input-value pos)))]))))) + (when (= :input (state/sub :editor/action)) + (when-let [action-data (state/sub :editor/action-data)] + (let [{:keys [pos options]} action-data + input-value (get state ::input-value)] + (when (seq options) + (let [command (:command (first options))] + [:div.p-2.rounded-md.shadow-lg + (for [{:keys [id placeholder type autoFocus] :as input-item} options] + [:div.my-3 {:key id} + [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5 + (merge + (cond-> + {:key (str "modal-input-" (name id)) + :id (str "modal-input-" (name id)) + :type (or type "text") + :on-change (fn [e] + (swap! input-value assoc id (util/evalue e))) + :auto-complete (if (util/chrome?) "chrome-off" "off")} + placeholder + (assoc :placeholder placeholder) + autoFocus + (assoc :auto-focus true)) + (dissoc input-item :id))]]) + (ui/button + "Submit" + :on-click + (fn [e] + (util/stop e) + (on-submit command @input-value pos)))])))))) (rum/defc absolute-modal < rum/static [cp set-default-width? {:keys [top left rect]}] @@ -305,37 +357,37 @@ to-max-height (if y-overflow-vh? max-height to-max-height) pos-rect (when (and (seq rect) editing-key) (:rect (cursor/get-caret-pos (state/get-input)))) - y-diff (when pos-rect (- (:height pos-rect) (:height rect)))] + y-diff (when pos-rect (- (:height pos-rect) (:height rect))) + style (merge + {:top (+ top offset-top (if (int? y-diff) y-diff 0)) + :max-height to-max-height + :max-width 700 + ;; TODO: auto responsive fixed size + :width "fit-content" + :z-index 11} + (when set-default-width? + {:width max-width}) + (let [^js/HTMLElement editor + (js/document.querySelector ".editor-wrapper")] + (if (<= (.-clientWidth editor) (+ left (if set-default-width? max-width 500))) + {:right 0} + {:left (if (or (nil? y-diff) (and y-diff (= y-diff 0))) left 0)})))] [:div.absolute.rounded-md.shadow-lg.absolute-modal {:ref *el :class (if y-overflow-vh? "is-overflow-vh-y" "") :on-mouse-down (fn [e] (.stopPropagation e)) - :style (merge - {:top (+ top offset-top (if (int? y-diff) y-diff 0)) - :max-height to-max-height - :max-width 700 - ;; TODO: auto responsive fixed size - :width "fit-content" - :z-index 11} - (when set-default-width? - {:width max-width}) - (let [^js/HTMLElement editor - (js/document.querySelector ".editor-wrapper")] - (if (<= (.-clientWidth editor) (+ left (if set-default-width? max-width 500))) - {:right 0} - {:left (if (and y-diff (= y-diff 0)) left 0)})))} + :style style} cp])) (rum/defc transition-cp < rum/reactive - [cp set-default-width? pos] - (when pos - (when-let [pos (rum/react pos)] - (ui/css-transition - {:class-names "fade" - :timeout {:enter 500 - :exit 300}} - (absolute-modal cp set-default-width? pos))))) + [cp set-default-width?] + (when-let [pos (:pos (state/sub :editor/action-data))] + (ui/css-transition + {:class-names "fade" + :timeout {:enter 500 + :exit 300}} + (absolute-modal cp set-default-width? pos)))) (rum/defc image-uploader < rum/reactive [id format] @@ -354,8 +406,7 @@ [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1 (ui/loading (util/format "Uploading %s%" (util/format "%2d" processing)))] - false - *slash-caret-pos)))]) + false)))]) (defn- set-up-key-down! [state format] @@ -452,9 +503,9 @@ (let [content (state/sub-edit-content)] (mock-textarea content))) -(defn animated-modal - [key component set-default-width? *pos] - (when *pos +(rum/defc animated-modal < rum/reactive + [key component set-default-width?] + (when-let [pos (:pos (state/get-editor-action-data))] (ui/css-transition {:key key :class-names {:enter "origin-top-left opacity-0 transform scale-95" @@ -466,48 +517,48 @@ (absolute-modal component set-default-width? - *pos))))) + pos))))) (rum/defc modals < rum/reactive "React to atom changes, find and render the correct modal" [id format] - (ui/transition-group - (cond - (and (util/react *show-commands) - (not (state/sub :editor/show-page-search?)) - (not (state/sub :editor/show-block-search?)) - (not (state/sub :editor/show-template-search?)) - (not (state/sub :editor/show-input)) - (not (state/sub :editor/show-zotero)) - (not (state/sub :editor/show-date-picker?))) - (animated-modal "commands" (commands id format) true (util/react *slash-caret-pos)) + (let [action (state/sub :editor/action)] + (cond + (= action :commands) + (animated-modal "commands" (commands id format) true) - (and (util/react *show-block-commands) @*angle-bracket-caret-pos) - (animated-modal "block-commands" (block-commands id format) true (util/react *angle-bracket-caret-pos)) + (= action :block-commands) + (animated-modal "block-commands" (block-commands id format) true) - (state/sub :editor/show-page-search?) - (animated-modal "page-search" (page-search id format) true (util/react *slash-caret-pos)) + (contains? #{:page-search :page-search-hashtag} action) + (animated-modal "page-search" (page-search id format) true) - (state/sub :editor/show-block-search?) - (animated-modal "block-search" (block-search id format) false (util/react *slash-caret-pos)) + (= :block-search action) + (animated-modal "block-search" (block-search id format) true) - (state/sub :editor/show-template-search?) - (animated-modal "template-search" (template-search id format) true (util/react *slash-caret-pos)) + (= :template-search action) + (animated-modal "template-search" (template-search id format) true) - (state/sub :editor/show-date-picker?) - (animated-modal "date-picker" (datetime-comp/date-picker id format nil) false (util/react *slash-caret-pos)) + (= :property-search action) + (animated-modal "property-search" (property-search id) true) - (state/sub :editor/show-input) - (animated-modal "input" (input id - (fn [command m _pos] - (editor-handler/handle-command-input command id format m))) - true (util/react *slash-caret-pos)) + (= :property-value-search action) + (animated-modal "property-value-search" (property-value-search id) true) - (state/sub :editor/show-zotero) - (animated-modal "zotero-search" (zotero/zotero-search id) false (util/react *slash-caret-pos)) + (= :datepicker action) + (animated-modal "date-picker" (datetime-comp/date-picker id format nil) false) - :else - nil))) + (= :input action) + (animated-modal "input" (input id + (fn [command m] + (editor-handler/handle-command-input command id format m))) + true) + + (= :zotero action) + (animated-modal "zotero-search" (zotero/zotero-search id) false) + + :else + nil))) (rum/defcs box < rum/reactive {:init (fn [state] diff --git a/src/main/frontend/components/header.cljs b/src/main/frontend/components/header.cljs index d0e7015766..75e3fb3504 100644 --- a/src/main/frontend/components/header.cljs +++ b/src/main/frontend/components/header.cljs @@ -172,10 +172,10 @@ {:title [:div.flex-row.flex.justify-between.items-center [:span (t :join-community)]] - :options {:href "https://discord.gg/KpN4eHY" - :title (t :discord-title) + :options {:href "https://discuss.logseq.com" + :title (t :discourse-title) :target "_blank"} - :icon (ui/icon "brand-discord")}] + :icon (ui/icon "message-circle")}] (concat page-menu-and-hr) (remove nil?)) {}))) diff --git a/src/main/frontend/components/journal.cljs b/src/main/frontend/components/journal.cljs index 755cf9903f..d26a980c56 100644 --- a/src/main/frontend/components/journal.cljs +++ b/src/main/frontend/components/journal.cljs @@ -56,10 +56,7 @@ (if today? (blocks-cp repo page format) - (ui/lazy-visible (fn [] - (blocks-cp repo page format)) - nil - {})) + (ui/lazy-visible (fn [] (blocks-cp repo page format)))) {}) diff --git a/src/main/frontend/components/onboarding.cljs b/src/main/frontend/components/onboarding.cljs index 08417e3484..a347b85135 100644 --- a/src/main/frontend/components/onboarding.cljs +++ b/src/main/frontend/components/onboarding.cljs @@ -12,9 +12,9 @@ (defn help [] [:div.help.cp__sidebar-help-docs - (let [discord-with-icon [:div.flex-row.inline-flex.items-center - [:span.mr-1 (t :help/community)] - (ui/icon "brand-discord" {:style {:font-size 20}})] + (let [discourse-with-icon [:div.flex-row.inline-flex.items-center + [:span.mr-1 (t :help/forum-community)] + (ui/icon "message-circle" {:style {:font-size 20}})] list [{:title "Usage" :children [[[:a @@ -29,7 +29,7 @@ {:title "Community" :children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"] [(t :help/blog) "https://blog.logseq.com"] - [discord-with-icon "https://discord.gg/KpN4eHY"]]} + [discourse-with-icon "https://discuss.logseq.com"]]} {:title "Development" :children [[(t :help/roadmap) "https://trello.com/b/8txSM12G/roadmap"] diff --git a/src/main/frontend/components/plugins.cljs b/src/main/frontend/components/plugins.cljs index b950221e90..437467afdb 100644 --- a/src/main/frontend/components/plugins.cljs +++ b/src/main/frontend/components/plugins.cljs @@ -855,26 +855,31 @@ "type of :toolbar, :pagebar" [_state type] (when (state/sub [:plugin/installed-ui-items]) - (let [pinned-items (state/sub [:plugin/preferences :pinnedToolbarItems]) + (let [toolbar? (= :toolbar type) + pinned-items (state/sub [:plugin/preferences :pinnedToolbarItems]) pinned-items (and (sequential? pinned-items) (into #{} pinned-items)) items (state/get-plugins-ui-items-with-type type) items (sort-by #(:key (second %)) items)] (when-let [items (and (seq items) - (map #(assoc-in % [1 :pinned?] - (let [[_ {:keys [key]} pid] % - pkey (str (name pid) ":" key)] - (contains? pinned-items pkey))) - items))] + (if toolbar? + (map #(assoc-in % [1 :pinned?] + (let [[_ {:keys [key]} pid] % + pkey (str (name pid) ":" key)] + (contains? pinned-items pkey))) + items) + items))] [:div {:class (str "ui-items-container") :data-type (name type)} (conj (for [[_ {:keys [key pinned?] :as opts} pid] items] - (when (or (not (set? pinned-items)) pinned?) + (when (or (not toolbar?) + (not (set? pinned-items)) pinned?) (rum/with-key (ui-item-renderer pid type opts) key)))) ;; manage plugin buttons - (toolbar-plugins-manager-list items)])))) + (when toolbar? + (toolbar-plugins-manager-list items))])))) (rum/defcs hook-ui-fenced-code < rum/reactive [_state content {:keys [render edit] :as _opts}] diff --git a/src/main/frontend/components/reference.cljs b/src/main/frontend/components/reference.cljs index 79bcf3d28f..fde8f74905 100644 --- a/src/main/frontend/components/reference.cljs +++ b/src/main/frontend/components/reference.cljs @@ -169,9 +169,7 @@ (ui/component-error "Linked References: Unexpected error") (ui/lazy-visible (fn [] - (references* page-name)) - nil - {}))) + (references* page-name))))) (rum/defcs unlinked-references-aux < rum/reactive db-mixins/query diff --git a/src/main/frontend/components/search.cljs b/src/main/frontend/components/search.cljs index 828daff924..c91ccd3a30 100644 --- a/src/main/frontend/components/search.cljs +++ b/src/main/frontend/components/search.cljs @@ -216,7 +216,7 @@ :new-page [:div.text.font-bold (str (t :new-page) ": ") - [:span.ml-1 (str "\"" search-q "\"")]] + [:span.ml-1 (str "\"" (string/trim search-q) "\"")]] :go-to-whiteboard [:div.text.font-bold (str (t :go-to-whiteboard) ": ") diff --git a/src/main/frontend/components/widgets.cljs b/src/main/frontend/components/widgets.cljs index 6a9185725d..dcdfd3df3e 100644 --- a/src/main/frontend/components/widgets.cljs +++ b/src/main/frontend/components/widgets.cljs @@ -39,9 +39,9 @@ [:li.mt-8 [:div.font-bold.mb-2 "I need some help"] - [:p "👋 Join our discord group to chat with the makers and our helpful community members."] + [:p "👋 Join our Forum to chat with the makers and our helpful community members."] (ui/button "Join the community" - :href "https://discord.gg/KpN4eHY" + :href "https://discuss.logseq.com" :target "_blank")]]] [:div.cp__widgets-open-local-directory [:div.select-file-wrap.cursor diff --git a/src/main/frontend/core.cljs b/src/main/frontend/core.cljs index 85b0a4006d..32eef7d450 100644 --- a/src/main/frontend/core.cljs +++ b/src/main/frontend/core.cljs @@ -31,7 +31,7 @@ " Welcome to Logseq! If you encounter any problem, feel free to file an issue on GitHub (https://github.com/logseq/logseq) - or join our Discord server (https://discord.gg/KpN4eHY). + or join our forum (https://discuss.logseq.com). .____ | | ____ ____ ______ ____ ______ | | / _ \\ / ___\\/ ___// __ \\/ ____/ diff --git a/src/main/frontend/db/model.cljs b/src/main/frontend/db/model.cljs index 3c0847e5ff..d0875b3681 100644 --- a/src/main/frontend/db/model.cljs +++ b/src/main/frontend/db/model.cljs @@ -1341,6 +1341,40 @@ [(get m :template) e])) (into {})))) +(defn get-all-properties + [] + (let [properties (d/q + '[:find [?p ...] + :where + [_ :block/properties ?p]] + (conn/get-db)) + properties (remove (fn [m] (empty? m)) properties)] + (->> (map keys properties) + (apply concat) + distinct + (remove #{:id}) + sort))) + +(defn get-property-values + [property] + (let [pred (fn [_db properties] + (get properties property))] + (->> + (d/q + '[:find [?property-val ...] + :in $ ?pred + :where + [_ :block/properties ?p] + [(?pred $ ?p) ?property-val]] + (conn/get-db) + pred) + (map (fn [x] (if (coll? x) x [x]))) + (apply concat) + (map str) + (remove string/blank?) + (distinct) + (sort)))) + (defn get-template-by-name [name] (when (string? name) diff --git a/src/main/frontend/dicts.cljc b/src/main/frontend/dicts.cljc index 65660d11f5..3836e361e3 100644 --- a/src/main/frontend/dicts.cljc +++ b/src/main/frontend/dicts.cljc @@ -26,6 +26,7 @@ :help/privacy "Privacy policy" :help/terms "Terms" :help/community "Discord community" + :help/forum-community "Forum community" :help/awesome-logseq "Awesome Logseq" :help/shortcuts "Keyboard shortcuts" :help/shortcuts-triggers "Triggers" @@ -253,7 +254,8 @@ :import "Import" :join-community "Join the community" :sponsor-us "Sponsor Us" - :discord-title "Our discord group!" + :discourse-title "Our forum!" + :discord-title "Our discord group!" ;; unused :help-shortcut-title "Click to check shortcuts and other tips" :loading "Loading" :cloning "Cloning" @@ -809,11 +811,11 @@ :right-side-bar/favorites "收藏" :right-side-bar/page-graph "页面图谱:" :right-side-bar/block-ref "块引用" - :right-side-bar/graph-view "Graph view" - :right-side-bar/all-pages "All pages" - :right-side-bar/flashcards "Flashcards" - :right-side-bar/new-page "New page" - :left-side-bar/journals "Journals" + :right-side-bar/graph-view "图谱视角" + :right-side-bar/all-pages "全部页面" + :right-side-bar/flashcards "记忆卡片" + :right-side-bar/new-page "新页面" + :left-side-bar/journals "日志" :left-side-bar/new-page "新页面" :left-side-bar/nav-favorites "收藏页面" :left-side-bar/nav-shortcuts "快捷导航" @@ -964,7 +966,7 @@ :all-journals "日记" :publishing "发布" :export "导出" - :all-graphs "所有库" + :all-graphs "所有图谱" :all-pages "所有页面" :all-files "所有文件" :remove-orphaned-pages "删除空页面" @@ -1104,11 +1106,11 @@ :right-side-bar/recent "最近" :right-side-bar/contents "目錄" :right-side-bar/block-ref "塊引用" - :right-side-bar/graph-view "Graph view" - :right-side-bar/all-pages "All pages" - :right-side-bar/flashcards "Flashcards" - :right-side-bar/new-page "New page" - :left-side-bar/journals "Journals" + :right-side-bar/graph-view "圖譜視角" + :right-side-bar/all-pages "全部頁面" + :right-side-bar/flashcards "記憶卡片" + :right-side-bar/new-page "新頁面" + :left-side-bar/journals "日誌" :format/preferred-mode "請選擇偏好格式" :format/markdown "Markdown" :format/org-mode "Org Mode" diff --git a/src/main/frontend/extensions/html_parser.cljs b/src/main/frontend/extensions/html_parser.cljs index 3496764e41..bf010ac916 100644 --- a/src/main/frontend/extensions/html_parser.cljs +++ b/src/main/frontend/extensions/html_parser.cljs @@ -11,7 +11,11 @@ [hiccup] (walk/postwalk (fn [f] (if (map? f) - (dissoc f :style) + (apply dissoc f (conj (filter (fn [key] + (string/starts-with? (str key) ":data-")) + (keys f)) + :style + :class)) f)) hiccup)) (defn- export-hiccup @@ -73,7 +77,7 @@ :else nil) children' (map-join children)] - (when-not (string/blank? children') + (when (not-empty children') (str (if (string? pattern) pattern (apply str pattern)) children' (if (string? pattern) pattern (apply str (reverse pattern))))))) @@ -122,11 +126,15 @@ :org (util/format "[[%s][%s]]" href label) nil)))) :img (let [src (:src attrs) - alt (or (:alt attrs) "")] - (case format - :markdown (util/format "![%s](%s)" alt src) - :org (util/format "[[%s][%s]]" src alt) - nil)) + alt (or (:alt attrs) "") + ;; reject url-encoded and utf8-encoded(svg) + unsafe-data-url? (and (string/starts-with? src "data:") + (not (re-find #"^data:.*?;base64," src)))] + (when-not unsafe-data-url? + (case format + :markdown (util/format "![%s](%s)" alt src) + :org (util/format "[[%s][%s]]" src alt) + nil))) :p (util/format "%s" (map-join children)) @@ -140,12 +148,19 @@ :span} %)) (emphasis-transform tag attrs children) - :code (if @*inside-pre? + :code (cond + @*inside-pre? (map-join children) + + (string? (first children)) (let [pattern (config/get-code format)] (str " " (str pattern (first children) pattern) - " "))) + " ")) + + ;; skip monospace style, since it has more complex children + :else + (map-join children)) :pre (do @@ -170,6 +185,9 @@ :li (str "- " (map-join children)) + :br + "\n" + :dt (case format :org (str "- " (map-join children) " ") @@ -218,7 +236,7 @@ (for [x hiccup] (single-hiccup-transform x)) (single-hiccup-transform hiccup))] - (string/replace (apply str result) #"\n\n+" "\n\n"))) + (apply str result))) (defn hiccup->doc [format hiccup] @@ -226,9 +244,8 @@ (if (string/blank? s) "" (-> s - (string/trim) - (string/replace "\n\n\n\n" "\n\n") - (string/replace "\n\n\n" "\n\n"))))) + string/trim + (string/replace #"\n\n+" "\n\n"))))) (defn html-decode-hiccup [hiccup] diff --git a/src/main/frontend/extensions/srs.cljs b/src/main/frontend/extensions/srs.cljs index 832b4980de..6e57230d8d 100644 --- a/src/main/frontend/extensions/srs.cljs +++ b/src/main/frontend/extensions/srs.cljs @@ -254,6 +254,10 @@ :or {use-cache? true}}] (when (string? query-string) (let [query-string (template/resolve-dynamic-template! query-string) + query-string (if (and (not (string/starts-with? query-string "(")) + (not (string/starts-with? query-string "["))) + (util/format "[[%s]]" (string/trim query-string)) + query-string) {:keys [query sort-by rules]} (query-dsl/parse query-string) query* (concat [['?b :block/refs '?bp] ['?bp :block/name card-hash-tag]] (if (coll? (first query)) @@ -395,14 +399,15 @@ [:p.p-2 "Congrats, you've reviewed all the cards for this query, see you next time! 💯"]) (defn- btn-with-shortcut [{:keys [shortcut id btn-text background on-click]}] - (ui/button [:span btn-text " " (ui/render-keyboard-shortcut shortcut)] - :id id - :background background - :on-click (fn [e] - (when-let [elem (gobj/get e "target")] - (js/console.log (.-classList elem)) - (.add (.-classList elem) "opacity-25")) - (js/setTimeout #(on-click) 10)))) + (ui/button + [:span btn-text " " (ui/render-keyboard-shortcut shortcut)] + :id id + :class id + :background background + :on-click (fn [e] + (when-let [elem (gobj/get e "target")] + (.add (.-classList elem) "opacity-25")) + (js/setTimeout #(on-click) 10)))) (rum/defcs view < rum/reactive @@ -419,7 +424,8 @@ modal? :modal? cb :callback} card-index] - (let [cards (map ->card blocks) + (let [blocks (if (fn? blocks) (blocks) blocks) + cards (map ->card blocks) review-records (::review-records state) ;; TODO: needs refactor card (if preview? @@ -433,7 +439,10 @@ root-block-id (:block/uuid root-block)] [:div.ls-card.content {:class (when (or preview? modal?) - (util/hiccup->class ".flex.flex-col.resize.overflow-y-auto"))} + (str (util/hiccup->class ".flex.flex-col.resize.overflow-y-auto") + (when modal? " modal-cards"))) + :on-mouse-down (fn [e] + (util/stop e))} (let [repo (state/get-current-repo)] [:div {:style {:margin-top 20}} (component-block/breadcrumb {} repo root-block-id {})]) @@ -446,20 +455,20 @@ (if (or preview? modal?) [:div.flex.my-4.justify-between (when-not (and (not preview?) (= next-phase 1)) - (ui/button [:span (case next-phase - 1 "Hide answers" - 2 "Show answers" - 3 "Show clozes") - (ui/render-keyboard-shortcut [:s])] - :id "card-answers" - :class "mr-2" - :on-click #(reset! phase next-phase))) - + (ui/button + [:span (case next-phase + 1 "Hide answers" + 2 "Show answers" + 3 "Show clozes") + (ui/render-keyboard-shortcut [:s])] + :class "mr-2 card-answers" + :on-click #(reset! phase next-phase))) (when (and (> (count cards) 1) preview?) (ui/button [:span "Next " (ui/render-keyboard-shortcut [:n])] - :id "card-next" - :class "mr-2" - :on-click #(skip-card card card-index cards phase review-records cb))) + :class "mr-2 card-next" + :on-click (fn [e] + (util/stop e) + (skip-card card card-index cards phase review-records cb)))) (when (and (not preview?) (= 1 next-phase)) [:<> @@ -489,9 +498,11 @@ :class "tippy-hover" :interactive true} (ui/button [:span "Reset"] - :id "card-reset" - :class (util/hiccup->class "opacity-60.hover:opacity-100") - :on-click #(operation-reset! card))))] + :id "card-reset" + :class (util/hiccup->class "opacity-60.hover:opacity-100.card-reset") + :on-click (fn [e] + (util/stop e) + (operation-reset! card)))))] [:div.my-3 (ui/button "Review cards" :small? true)])])))) (rum/defc view-modal < @@ -504,9 +515,10 @@ blocks (if (:random-mode? option) (shuffle blocks) blocks)] - (if (seq blocks) - (view blocks option card-index) - review-finished))) + [:div#cards-modal + (if (seq blocks) + (view blocks option card-index) + review-finished)])) (rum/defc preview-cp [block-id] @@ -639,28 +651,26 @@ :font-weight 600} @*random-mode? (assoc :color "orange"))})])]] - (if (seq review-cards) + (if (or @*preview-mode? (seq review-cards)) [:div.px-1 - (when-not modal? + (when (and (not modal?) (not @*preview-mode?)) {:on-click (fn [] - (let [blocks-f (if @*preview-mode? - (fn [] (query repo query-string)) - (fn [] - (let [query-result (query repo query-string)] - (:result (query-scheduled repo query-result (tl/local-now))))))] + (let [blocks-f (fn [] + (let [query-result (query repo query-string)] + (:result (query-scheduled repo query-result (tl/local-now)))))] (state/set-modal! #(view-modal blocks-f {:modal? true :random-mode? *random-mode? - :preview? @*preview-mode? + :preview? false :callback callback-fn} *card-index) {:id :srs})))}) (let [view-fn (if modal? view-modal view) - blocks-fn (if @*preview-mode? - (fn [] (query repo query-string)) - review-cards)] - (view-fn blocks-fn + blocks (if @*preview-mode? + (query repo query-string) + review-cards)] + (view-fn blocks (merge config {:global? global? :random-mode? @*random-mode? diff --git a/src/main/frontend/extensions/srs/handler.cljs b/src/main/frontend/extensions/srs/handler.cljs index 28e017e093..95b3d3dda0 100644 --- a/src/main/frontend/extensions/srs/handler.cljs +++ b/src/main/frontend/extensions/srs/handler.cljs @@ -1,9 +1,11 @@ -(ns frontend.extensions.srs.handler) +(ns frontend.extensions.srs.handler + (:require [dommy.core :refer-macros [sel]])) (defn click [id] - (when-let [node (js/document.getElementById id)] - (.click node))) + (let [nodes (sel [:#cards-modal (str "." id)])] + (doseq [node nodes] + (.click node)))) (defn toggle-answers [] (click "card-answers")) diff --git a/src/main/frontend/extensions/zotero/handler.cljs b/src/main/frontend/extensions/zotero/handler.cljs index 82a1818d2d..22742790d8 100644 --- a/src/main/frontend/extensions/zotero/handler.cljs +++ b/src/main/frontend/extensions/zotero/handler.cljs @@ -41,7 +41,7 @@ (defn handle-command-zotero [id page-name] - (state/set-editor-show-zotero! false) + (state/clear-editor-action!) (editor-handler/insert-command! id (str "[[" page-name "]]") nil {})) (defn- create-abstract-note! diff --git a/src/main/frontend/fs/watcher_handler.cljs b/src/main/frontend/fs/watcher_handler.cljs index 0f354071d2..b27f2b4a5a 100644 --- a/src/main/frontend/fs/watcher_handler.cljs +++ b/src/main/frontend/fs/watcher_handler.cljs @@ -90,7 +90,7 @@ (when dir-exists? (when-let [page-name (db/get-file-page path)] (println "Delete page: " page-name ", file path: " path ".") - (page-handler/delete! page-name #() :unlink-file? true)))) + (page-handler/delete! page-name #() :delete-file? false)))) (and (contains? #{"add" "change" "unlink"} type) (string/ends-with? path "logseq/custom.css")) diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index 69769c1b90..bffd7a4e53 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -4,10 +4,7 @@ [clojure.string :as string] [clojure.walk :as w] [dommy.core :as dom] - [frontend.commands :as commands - :refer [*angle-bracket-caret-pos - *show-block-commands *show-commands - *slash-caret-pos]] + [frontend.commands :as commands] [frontend.config :as config] [frontend.date :as date] [frontend.db :as db] @@ -525,8 +522,7 @@ (defn clear-when-saved! [] - (state/clear-editor-show-state!) - (commands/restore-state true)) + (commands/restore-state)) (defn get-state [] @@ -664,7 +660,7 @@ (defn properties-block [properties format page] (let [content (property/insert-properties format "" properties) - refs (gp-block/get-page-refs-from-properties properties + refs (gp-block/get-page-refs-from-properties format properties (db/get-db (state/get-current-repo)) (state/get-date-formatter))] {:block/pre-block? true @@ -1292,14 +1288,7 @@ ;; non English input method (when-not (state/editor-in-composition?) (when (state/get-current-repo) - (when (and (not @commands/*show-commands) - (not @commands/*show-block-commands) - (not (state/get-editor-show-page-search?)) - (not (state/get-editor-show-page-search-hashtag?)) - (not (state/get-editor-show-block-search?)) - (not (state/get-editor-show-date-picker?)) - (not (state/get-editor-show-template-search?)) - (not (state/get-editor-show-input))) + (when-not (state/get-editor-action) (try (let [input-id (state/get-edit-input-id) block (state/get-edit-block) @@ -1357,13 +1346,7 @@ nil) (when restore? - (let [restore-slash-caret-pos? (if (and - (seq? command-output) - (= :editor/click-hidden-file-input - (ffirst command-output))) - false - true)] - (commands/restore-state restore-slash-caret-pos?)))) + (commands/restore-state))) (defn get-asset-file-link [format url file-name image?] @@ -1570,12 +1553,12 @@ "[[" (do (commands/handle-step [:editor/search-page]) - (reset! commands/*slash-caret-pos (cursor/get-caret-pos input))) + (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})) "((" (do (commands/handle-step [:editor/search-block :reference]) - (reset! commands/*slash-caret-pos (cursor/get-caret-pos input))) + (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})) nil))))) @@ -1626,12 +1609,20 @@ [q] (search/template-search q)) +(defn get-matched-properties + [q] + (search/property-search q)) + +(defn get-matched-property-values + [property q] + (search/property-value-search property q)) + (defn get-matched-commands [input] (try (let [edit-content (or (gobj/get input "value") "") pos (cursor/pos input) - last-slash-caret-pos (:pos @*slash-caret-pos) + last-slash-caret-pos (:pos (:pos (state/get-editor-action-data))) last-command (and last-slash-caret-pos (subs edit-content last-slash-caret-pos pos))] (when (> pos 0) (or @@ -1649,7 +1640,7 @@ (let [edit-content (gobj/get input "value") pos (cursor/pos input) last-command (subs edit-content - (:pos @*angle-bracket-caret-pos) + (:pos (:pos (state/get-editor-action-data))) pos)] (when (> pos 0) (or @@ -1664,14 +1655,8 @@ (defn auto-complete? [] - (or @*show-commands - @*show-block-commands - @*asset-uploading? - (state/get-editor-show-input) - (state/get-editor-show-page-search?) - (state/get-editor-show-block-search?) - (state/get-editor-show-template-search?) - (state/get-editor-show-date-picker?))) + (or @*asset-uploading? + (state/get-editor-action))) (defn get-current-input-char [input] @@ -1785,9 +1770,7 @@ (defn close-autocomplete-if-outside [input] (when (and input - (or (state/get-editor-show-page-search?) - (state/get-editor-show-page-search-hashtag?) - (state/get-editor-show-block-search?)) + (state/get-editor-action) (not (wrapped-by? input "[[" "]]"))) (when (get-search-q) (let [value (gobj/get input "value") @@ -1800,9 +1783,7 @@ (string/includes? between "]") (string/includes? between "(") (string/includes? between ")"))) - (state/set-editor-show-block-search! false) - (state/set-editor-show-page-search! false) - (state/set-editor-show-page-search-hashtag! false)))))) + (state/clear-editor-action!)))))) (defn resize-image! [block-id metadata full_text size] @@ -1842,25 +1823,51 @@ (defn handle-last-input [] (let [input (state/get-input) pos (cursor/pos input) - last-input-char (util/nth-safe (.-value input) (dec pos))] + last-input-char (util/nth-safe (.-value input) (dec pos)) + last-prev-input-char (util/nth-safe (.-value input) (dec (dec pos))) + prev-prev-input-char (util/nth-safe (.-value input) (- pos 3))] ;; TODO: is it cross-browser compatible? ;; (not= (gobj/get native-e "inputType") "insertFromPaste") - (when (= last-input-char (state/get-editor-command-trigger)) - (when (seq (get-matched-commands input)) - (reset! commands/*slash-caret-pos (cursor/get-caret-pos input)) - (reset! commands/*show-commands true))) + (cond + (= last-input-char (state/get-editor-command-trigger)) + (do + (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}) + (commands/reinit-matched-commands!) + (state/set-editor-show-commands!)) - (if (= last-input-char commands/angle-bracket) - (when (seq (get-matched-block-commands input)) - (reset! commands/*angle-bracket-caret-pos (cursor/get-caret-pos input)) - (reset! commands/*show-block-commands true)) + (= last-input-char commands/angle-bracket) + (do + (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}) + (commands/reinit-matched-block-commands!) + (state/set-editor-show-block-commands!)) + + (and (= last-input-char last-prev-input-char commands/colon) + (or (nil? prev-prev-input-char) + (= prev-prev-input-char "\n"))) + (do + (cursor/move-cursor-backward input 2) + (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}) + (state/set-editor-action! :property-search)) + + (and + (not= :property-search (state/get-editor-action)) + (or (wrapped-by? input "" "::") + (wrapped-by? input "\n" "::"))) + (do + (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}) + (state/set-editor-action! :property-search)) + + (and (= last-input-char commands/colon) (= :property-search (state/get-editor-action))) + (state/clear-editor-action!) + + :else nil))) (defn block-on-chosen-handler [_input id q format] (fn [chosen _click?] - (state/set-editor-show-block-search! false) + (state/clear-editor-action!) (let [uuid-string (str (:block/uuid chosen))] ;; block reference @@ -1883,7 +1890,7 @@ (defn block-non-exist-handler [input] (fn [] - (state/set-editor-show-block-search! false) + (state/clear-editor-action!) (cursor/move-cursor-forward input 2))) (defn- paste-block-cleanup @@ -2053,6 +2060,44 @@ (insert-template! element-id db-id {:replace-empty-target? true}))) +(defn get-searching-property + [input] + (let [value (.-value input) + pos (util/get-selection-start input) + postfix (subs value pos) + end-index (when-let [idx (string/index-of postfix "::")] + (+ (max 0 (count (subs value 0 pos))) idx)) + start-index (or (when-let [p (string/last-index-of (subs value 0 pos) "\n")] + (inc p)) + 0)] + {:end-index end-index + :searching-property (when (and start-index end-index (>= end-index start-index)) + (subs value start-index end-index))})) + +(defn property-on-chosen-handler + [element-id q] + (fn [property] + (when-let [input (gdom/getElement element-id)] + (let [{:keys [end-index searching-property]} (get-searching-property input)] + (cursor/move-cursor-to input (+ end-index 2)) + (commands/insert! element-id (str (or property q) ":: ") + {:last-pattern (str searching-property "::")}) + (state/clear-editor-action!) + (js/setTimeout (fn [] + (let [pos (let [input (gdom/getElement element-id)] + (cursor/get-caret-pos input))] + (state/set-editor-action-data! {:property (or property q) + :pos pos}) + (state/set-editor-action! :property-value-search))) + 50))))) + +(defn property-value-on-chosen-handler + [element-id q] + (fn [property-value] + (commands/insert! element-id (str ":: " (or property-value q)) + {:last-pattern (str ":: " q)}) + (state/clear-editor-action!))) + (defn parent-is-page? [{{:block/keys [parent page]} :data :as node}] {:pre [(tree/satisfied-inode? node)]} @@ -2252,9 +2297,9 @@ parent-id bounds {:backward-pos backward-pos :check-fn (fn [_ _ _] - (reset! commands/*slash-caret-pos new-pos) + (state/set-editor-action-data! {:pos new-pos}) (commands/handle-step [:editor/search-page]))}))] - (state/set-editor-show-page-search! false) + (state/clear-editor-action!) (let [selection (get-selection-and-format) {:keys [selection-start selection-end selection]} selection] (if selection @@ -2285,9 +2330,9 @@ parent-id bounds {:backward-pos backward-pos :check-fn (fn [_ _ _] - (reset! commands/*slash-caret-pos new-pos) + (state/set-editor-action-data! {:pos new-pos}) (commands/handle-step [:editor/search-block]))}))] - (state/set-editor-show-block-search! false) + (state/clear-editor-action!) (if-let [embed-ref (thingatpt/embed-macro-at-point input)] (let [{:keys [raw-content start end]} embed-ref] (delete-and-update input start end) @@ -2568,16 +2613,14 @@ (= (util/nth-safe value (dec current-pos)) (state/get-editor-command-trigger))) (do (util/stop e) - (reset! *slash-caret-pos nil) - (reset! *show-commands false) + (commands/restore-state) (delete-and-update input (dec current-pos) current-pos)) (and (> current-pos 1) (= (util/nth-safe value (dec current-pos)) commands/angle-bracket)) (do (util/stop e) - (reset! *angle-bracket-caret-pos nil) - (reset! *show-block-commands false) + (commands/restore-state) (delete-and-update input (dec current-pos) current-pos)) ;; pair @@ -2595,10 +2638,10 @@ (commands/delete-pair! id) (cond (and (= deleted "[") (state/get-editor-show-page-search?)) - (state/set-editor-show-page-search! false) + (state/clear-editor-action!) (and (= deleted "(") (state/get-editor-show-block-search?)) - (state/set-editor-show-block-search! false) + (state/clear-editor-action!) :else nil)) @@ -2606,7 +2649,7 @@ ;; deleting hashtag (and (= deleted "#") (state/get-editor-show-page-search-hashtag?)) (do - (state/set-editor-show-page-search-hashtag! false) + (state/clear-editor-action!) (delete-and-update input (dec current-pos) current-pos)) ;; just delete @@ -2635,9 +2678,7 @@ (fn [e] (cond (state/editing?) - (when (and (not (state/get-editor-show-input)) - (not (state/get-editor-show-date-picker?)) - (not (state/get-editor-show-template-search?))) + (when-not (state/get-editor-action) (util/stop e) (indent-outdent (not (= :left direction)))) @@ -2688,7 +2729,7 @@ (and (= key "#") (and (> pos 0) (= "#" (util/nth-safe value (dec pos))))) - (state/set-editor-show-page-search-hashtag! false) + (state/clear-editor-action!) (and (contains? (set/difference (set (keys reversed-autopair-map)) #{"`"}) @@ -2720,7 +2761,7 @@ (if (= key "#") (state/set-editor-last-pos! (inc (cursor/pos input))) ;; In keydown handler, the `#` is not inserted yet. (state/set-editor-last-pos! (cursor/pos input))) - (reset! commands/*slash-caret-pos (cursor/get-caret-pos input))) + (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})) (let [sym "$"] (and (= key sym) @@ -2759,92 +2800,92 @@ blank-selected? (string/blank? (util/get-selected-text)) is-processed? (util/event-is-composing? e true) ;; #3440 non-enter-processed? (and is-processed? ;; #3251 - (not= code keycode/enter-code))] ;; #3459 - (when-not (or (state/get-editor-show-input) non-enter-processed?) - (cond - (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp"} k)) - (not (:editor/show-page-search? @state/state)) - (not (:editor/show-page-search-hashtag? @state/state)) - (wrapped-by? input "[[" "]]")) - (let [orig-pos (cursor/get-caret-pos input) - value (gobj/get input "value") - square-pos (string/last-index-of (subs value 0 (:pos orig-pos)) "[[") - pos (+ square-pos 2) - _ (state/set-editor-last-pos! pos) - pos (assoc orig-pos :pos pos) - command-step (if (= \# (util/nth-safe value (dec square-pos))) - :editor/search-page-hashtag - :editor/search-page)] - (commands/handle-step [command-step]) - (reset! commands/*slash-caret-pos pos)) + (not= code keycode/enter-code)) ;; #3459 + editor-action (state/get-editor-action)] + (cond + (and (= :commands (state/get-editor-action)) (not= k (state/get-editor-command-trigger))) + (let [matched-commands (get-matched-commands input)] + (if (seq matched-commands) + (reset! commands/*matched-commands matched-commands) + (state/clear-editor-action!))) - (and blank-selected? - (contains? keycode/left-square-brackets-keys k) - (= (:key last-key-code) k) - (> current-pos 0) - (not (wrapped-by? input "[[" "]]"))) - (do - (commands/handle-step [:editor/input "[[]]" {:backward-truncate-number 2 - :backward-pos 2}]) - (commands/handle-step [:editor/search-page]) - (reset! commands/*slash-caret-pos (cursor/get-caret-pos input))) - - (and blank-selected? - (contains? keycode/left-paren-keys k) - (= (:key last-key-code) k) - (> current-pos 0) - (not (wrapped-by? input "((" "))"))) - (do - (commands/handle-step [:editor/input "(())" {:backward-truncate-number 2 - :backward-pos 2}]) - (commands/handle-step [:editor/search-block :reference]) - (reset! commands/*slash-caret-pos (cursor/get-caret-pos input))) - - (and (= "〈" c) - (= "《" (util/nth-safe value (dec (dec current-pos)))) - (> current-pos 0)) - (do - (commands/handle-step [:editor/input commands/angle-bracket {:last-pattern "《〈" - :backward-pos 0}]) - (reset! commands/*angle-bracket-caret-pos (cursor/get-caret-pos input)) - (reset! commands/*show-block-commands true)) - - (and (= c " ") - (or (= (util/nth-safe value (dec (dec current-pos))) "#") - (not (state/get-editor-show-page-search?)) - (and (state/get-editor-show-page-search?) - (not= (util/nth-safe value current-pos) "]")))) - (state/set-editor-show-page-search-hashtag! false) - - (and @*show-commands (not= k (state/get-editor-command-trigger))) - (let [matched-commands (get-matched-commands input)] - (if (seq matched-commands) + (and (= :block-commands editor-action) (not= key-code 188)) ; not < + (let [matched-block-commands (get-matched-block-commands input)] + (if (seq matched-block-commands) + (cond + (= key-code 9) ;tab (do - (reset! *show-commands true) - (reset! commands/*matched-commands matched-commands)) - (reset! *show-commands false))) + (util/stop e) + (insert-command! input-id + (last (first matched-block-commands)) + format + {:last-pattern commands/angle-bracket})) - (and @*show-block-commands (not= key-code 188)) ; not < - (let [matched-block-commands (get-matched-block-commands input)] - (if (seq matched-block-commands) - (cond - (= key-code 9) ;tab - (when @*show-block-commands - (util/stop e) - (insert-command! input-id - (last (first matched-block-commands)) - format - {:last-pattern commands/angle-bracket})) + :else + (reset! commands/*matched-block-commands matched-block-commands)) + (state/clear-editor-action!))) - :else - (reset! commands/*matched-block-commands matched-block-commands)) - (reset! *show-block-commands false))) + (and (contains? #{:commands :block-commands} (state/get-editor-action)) + (= c (util/nth-safe value (dec (dec current-pos))) " ")) + (state/clear-editor-action!) - (nil? @search-timeout) - (close-autocomplete-if-outside input) + (and (state/get-editor-show-page-search-hashtag?) + (= c " ")) + (state/clear-editor-action!) - :else - nil)) + :else + (when (and (not editor-action) (not non-enter-processed?)) + (cond + (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp"} k)) + (wrapped-by? input "[[" "]]")) + (let [orig-pos (cursor/get-caret-pos input) + value (gobj/get input "value") + square-pos (string/last-index-of (subs value 0 (:pos orig-pos)) "[[") + pos (+ square-pos 2) + _ (state/set-editor-last-pos! pos) + pos (assoc orig-pos :pos pos) + command-step (if (= \# (util/nth-safe value (dec square-pos))) + :editor/search-page-hashtag + :editor/search-page)] + (commands/handle-step [command-step]) + (state/set-editor-action-data! {:pos pos})) + + (and blank-selected? + (contains? keycode/left-square-brackets-keys k) + (= (:key last-key-code) k) + (> current-pos 0) + (not (wrapped-by? input "[[" "]]"))) + (do + (commands/handle-step [:editor/input "[[]]" {:backward-truncate-number 2 + :backward-pos 2}]) + (commands/handle-step [:editor/search-page]) + (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})) + + (and blank-selected? + (contains? keycode/left-paren-keys k) + (= (:key last-key-code) k) + (> current-pos 0) + (not (wrapped-by? input "((" "))"))) + (do + (commands/handle-step [:editor/input "(())" {:backward-truncate-number 2 + :backward-pos 2}]) + (commands/handle-step [:editor/search-block :reference]) + (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})) + + (and (= "〈" c) + (= "《" (util/nth-safe value (dec (dec current-pos)))) + (> current-pos 0)) + (do + (commands/handle-step [:editor/input commands/angle-bracket {:last-pattern "《〈" + :backward-pos 0}]) + (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}) + (state/set-editor-show-block-commands!)) + + (nil? @search-timeout) + (close-autocomplete-if-outside input) + + :else + nil))) (when-not (or (= k "Shift") is-processed?) (state/set-last-key-code! {:key-code key-code :code code @@ -2861,7 +2902,7 @@ (defn editor-on-change! [block id search-timeout] (fn [e] - (if (state/sub :editor/show-block-search?) + (if (= :block-search (state/sub :editor/action)) (let [timeout 300] (when @search-timeout (js/clearTimeout @search-timeout)) @@ -2952,14 +2993,18 @@ "shortcut cut action: * when in selection mode, cut selected blocks * when in edit mode with text selected, cut selected text - * otherwise same as delete shortcut" + * otherwise nothing need to be handled." [e] (cond (state/selection?) (shortcut-cut-selection e) - (state/editing?) - (keydown-backspace-handler true e))) + (and (state/editing?) (util/input-text-selected? + (gdom/getElement (state/get-edit-input-id)))) + (keydown-backspace-handler true e) + + :else + nil)) (defn delete-selection [e] diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index 7962d48aab..7ab0add047 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -207,7 +207,8 @@ (set block-properties) (set all-properties)) shown-properties (set/intersection (set all-properties) shown-properties)] - (state/set-modal! (query-properties-settings block shown-properties all-properties)))) + (state/set-modal! (query-properties-settings block shown-properties all-properties) + {:center? true}))) (defmethod handle :modal/show-cards [_] (state/set-modal! srs/global-cards {:id :srs diff --git a/src/main/frontend/handler/page.cljs b/src/main/frontend/handler/page.cljs index 89009f2021..e75f234cd0 100644 --- a/src/main/frontend/handler/page.cljs +++ b/src/main/frontend/handler/page.cljs @@ -74,7 +74,7 @@ (let [p (common-handler/get-page-default-properties title) ps (merge p properties) content (page-property/insert-properties format "" ps) - refs (gp-block/get-page-refs-from-properties properties + refs (gp-block/get-page-refs-from-properties format properties (db/get-db (state/get-current-repo)) (state/get-date-formatter))] {:block/uuid (db/new-block-id) @@ -243,13 +243,21 @@ (util/replace-ignore-case (str " " old-tag " ") (str " " new-tag " ")) (util/replace-ignore-case (str " " old-tag "$") (str " " new-tag))))) +(defn- replace-property-ref! + [content old-name new-name] + (let [new-name (keyword (string/replace (string/lower-case new-name) #"\s+" "-")) + old-property (str old-name "::") + new-property (str (name new-name) "::")] + (util/replace-ignore-case content old-property new-property))) + (defn- replace-old-page! "Unsanitized names" [content old-name new-name] (when (and (string? content) (string? old-name) (string? new-name)) (-> content (replace-page-ref! old-name new-name) - (replace-tag-ref! old-name new-name)))) + (replace-tag-ref! old-name new-name) + (replace-property-ref! old-name new-name)))) (defn- walk-replace-old-page! "Unsanitized names" @@ -268,6 +276,9 @@ new-name (replace-old-page! f old-name new-name)) + (and (keyword f) (= (name f) old-name)) + (keyword (string/replace (string/lower-case new-name) #"\s+" "-")) + :else f)) form)) @@ -312,6 +323,7 @@ (defn delete! [page-name ok-handler & {:keys [delete-file?] :or {delete-file? true}}] + (route-handler/redirect-to-home!) (when page-name (when-let [repo (state/get-current-repo)] (let [page-name (util/page-name-sanity-lc page-name) @@ -371,6 +383,7 @@ {:block/uuid uuid :block/content content :block/properties properties + :block/properties-order (map first properties) :block/refs (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page)) :block/path-refs (rename-update-block-refs! (:block/path-refs block) (:db/id page) (:db/id to-page))})))) blocks) (remove nil?))] @@ -478,7 +491,11 @@ pages (cons page pages)] (doseq [{:block/keys [name original-name]} pages] (let [old-page-title (or original-name name) - new-page-title (string/replace old-page-title old-name new-name) + ;; only replace one time, for the case that the namespace is a sub-string of the sub-namespace page name + ;; Example: has pages [[work]] [[work/worklog]], + ;; we want to rename [[work/worklog]] to [[work1/worklog]] when rename [[work]] to [[work1]], + ;; but don't rename [[work/worklog]] to [[work1/work1log]] + new-page-title (string/replace-first old-page-title old-name new-name) redirect? (= name (:block/name page))] (when (and old-page-title new-page-title) (p/let [_ (rename-page-aux old-page-title new-page-title redirect?)] @@ -555,7 +572,8 @@ (rename-namespace-pages! repo old-name new-name)) (rename-nested-pages old-name new-name)) (when (string/blank? new-name) - (notification/show! "Please use a valid name, empty name is not allowed!" :error))))) + (notification/show! "Please use a valid name, empty name is not allowed!" :error))) + (ui-handler/re-render-root!))) (defn- split-col-by-element [col element] @@ -668,7 +686,7 @@ ;; Editor (defn page-not-exists-handler [input id q current-pos] - (state/set-editor-show-page-search! false) + (state/clear-editor-action!) (if (state/org-mode-file-link? (state/get-current-repo)) (let [page-ref-text (get-page-ref-text q) value (gobj/get input "value") @@ -689,15 +707,17 @@ [input id _q pos format] (let [current-pos (cursor/pos input) edit-content (state/sub [:editor/content id]) + action (state/get-editor-action) + hashtag? (= action :page-search-hashtag) q (or @editor-handler/*selected-text - (when (state/sub :editor/show-page-search-hashtag?) + (when hashtag? (gp-util/safe-subs edit-content pos current-pos)) (when (> (count edit-content) current-pos) (gp-util/safe-subs edit-content pos current-pos)))] - (if (state/sub :editor/show-page-search-hashtag?) + (if hashtag? (fn [chosen _click?] - (state/set-editor-show-page-search! false) + (state/clear-editor-action!) (let [wrapped? (= "[[" (gp-util/safe-subs edit-content (- pos 2) pos)) chosen (if (string/starts-with? chosen "New page: ") ;; FIXME: What if a page named "New page: XXX"? (subs chosen 10) @@ -719,7 +739,7 @@ :end-pattern (when wrapped? "]]") :forward-pos forward-pos}))) (fn [chosen _click?] - (state/set-editor-show-page-search! false) + (state/clear-editor-action!) (let [chosen (if (string/starts-with? chosen "New page: ") (subs chosen 10) chosen) diff --git a/src/main/frontend/search.cljs b/src/main/frontend/search.cljs index fa7de22b05..affdcf1dcf 100644 --- a/src/main/frontend/search.cljs +++ b/src/main/frontend/search.cljs @@ -3,6 +3,7 @@ [clojure.string :as string] [logseq.graph-parser.config :as gp-config] [frontend.db :as db] + [frontend.db.model :as db-model] [frontend.regex :as regex] [frontend.search.browser :as search-browser] [frontend.search.db :as search-db :refer [indices]] @@ -155,11 +156,38 @@ ([q] (template-search q 10)) ([q limit] - (let [q (clean-str q) - templates (db/get-all-templates)] - (when (seq templates) - (let [result (fuzzy-search (keys templates) q :limit limit)] - (vec (select-keys templates result))))))) + (when q + (let [q (clean-str q) + templates (db/get-all-templates)] + (when (seq templates) + (let [result (fuzzy-search (keys templates) q :limit limit)] + (vec (select-keys templates result)))))))) + +(defn property-search + ([q] + (property-search q 10)) + ([q limit] + (when q + (let [q (clean-str q) + properties (map name (db-model/get-all-properties))] + (when (seq properties) + (if (string/blank? q) + properties + (let [result (fuzzy-search properties q :limit limit)] + (vec result)))))))) + +(defn property-value-search + ([property q] + (property-value-search property q 10)) + ([property q limit] + (when q + (let [q (clean-str q) + result (db-model/get-property-values (keyword property))] + (when (seq result) + (if (string/blank? q) + result + (let [result (fuzzy-search result q :limit limit)] + (vec result)))))))) (defn sync-search-indice! [repo tx-report] diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index 4c9d846114..2dc36033e4 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -97,12 +97,9 @@ :config {} :block/component-editing-mode? false :editor/draw-mode? false - :editor/show-page-search? false - :editor/show-page-search-hashtag? false - :editor/show-date-picker? false + :editor/action nil + :editor/action-data nil ;; With label or other data - :editor/show-input nil - :editor/show-zotero false :editor/last-saved-cursor nil :editor/editing? nil :editor/in-composition? false @@ -598,63 +595,58 @@ [value] (set-state! :search/mode value)) -(defn set-editor-show-page-search! +(defn set-editor-action! [value] - (set-state! :editor/show-page-search? value)) + (set-state! :editor/action value)) + +(defn set-editor-action-data! + [value] + (set-state! :editor/action-data value)) + +(defn get-editor-action + [] + (:editor/action @state)) + +(defn get-editor-action-data + [] + (:editor/action-data @state)) (defn get-editor-show-page-search? [] - (get @state :editor/show-page-search?)) + (= (get-editor-action) :page-search)) -(defn set-editor-show-page-search-hashtag! - [value] - (set-state! :editor/show-page-search? value) - (set-state! :editor/show-page-search-hashtag? value)) (defn get-editor-show-page-search-hashtag? [] - (get @state :editor/show-page-search-hashtag?)) -(defn set-editor-show-block-search! - [value] - (set-state! :editor/show-block-search? value)) + (= (get-editor-action) :page-search-hashtag)) + (defn get-editor-show-block-search? [] - (get @state :editor/show-block-search?)) -(defn set-editor-show-template-search! - [value] - (set-state! :editor/show-template-search? value)) -(defn get-editor-show-template-search? - [] - (get @state :editor/show-template-search?)) -(defn set-editor-show-date-picker! - [value] - (set-state! :editor/show-date-picker? value)) -(defn get-editor-show-date-picker? - [] - (get @state :editor/show-date-picker?)) + (= (get-editor-action) :block-search)) + (defn set-editor-show-input! [value] - (set-state! :editor/show-input value)) + (if value + (do + (set-editor-action-data! (assoc (get-editor-action-data) :options value)) + (set-editor-action! :input)) + (do + (set-editor-action! nil) + (set-editor-action-data! nil)))) (defn get-editor-show-input [] - (get @state :editor/show-input)) + (when (= (get-editor-action) :input) + (get @state :editor/action-data))) +(defn set-editor-show-commands! + [] + (when-not (get-editor-action) (set-editor-action! :commands))) +(defn set-editor-show-block-commands! + [] + (when-not (get-editor-action) (set-editor-action! :block-commands))) - -(defn set-editor-show-zotero! - [value] - (set-state! :editor/show-zotero value)) - -;; TODO: refactor, use one state -(defn clear-editor-show-state! +(defn clear-editor-action! [] (swap! state (fn [state] - (assoc state - :editor/show-input nil - :editor/show-zotero false - :editor/show-date-picker? false - :editor/show-block-search? false - :editor/show-template-search? false - :editor/show-page-search? false - :editor/show-page-search-hashtag? false)))) + (assoc state :editor/action nil)))) (defn set-edit-input-id! [input-id] diff --git a/src/main/frontend/ui.cljs b/src/main/frontend/ui.cljs index be8236f7c7..f9f42c6bfc 100644 --- a/src/main/frontend/ui.cljs +++ b/src/main/frontend/ui.cljs @@ -25,7 +25,7 @@ ["react-tippy" :as react-tippy] ["react-transition-group" :refer [CSSTransition TransitionGroup]] ["@logseq/react-tweet-embed" :as react-tweet-embed] - ["react-visibility-sensor" :as rvs] + ["react-intersection-observer" :as react-intersection-observer] [rum.core :as rum] [frontend.db-mixins :as db-mixins] [frontend.mobile.util :as mobile-util] @@ -38,7 +38,7 @@ (def resize-consumer (r/adapt-class (gobj/get Resize "ResizeConsumer"))) (def Tippy (r/adapt-class (gobj/get react-tippy "Tooltip"))) (def ReactTweetEmbed (r/adapt-class react-tweet-embed)) -(def visibility-sensor (r/adapt-class (gobj/get rvs "default"))) +(def useInView (gobj/get react-intersection-observer "useInView")) (defn reset-ios-whole-page-offset! [] @@ -65,10 +65,7 @@ (plugin-handler/hook-plugin-editor :input-selection-end (bean/->js e))))))) state)} [{:keys [on-change] :as props}] - (let [skip-composition? (or - (state/sub :editor/show-page-search?) - (state/sub :editor/show-block-search?) - (state/sub :editor/show-template-search?)) + (let [skip-composition? (state/sub :editor/action) on-composition (fn [e] (if skip-composition? (on-change e) @@ -400,11 +397,13 @@ get-group-name empty-placeholder item-render - class]}] + class + header]}] (let [current-idx (get state ::current-idx)] [:div#ui__ac {:class class} (if (seq matched) [:div#ui__ac-inner.hide-scrollbar + (when header header) (for [[idx item] (medley/indexed matched)] [:<> {:key idx} @@ -901,13 +900,10 @@ label-right]] (progress-bar width)]) -(rum/defcs lazy-visible-inner < rum/reactive - {:init (fn [state] - (assoc state - ::ref (atom nil)))} - [state visible? content-fn] +(rum/defcs lazy-visible-inner + [state visible? content-fn ref] [:div.lazy-visibility - {:ref #(reset! (::ref state) %) + {:ref ref :style {:min-height 24}} (if visible? (when (fn? content-fn) @@ -925,25 +921,13 @@ [:div.h-2.bg-base-4.rounded.col-span-1]] [:div.h-2.bg-base-4.rounded]]]]])]) -(rum/defcs lazy-visible < - (rum/local false ::visible?) - (rum/local true ::active?) - [state content-fn sensor-opts {:keys [once?]}] - (let [*active? (::active? state)] - (if (or (util/mobile?) (mobile-util/native-platform?)) - (content-fn) - (let [*visible? (::visible? state)] - (visibility-sensor - (merge - {:on-change (fn [v] - (reset! *visible? v) - (when (and once? v) - (reset! *active? false))) - :partialVisibility true - :offset {:top -300 - :bottom -300} - :scrollCheck true - :scrollThrottle 500 - :active @*active?} - sensor-opts) - (lazy-visible-inner @*visible? content-fn)))))) +(rum/defc lazy-visible + [content-fn] + (let [[hasBeenSeen setHasBeenSeen] (rum/use-state false) + inViewState (useInView #js {:rootMargin "100px" + :onChange (fn [v entry] + (let [self-top (.-top (.-boundingClientRect entry)) + v (if v v (if (> self-top 0) false true))] + (setHasBeenSeen v)))}) + ref (.-ref inViewState)] + (lazy-visible-inner hasBeenSeen content-fn ref))) diff --git a/src/main/frontend/util.cljc b/src/main/frontend/util.cljc index a30c120627..26031a1a4f 100644 --- a/src/main/frontend/util.cljc +++ b/src/main/frontend/util.cljc @@ -312,6 +312,11 @@ (when input (.-selectionEnd input))) +(defn input-text-selected? + [input] + (not= (get-selection-start input) + (get-selection-end input))) + (defn get-selection-direction [input] (when input diff --git a/src/main/frontend/util/cursor.cljs b/src/main/frontend/util/cursor.cljs index a414cce6af..cf4b309d65 100644 --- a/src/main/frontend/util/cursor.cljs +++ b/src/main/frontend/util/cursor.cljs @@ -111,9 +111,10 @@ (defn beginning-of-line? [input] (let [[content pos] (get-input-content&pos input)] - (or (zero? pos) - (when-let [pre-char (subs content (dec pos) pos)] - (= pre-char \newline))))) + (when content + (or (zero? pos) + (when-let [pre-char (subs content (dec pos) pos)] + (= pre-char \newline)))))) (defn move-cursor-to-line-end [input] diff --git a/src/main/frontend/util/text.cljs b/src/main/frontend/util/text.cljs index e27bfb7051..95a2500e90 100644 --- a/src/main/frontend/util/text.cljs +++ b/src/main/frontend/util/text.cljs @@ -86,19 +86,21 @@ (defn get-string-all-indexes "Get all indexes of `value` in the string `s`." - [s value] - (loop [acc [] - i 0] - (if-let [i (string/index-of s value i)] - (recur (conj acc i) (+ i (count value))) - acc))) + [s value {:keys [before?] :or {before? true}}] + (if (= value "") + (if before? [0] [(dec (count s))]) + (loop [acc [] + i 0] + (if-let [i (string/index-of s value i)] + (recur (conj acc i) (+ i (count value))) + acc)))) (defn wrapped-by? "`pos` must be wrapped by `before` and `and` in string `value`, e.g. ((a|b))" [value pos before end] - (let [before-matches (->> (get-string-all-indexes value before) + (let [before-matches (->> (get-string-all-indexes value before {:before? true}) (map (fn [i] [i :before]))) - end-matches (->> (get-string-all-indexes value end) + end-matches (->> (get-string-all-indexes value end {:before? false}) (map (fn [i] [i :end]))) indexes (sort-by first (concat before-matches end-matches [[pos :between]])) ks (map second indexes) diff --git a/src/main/frontend/version.cljs b/src/main/frontend/version.cljs index 5eac6181a1..35201d4889 100644 --- a/src/main/frontend/version.cljs +++ b/src/main/frontend/version.cljs @@ -1,3 +1,3 @@ (ns frontend.version) -(defonce version "0.7.5") +(defonce version "0.7.6") diff --git a/src/test/frontend/db/query_dsl_test.cljs b/src/test/frontend/db/query_dsl_test.cljs index f989dce426..82f80b57a1 100644 --- a/src/test/frontend/db/query_dsl_test.cljs +++ b/src/test/frontend/db/query_dsl_test.cljs @@ -394,7 +394,7 @@ tags: other (dsl-query "(or [[tag2]] [[page 3]])"))) "OR query with nonexistent page should return meaningful results") - (is (= ["foo:: bar\n" "b1 [[page 1]] #tag2" "b3"] + (is (= ["b1 [[page 1]] #tag2" "foo:: bar\n" "b3"] (->> (dsl-query "(not [[page 2]])") ;; Only filter to page1 to get meaningful results (filter #(= "page1" (get-in % [:block/page :block/name]))) diff --git a/src/test/frontend/extensions/calc_test.cljc b/src/test/frontend/extensions/calc_test.cljc index 1d7a42162d..53b795a2c8 100644 --- a/src/test/frontend/extensions/calc_test.cljc +++ b/src/test/frontend/extensions/calc_test.cljc @@ -101,7 +101,14 @@ 0.0 "acos(cos(0))" 5.0 "2 * log(10) + 3" 1.0 "-2 * log(10) + 3" - 10.0 "ln(1) + 10"))) + 10.0 "ln(1) + 10")) + (testing "avoiding rounding errors" + (are [value expr] (= value (run expr)) + 3.3 "1.1 + 2.2" + 2.2 "3.3 - 1.1" + 0.0001 "1/10000" + 1e-7 "1/10000000" + ))) (deftest variables (testing "variables can be remembered" diff --git a/src/test/frontend/util/text_test.cljs b/src/test/frontend/util/text_test.cljs index 49cd34b3ef..77701950e9 100644 --- a/src/test/frontend/util/text_test.cljs +++ b/src/test/frontend/util/text_test.cljs @@ -23,11 +23,11 @@ (deftest get-string-all-indexes [] (are [x y] (= x y) - (text-util/get-string-all-indexes "[[hello]] [[world]]" "[[") + (text-util/get-string-all-indexes "[[hello]] [[world]]" "[[" {}) [0 10] - (text-util/get-string-all-indexes "abc abc ab" "ab") + (text-util/get-string-all-indexes "abc abc ab" "ab" {}) [0 4 8] - (text-util/get-string-all-indexes "a.c a.c ab" "a.") + (text-util/get-string-all-indexes "a.c a.c ab" "a." {}) [0 4])) diff --git a/yarn.lock b/yarn.lock index af068a8498..f0f77150c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6623,7 +6623,7 @@ prompts@^2.3.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@15.x, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.x, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.2: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6811,6 +6811,11 @@ react-grid-layout@0.16.6: react-draggable "3.x" react-resizable "1.x" +react-icon-base@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.0.tgz#a196e33fdf1e7aaa1fda3aefbb68bdad9e82a79d" + integrity sha512-9wwKJa2LB8ujtJB5MAXYYEM7JfYThZTj0YnfGxzLLWkifaLIGc7iTde2EpJ7ka5MjneRHnlxbIn5VV9k2WjUVA== + react-icon-base@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.2.tgz#a17101dad9c1192652356096860a9ab43a0766c7" @@ -6820,6 +6825,13 @@ react-icons@2.2.7: version "2.2.7" resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-2.2.7.tgz#d7860826b258557510dac10680abea5ca23cf650" integrity sha512-0n4lcGqzJFcIQLoQytLdJCE0DKSA9dkwEZRYoGrIDJZFvIT6Hbajx5mv9geqhqFiNjUgtxg8kPyDfjlhymbGFg== + dependencies: + react-icon-base "2.1.0" + +react-intersection-observer@^9.3.5: + version "9.3.5" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.3.5.tgz#df97584c1ef1549a47d4af6380db2fb4b76d7bba" + integrity sha512-TiJXVUapzAaIrZCAMBLjyWvwGYNGm0Xpkcwm3NY23b9PsJEBavul0hRFmrwc/LOmBUA/8TlkjCj7lCvjM0q1Hg== react-is@^16.13.1, react-is@^16.3.1, react-is@^16.7.0: version "16.13.1" @@ -6880,13 +6892,6 @@ react-transition-group@4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react-visibility-sensor@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/react-visibility-sensor/-/react-visibility-sensor-5.1.1.tgz#5238380960d3a0b2be0b7faddff38541e337f5a9" - integrity sha512-cTUHqIK+zDYpeK19rzW6zF9YfT4486TIgizZW53wEZ+/GPBbK7cNS0EHyJVyHYacwFEvvHLEKfgJndbemWhB/w== - dependencies: - prop-types "^15.7.2" - react@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"