From 3bef80932d2f3b0dab0056a7c685df661dd075e0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 Mar 2021 08:43:23 +0100 Subject: [PATCH] :recycle: Replace slate editor with draft-js. --- common/app/common/attrs.cljc | 13 +- common/app/common/text.cljc | 79 +++++ frontend/package.json | 4 +- .../resources/styles/main/partials/texts.scss | 72 +++- frontend/src/app/main/data/workspace.cljs | 125 +------ .../src/app/main/data/workspace/common.cljs | 109 +++++- .../data/workspace/libraries_helpers.cljs | 28 +- .../src/app/main/data/workspace/texts.cljs | 312 +++++++++--------- frontend/src/app/main/refs.cljs | 6 + frontend/src/app/main/ui.cljs | 16 +- .../app/main/ui/handoff/attributes/text.cljs | 94 +++--- frontend/src/app/main/ui/shapes/text.cljs | 91 +++-- .../src/app/main/ui/shapes/text/embed.cljs | 57 ++-- .../src/app/main/ui/shapes/text/styles.cljs | 201 ++++++----- frontend/src/app/main/ui/workspace.cljs | 1 - .../app/main/ui/workspace/shapes/text.cljs | 21 +- .../main/ui/workspace/shapes/text/editor.cljs | 303 ++++++----------- .../app/main/ui/workspace/sidebar/assets.cljs | 4 +- .../workspace/sidebar/options/menus/fill.cljs | 4 +- .../workspace/sidebar/options/menus/text.cljs | 29 +- .../sidebar/options/menus/typography.cljs | 26 +- .../sidebar/options/shapes/multiple.cljs | 16 +- .../sidebar/options/shapes/text.cljs | 74 +++-- .../src/app/main/ui/workspace/viewport.cljs | 12 +- frontend/src/app/util/code_gen.cljs | 59 +++- frontend/src/app/util/text.cljs | 123 ------- frontend/src/app/util/text_editor.cljs | 298 +++++++++++++++++ frontend/yarn.lock | 76 ++++- 28 files changed, 1272 insertions(+), 981 deletions(-) create mode 100644 common/app/common/text.cljc delete mode 100644 frontend/src/app/util/text.cljs create mode 100644 frontend/src/app/util/text_editor.cljs diff --git a/common/app/common/attrs.cljc b/common/app/common/attrs.cljc index 3cb24a935c..f341a9adb9 100644 --- a/common/app/common/attrs.cljc +++ b/common/app/common/attrs.cljc @@ -7,7 +7,8 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.common.attrs) +(ns app.common.attrs + (:refer-clojure :exclude [merge])) ;; Extract some attributes of a list of shapes. ;; For each attribute, if the value is the same in all shapes, @@ -48,7 +49,6 @@ (loop [attr (first attrs) attrs (rest attrs) result (transient {})] - (if attr (let [value (loop [curr (first objs) @@ -75,3 +75,12 @@ (persistent! result))))) +(defn merge + "Attrs specific merge function." + [obj attrs] + (reduce-kv (fn [obj k v] + (if (nil? v) + (dissoc obj k) + (assoc obj k v))) + obj + attrs)) diff --git a/common/app/common/text.cljc b/common/app/common/text.cljc new file mode 100644 index 0000000000..d8123c0393 --- /dev/null +++ b/common/app/common/text.cljc @@ -0,0 +1,79 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020-2021 UXBOX Labs SL + +(ns app.common.text + (:require + [app.common.attrs :as attrs] + [app.common.data :as d] + [app.util.transit :as t] + [clojure.walk :as walk] + [cuerdas.core :as str])) + +(def default-text-attrs + {:typography-ref-file nil + :typography-ref-id nil + :font-id "sourcesanspro" + :font-family "sourcesanspro" + :font-variant-id "regular" + :font-size "14" + :font-weight "400" + :font-style "normal" + :line-height "1.2" + :letter-spacing "0" + :text-transform "none" + :text-align "left" + :text-decoration "none" + :fill-color nil + :fill-opacity 1}) + +(def typography-fields + [:font-id + :font-family + :font-variant-id + :font-size + :font-weight + :font-style + :line-height + :letter-spacing + :text-transform]) + +(def default-typography + (merge + {:name "Source Sans Pro Regular"} + (select-keys default-text-attrs typography-fields))) + +(defn transform-nodes + ([transform root] + (transform-nodes identity transform root)) + ([pred transform root] + (walk/postwalk + (fn [item] + (if (and (map? item) (pred item)) + (transform item) + item)) + root))) + +(defn node-seq + ([root] (node-seq identity root)) + ([match? root] + (->> (tree-seq map? :children root) + (filter match?) + (seq)))) + +(defn ^boolean is-text-node? + [node] + (string? (:text node))) + +(defn ^boolean is-paragraph-node? + [node] + (= "paragraph" (:type node))) + +(defn ^boolean is-root-node? + [node] + (= "root" (:type node))) diff --git a/frontend/package.json b/frontend/package.json index b8c8974a2f..dd4ff7333e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,10 +34,10 @@ "shadow-cljs": "^2.11.20" }, "dependencies": { - "humanize-duration": "~3.25.0", - "luxon": "~1.25.0", "date-fns": "^2.19.0", + "draft-js": "^0.11.7", "highlight.js": "^10.6.0", + "humanize-duration": "~3.25.0", "js-beautify": "^1.13.5", "mousetrap": "^1.6.5", "randomcolor": "^0.6.2", diff --git a/frontend/resources/styles/main/partials/texts.scss b/frontend/resources/styles/main/partials/texts.scss index 24ebc8f0f6..b132d18cc9 100644 --- a/frontend/resources/styles/main/partials/texts.scss +++ b/frontend/resources/styles/main/partials/texts.scss @@ -1,5 +1,69 @@ -foreignObject .rich-text { - color: $color-black; - height: 100%; - white-space: pre-wrap; +foreignObject { + .text-editor, .rich-text { + color: $color-black; + height: 100%; + white-space: pre-wrap; + font-family: sourcesanspro; + + div { + line-height: inherit; + user-select: text; + } + + span { + line-height: inherit; + } + } + + .text-editor { + .public-DraftStyleDefault-rtl { + direction: rtl; + } + .public-DraftStyleDefault-rtl { + direction: ltr; + } + + .DraftEditor-root { + height: 100%; + display: flex; + flex-direction: column; + } + + &.align-top { + .DraftEditor-root { + justify-content: flex-start; + } + } + + &.align-center { + .DraftEditor-root { + justify-content: center; + } + } + + &.align-bottom { + .DraftEditor-root { + justify-content: flex-end; + } + } + } + + .rich-text .paragraphs { + height: 100%; + display: flex; + flex-direction: column; + + &.align-top { + justify-content: flex-start; + } + + &.align-center { + justify-content: center; + } + + &.align-bottom { + justify-content: flex-end; + } + } } + diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 8ea1041d46..4901d0a82f 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -33,7 +33,6 @@ [app.main.data.workspace.notifications :as dwn] [app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.selection :as dws] - [app.main.data.workspace.texts :as dwtxt] [app.main.data.workspace.transforms :as dwt] [app.main.repo :as rp] [app.main.store :as st] @@ -603,22 +602,6 @@ (let [selected (get-in state [:workspace-local :selected])] (rx/from (map #(update-shape % attrs) selected)))))) -(defn update-color-on-selected-shapes - [{:keys [fill-color stroke-color] :as attrs}] - (us/verify ::shape-attrs attrs) - (ptk/reify ::update-color-on-selected-shapes - ptk/WatchEvent - (watch [_ state stream] - (let [selected (get-in state [:workspace-local :selected]) - update-fn - (fn [shape] - (cond-> (merge shape attrs) - (and (= :text (:type shape)) - (string? (:fill-color attrs))) - (dwtxt/impl-update-shape-attrs {:fill (:fill-color attrs)})))] - (rx/of (dwc/update-shapes-recursive selected update-fn)))))) - - ;; --- Shape Movement (using keyboard shorcuts) (declare initial-selection-align) @@ -649,119 +632,13 @@ ;; --- Delete Selected -(defn- delete-shapes - [ids] - (us/assert (s/coll-of ::us/uuid) ids) - (ptk/reify ::delete-shapes - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (:current-page-id state) - objects (dwc/lookup-page-objects state page-id) - - get-empty-parents - (fn [parents] - (->> parents - (map (fn [id] - (let [obj (get objects id)] - (when (and (= :group (:type obj)) - (= 1 (count (:shapes obj)))) - obj)))) - (take-while (complement nil?)) - (map :id))) - - groups-to-unmask - (reduce (fn [group-ids id] - ;; When the shape to delete is the mask of a masked group, - ;; the mask condition must be removed, and it must be - ;; converted to a normal group. - (let [obj (get objects id) - parent (get objects (:parent-id obj))] - (if (and (:masked-group? parent) - (= id (first (:shapes parent)))) - (conj group-ids (:id parent)) - group-ids))) - #{} - ids) - - rchanges - (d/concat - (reduce (fn [res id] - (let [children (cp/get-children id objects) - parents (cp/get-parents id objects) - del-change #(array-map - :type :del-obj - :page-id page-id - :id %)] - (d/concat res - (map del-change (reverse children)) - [(del-change id)] - (map del-change (get-empty-parents parents)) - [{:type :reg-objects - :page-id page-id - :shapes (vec parents)}]))) - [] - ids) - (map #(array-map - :type :mod-obj - :page-id page-id - :id % - :operations [{:type :set - :attr :masked-group? - :val false}]) - groups-to-unmask)) - - uchanges - (d/concat - (reduce (fn [res id] - (let [children (cp/get-children id objects) - parents (cp/get-parents id objects) - parent (get objects (first parents)) - add-change (fn [id] - (let [item (get objects id)] - {:type :add-obj - :id (:id item) - :page-id page-id - :index (cp/position-on-parent id objects) - :frame-id (:frame-id item) - :parent-id (:parent-id item) - :obj item}))] - (d/concat res - (map add-change (reverse (get-empty-parents parents))) - [(add-change id)] - (map add-change children) - [{:type :reg-objects - :page-id page-id - :shapes (vec parents)}] - (when (some? parent) - [{:type :mod-obj - :page-id page-id - :id (:id parent) - :operations [{:type :set-touched - :touched (:touched parent)}]}])))) - [] - ids) - (map #(array-map - :type :mod-obj - :page-id page-id - :id % - :operations [{:type :set - :attr :masked-group? - :val true}]) - groups-to-unmask))] - - ;; (println "================ rchanges") - ;; (cljs.pprint/pprint rchanges) - ;; (println "================ uchanges") - ;; (cljs.pprint/pprint uchanges) - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) - (def delete-selected "Deselect all and remove all selected shapes." (ptk/reify ::delete-selected ptk/WatchEvent (watch [_ state stream] (let [selected (get-in state [:workspace-local :selected])] - (rx/of (delete-shapes selected) + (rx/of (dwc/delete-shapes selected) (dws/deselect-all)))))) ;; --- Shape Vertical Ordering diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index be9196187e..19043af5f6 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -395,7 +395,6 @@ ;; Shapes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - (defn expand-all-parents [ids objects] (ptk/reify ::expand-all-parents @@ -672,6 +671,114 @@ :shapes [shape-id]})))] (rx/of (commit-changes rchanges uchanges {:commit-local? true})))))) + +(defn delete-shapes + [ids] + (us/assert (s/coll-of ::us/uuid) ids) + (ptk/reify ::delete-shapes + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (:current-page-id state) + objects (lookup-page-objects state page-id) + + get-empty-parents + (fn [parents] + (->> parents + (map (fn [id] + (let [obj (get objects id)] + (when (and (= :group (:type obj)) + (= 1 (count (:shapes obj)))) + obj)))) + (take-while (complement nil?)) + (map :id))) + + groups-to-unmask + (reduce (fn [group-ids id] + ;; When the shape to delete is the mask of a masked group, + ;; the mask condition must be removed, and it must be + ;; converted to a normal group. + (let [obj (get objects id) + parent (get objects (:parent-id obj))] + (if (and (:masked-group? parent) + (= id (first (:shapes parent)))) + (conj group-ids (:id parent)) + group-ids))) + #{} + ids) + + rchanges + (d/concat + (reduce (fn [res id] + (let [children (cp/get-children id objects) + parents (cp/get-parents id objects) + del-change #(array-map + :type :del-obj + :page-id page-id + :id %)] + (d/concat res + (map del-change (reverse children)) + [(del-change id)] + (map del-change (get-empty-parents parents)) + [{:type :reg-objects + :page-id page-id + :shapes (vec parents)}]))) + [] + ids) + (map #(array-map + :type :mod-obj + :page-id page-id + :id % + :operations [{:type :set + :attr :masked-group? + :val false}]) + groups-to-unmask)) + + uchanges + (d/concat + (reduce (fn [res id] + (let [children (cp/get-children id objects) + parents (cp/get-parents id objects) + parent (get objects (first parents)) + add-change (fn [id] + (let [item (get objects id)] + {:type :add-obj + :id (:id item) + :page-id page-id + :index (cp/position-on-parent id objects) + :frame-id (:frame-id item) + :parent-id (:parent-id item) + :obj item}))] + (d/concat res + (map add-change (reverse (get-empty-parents parents))) + [(add-change id)] + (map add-change children) + [{:type :reg-objects + :page-id page-id + :shapes (vec parents)}] + (when (some? parent) + [{:type :mod-obj + :page-id page-id + :id (:id parent) + :operations [{:type :set-touched + :touched (:touched parent)}]}])))) + [] + ids) + (map #(array-map + :type :mod-obj + :page-id page-id + :id % + :operations [{:type :set + :attr :masked-group? + :val true}]) + groups-to-unmask))] + + ;; (println "================ rchanges") + ;; (cljs.pprint/pprint rchanges) + ;; (println "================ uchanges") + ;; (cljs.pprint/pprint uchanges) + (rx/of (commit-changes rchanges uchanges {:commit-local? true})))))) + + ;; --- Add shape to Workspace (defn- viewport-center diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 44fd6c31f6..437a5431a4 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -5,20 +5,20 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) 2020-2021 UXBOX Labs SL (ns app.main.data.workspace.libraries-helpers (:require - [cljs.spec.alpha :as s] - [clojure.set :as set] - [app.common.spec :as us] [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] [app.common.pages :as cp] + [app.common.spec :as us] + [app.common.text :as txt] [app.main.data.workspace.groups :as dwg] [app.util.logging :as log] - [app.util.text :as ut])) + [cljs.spec.alpha :as s] + [clojure.set :as set])) ;; Change this to :info :debug or :trace to debug this module (log/set-level! :warn) @@ -317,11 +317,11 @@ (->> shape :content ;; Check if any node in the content has a reference for the library - (ut/some-node - #(or (and (some? (:stroke-color-ref-id %)) - (= library-id (:stroke-color-ref-file %))) - (and (some? (:fill-color-ref-id %)) - (= library-id (:fill-color-ref-file %)))))) + (txt/node-seq + #(or (and (some? (:stroke-color-ref-id %)) + (= library-id (:stroke-color-ref-file %))) + (and (some? (:fill-color-ref-id %)) + (= library-id (:fill-color-ref-file %)))))) (some #(let [attr (name %) attr-ref-id (keyword (str attr "-ref-id")) @@ -336,9 +336,9 @@ (->> shape :content ;; Check if any node in the content has a reference for the library - (ut/some-node - #(and (some? (:typography-ref-id %)) - (= library-id (:typography-ref-file %))))))))) + (txt/node-seq + #(and (some? (:typography-ref-id %)) + (= library-id (:typography-ref-file %))))))))) (defmulti generate-sync-shape "Generate changes to synchronize one shape with all assets of the given type @@ -356,7 +356,7 @@ (defn- generate-sync-text-shape [shape container update-node] (let [old-content (:content shape) - new-content (ut/map-node update-node old-content) + new-content (txt/transform-nodes update-node old-content) rchanges [(make-change container {:type :mod-obj diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 27ef5ce4a1..94eb3076f5 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -5,199 +5,188 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) 2020-2021 UXBOX Labs SL (ns app.main.data.workspace.texts (:require - ["slate" :as slate :refer [Editor Node Transforms Text]] - ["slate-react" :as rslate] [app.common.math :as mth] [app.common.attrs :as attrs] + [app.common.text :as txt] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] + [app.common.data :as d] + [app.main.data.workspace.selection :as dws] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.transforms :as dwt] [app.main.fonts :as fonts] [app.util.object :as obj] - [app.util.text :as ut] + [app.util.text-editor :as ted] + [app.util.timers :as ts] [beicon.core :as rx] [cljs.spec.alpha :as s] - [clojure.walk :as walk] [goog.object :as gobj] + [cuerdas.core :as str] [potok.core :as ptk])) -(defn create-editor - [] - (rslate/withReact (slate/createEditor))) - -(defn assign-editor - [id editor] - (ptk/reify ::assign-editor +(defn update-editor + [editor] + (ptk/reify ::update-editor ptk/UpdateEvent (update [_ state] - (-> state - (assoc-in [:workspace-local :editors id] editor) - (update-in [:workspace-local :editor-n] (fnil inc 0)))))) + (if (some? editor) + (assoc state :workspace-editor editor) + (dissoc state :workspace-editor))))) + +(defn focus-editor + [] + (ptk/reify ::focus-editor + ptk/EffectEvent + (effect [_ state stream] + (when-let [editor (:workspace-editor state)] + (ts/schedule #(.focus ^js editor)))))) + +(defn update-editor-state + [{:keys [id] :as shape} editor-state] + (ptk/reify ::update-editor-state + ptk/UpdateEvent + (update [_ state] + (if (some? editor-state) + (update state :workspace-editor-state assoc id editor-state) + (update state :workspace-editor-state dissoc id))))) + +(defn initialize-editor-state + [{:keys [id content] :as shape}] + (ptk/reify ::initialize-editor-state + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-editor-state id] + (fn [_] + (ted/create-editor-state + (some->> content ted/import-content))))))) + +(defn finalize-editor-state + [{:keys [id] :as shape}] + (ptk/reify ::finalize-editor-state + ptk/WatchEvent + (watch [_ state stream] + (let [content (-> (get-in state [:workspace-editor-state id]) + (ted/get-editor-current-content))] + (if (ted/content-has-text? content) + (let [content (d/merge (ted/export-content content) + (dissoc (:content shape) :children))] + (rx/merge + (rx/of (update-editor-state shape nil)) + (when (not= content (:content shape)) + (rx/of (dwc/update-shapes [id] #(assoc % :content content)))))) + (rx/of (dws/deselect-shape id) + (dwc/delete-shapes [id]))))))) + +(defn select-all + "Select all content of the current editor. When not editor found this + event is noop." + [{:keys [id] :as shape}] + (ptk/reify ::editor-select-all + ptk/UpdateEvent + (update [_ state] + (d/update-in-when state [:workspace-editor-state id] ted/editor-select-all)))) ;; --- Helpers -(defn- calculate-full-selection - [editor] - (let [children (obj/get editor "children") - paragraphs (obj/get-in children [0 "children" 0 "children"]) - lastp (aget paragraphs (dec (alength paragraphs))) - lastptxt (.string Node lastp)] - #js {:anchor #js {:path #js [0 0 0] - :offset 0} - :focus #js {:path #js [0 0 (dec (alength paragraphs))] - :offset (alength lastptxt)}})) - -(defn- editor-select-all! - [editor] - (let [children (obj/get editor "children") - paragraphs (obj/get-in children [0 "children" 0 "children"]) - range (calculate-full-selection editor)] - (.select Transforms editor range))) - -(defn- editor-set! - ([editor props] - (editor-set! editor props #js {})) - ([editor props options] - (.setNodes Transforms editor props options) - editor)) - -(defn- transform-nodes - [pred transform data] - (walk/postwalk - (fn [item] - (if (and (map? item) (pred item)) - (transform item) - item)) - data)) - -;; --- Editor Related Helpers - -(defn- ^boolean is-text-node? - [node] - (cond - (object? node) (.isText Text node) - (map? node) (string? (:text node)) - (nil? node) false - :else (throw (ex-info "unexpected type" {:node node})))) - -(defn- ^boolean is-paragraph-node? - [node] - (cond - (object? node) (= (.-type node) "paragraph") - (map? node) (= "paragraph" (:type node)) - (nil? node) false - :else (throw (ex-info "unexpected type" {:node node})))) - -(defn- ^boolean is-root-node? - [node] - (cond - (object? node) (= (.-type node) "root") - (map? node) (= "root" (:type node)) - (nil? node) false - :else (throw (ex-info "unexpected type" {:node node})))) - -(defn- editor-current-values - [editor pred attrs universal?] - (let [options #js {:match pred :universal universal?} - _ (when (nil? (obj/get editor "selection")) - (obj/set! options "at" (calculate-full-selection editor))) - result (.nodes Editor editor options) - match (ffirst (es6-iterator-seq result))] - (when (object? match) - (let [attrs (clj->js attrs) - result (areduce attrs i ret #js {} - (let [val (obj/get match (aget attrs i))] - (if val - (obj/set! ret (aget attrs i) val) - ret)))] - (js->clj result :keywordize-keys true))))) - -(defn nodes-seq - [match? node] - (->> (tree-seq map? :children node) - (filter match?))) - (defn- shape-current-values [shape pred attrs] (let [root (:content shape) - nodes (->> (nodes-seq pred root) - (map #(if (is-text-node? %) - (merge ut/default-text-attrs %) + nodes (->> (txt/node-seq pred root) + (map #(if (txt/is-text-node? %) + (merge txt/default-text-attrs %) %)))] (attrs/get-attrs-multi nodes attrs))) -(defn current-text-values - [{:keys [editor default attrs shape]}] - (if editor - (editor-current-values editor is-text-node? attrs true) - (shape-current-values shape is-text-node? attrs))) - (defn current-paragraph-values - [{:keys [editor attrs shape]}] - (if editor - (editor-current-values editor is-paragraph-node? attrs false) - (shape-current-values shape is-paragraph-node? attrs))) + [{:keys [editor-state attrs shape]}] + (if editor-state + (-> (ted/get-editor-current-block-data editor-state) + (select-keys attrs)) + (shape-current-values shape txt/is-paragraph-node? attrs))) -(defn current-root-values - [{:keys [editor attrs shape]}] - (if editor - (editor-current-values editor is-root-node? attrs false) - (shape-current-values shape is-root-node? attrs))) +(defn current-text-values + [{:keys [editor-state attrs shape]}] + (if editor-state + (-> (ted/get-editor-current-inline-styles editor-state) + (select-keys attrs)) + (shape-current-values shape txt/is-text-node? attrs))) -(defn- merge-attrs - [node attrs] - (reduce-kv (fn [node k v] - (if (nil? v) - (dissoc node k) - (assoc node k v))) - node - attrs)) -(defn impl-update-shape-attrs - ([shape attrs] - ;; NOTE: this arity is used in workspace for properly update the - ;; fill color using colorpalette, then the predicate should be - ;; defined. - (impl-update-shape-attrs shape attrs is-text-node?)) - ([{:keys [type content] :as shape} attrs pred] - (assert (= :text type) "should be shape type") - (let [merge-attrs #(merge-attrs % attrs)] - (update shape :content #(transform-nodes pred merge-attrs %))))) +;; --- TEXT EDITION IMPL -(defn update-attrs - [{:keys [id editor attrs pred split] - :or {pred is-text-node?}}] - (if editor - (ptk/reify ::update-attrs - ptk/EffectEvent - (effect [_ state stream] - (editor-set! editor (clj->js attrs) #js {:match pred :split split}))) - - (ptk/reify ::update-attrs - ptk/WatchEvent - (watch [_ state stream] - (let [objects (dwc/lookup-page-objects state) - shape (get objects id) - ids (cond (= (:type shape) :text) [id] - (= (:type shape) :group) (cp/get-children id objects))] - (rx/of (dwc/update-shapes ids #(impl-update-shape-attrs % attrs pred)))))))) - -(defn update-text-attrs - [options] - (update-attrs (assoc options :pred is-text-node? :split true))) - -(defn update-paragraph-attrs - [options] - (update-attrs (assoc options :pred is-paragraph-node? :split false))) +(defn- update-shape + [shape pred-fn attrs] + (let [merge-attrs #(attrs/merge % attrs) + transform #(txt/transform-nodes pred-fn merge-attrs %)] + (update shape :content transform))) (defn update-root-attrs - [options] - (update-attrs (assoc options :pred is-root-node? :split false))) + [{:keys [id attrs]}] + (ptk/reify ::update-root-attrs + ptk/WatchEvent + (watch [_ state stream] + (let [objects (dwc/lookup-page-objects state) + shape (get objects id) + + update-fn #(update-shape % txt/is-root-node? attrs) + shape-ids (cond (= (:type shape) :text) [id] + (= (:type shape) :group) (cp/get-children id objects))] + + (rx/of (dwc/update-shapes shape-ids update-fn) + (focus-editor)))))) + +(defn update-paragraph-attrs + [{:keys [id attrs]}] + (let [attrs (d/without-nils attrs)] + (ptk/reify ::update-paragraph-attrs + ptk/UpdateEvent + (update [_ state] + (d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-block-data attrs)) + + ptk/WatchEvent + (watch [_ state stream] + (cond + (some? (get-in state [:workspace-editor-state id])) + (rx/of (focus-editor)) + + :else + (let [objects (dwc/lookup-page-objects state) + shape (get objects id) + + update-fn #(update-shape % txt/is-paragraph-node? attrs) + shape-ids (cond (= (:type shape) :text) [id] + (= (:type shape) :group) (cp/get-children id objects))] + + (rx/of (dwc/update-shapes shape-ids update-fn)))))))) + +(defn update-text-attrs + [{:keys [id attrs]}] + (let [attrs (d/without-nils attrs)] + (ptk/reify ::update-text-attrs + ptk/UpdateEvent + (update [_ state] + (d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles attrs)) + + ptk/WatchEvent + (watch [_ state stream] + (cond + (some? (get-in state [:workspace-editor-state id])) + (rx/of (focus-editor)) + + :else + (let [objects (dwc/lookup-page-objects state) + shape (get objects id) + + update-fn #(update-shape % txt/is-text-node? attrs) + shape-ids (cond (= (:type shape) :text) [id] + (= (:type shape) :group) (cp/get-children id objects))] + (rx/of (dwc/update-shapes shape-ids update-fn)))))))) + +;; --- RESIZE UTILS (defn update-overflow-text [id value] (ptk/reify ::update-overflow-text @@ -211,7 +200,7 @@ (ptk/reify ::start-edit-if-selected ptk/UpdateEvent (update [_ state] - (let [objects (dwc/lookup-page-objects state) + (let [objects (dwc/lookup-page-objects state) selected (->> state :workspace-local :selected (map #(get objects %)))] (cond-> state (and (= 1 (count selected)) @@ -284,7 +273,8 @@ ;; together. This improves the performance because we only re-render the ;; resized components once even if there are changes that applies to ;; lots of texts like changing a font -(defn resize-text [id new-width new-height] +(defn resize-text + [id new-width new-height] (ptk/reify ::resize-text IDeref (-deref [_] diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index f1647bc130..73c52b3812 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -180,6 +180,12 @@ (def workspace-frames (l/derived cp/select-frames workspace-page-objects)) +(def workspace-editor + (l/derived :workspace-editor st/state)) + +(def workspace-editor-state + (l/derived :workspace-editor-state st/state)) + (defn object-by-id [id] (l/derived #(get % id) workspace-page-objects)) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index fb5861ec0d..c5ca0bb902 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -9,35 +9,35 @@ (ns app.main.ui (:require - [app.config :as cfg] [app.common.data :as d] [app.common.exceptions :as ex] - [app.common.uuid :as uuid] [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.config :as cfg] [app.main.data.auth :refer [logout]] [app.main.data.messages :as dm] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.auth :refer [auth]] [app.main.ui.auth.verify-token :refer [verify-token]] - [app.main.ui.cursors :as c] [app.main.ui.context :as ctx] - [app.main.ui.onboarding] + [app.main.ui.cursors :as c] [app.main.ui.dashboard :refer [dashboard]] + [app.main.ui.handoff :refer [handoff]] [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] + [app.main.ui.onboarding] [app.main.ui.render :as render] [app.main.ui.settings :as settings] [app.main.ui.static :as static] [app.main.ui.viewer :refer [viewer-page]] - [app.main.ui.handoff :refer [handoff]] [app.main.ui.workspace :as workspace] [app.util.i18n :as i18n :refer [tr t]] - [app.util.timers :as ts] [app.util.router :as rt] - [cuerdas.core :as str] - [cljs.spec.alpha :as s] + [app.util.timers :as ts] [cljs.pprint :refer [pprint]] + [cljs.spec.alpha :as s] + [cuerdas.core :as str] [expound.alpha :as expound] [potok.core :as ptk] [rumext.alpha :as mf])) diff --git a/frontend/src/app/main/ui/handoff/attributes/text.cljs b/frontend/src/app/main/ui/handoff/attributes/text.cljs index e61c1922fd..2af5f9f31e 100644 --- a/frontend/src/app/main/ui/handoff/attributes/text.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/text.cljs @@ -5,24 +5,23 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) 2020-2021 UXBOX Labs SL (ns app.main.ui.handoff.attributes.text (:require - [rumext.alpha :as mf] + [app.common.text :as txt] + [app.main.fonts :as fonts] + [app.main.store :as st] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.handoff.attributes.common :refer [color-row]] + [app.main.ui.icons :as i] + [app.util.i18n :refer [tr]] + [app.util.code-gen :as cg] + [app.util.color :as uc] + [app.util.webapi :as wapi] [cuerdas.core :as str] [okulary.core :as l] - [app.util.data :as d] - [app.util.i18n :refer [t]] - [app.util.color :as uc] - [app.util.text :as ut] - [app.main.fonts :as fonts] - [app.main.ui.icons :as i] - [app.util.webapi :as wapi] - [app.main.ui.handoff.attributes.common :refer [color-row]] - [app.util.code-gen :as cg] - [app.main.store :as st] - [app.main.ui.components.copy-button :refer [copy-button]])) + [rumext.alpha :as mf])) (defn has-text? [shape] (:content shape)) @@ -72,7 +71,7 @@ ([style & properties] (cg/generate-css-props style properties params))) -(mf/defc typography-block [{:keys [shape locale text style full-style]}] +(mf/defc typography-block [{:keys [shape text style full-style]}] (let [typography-library-ref (mf/use-memo (mf/deps (:typography-ref-file style)) (make-typographies-library-ref (:typography-ref-file style))) @@ -93,7 +92,7 @@ {:style {:font-family (:font-family typography) :font-weight (:font-weight typography) :font-style (:font-style typography)}} - (t locale "workspace.assets.typography.sample")]] + (tr "workspace.assets.typography.sample")]] [:div.typography-entry-name (:name typography)] [:& copy-button {:data (copy-style-data typography)}]] @@ -102,7 +101,7 @@ {:style {:font-family (:font-family full-style) :font-weight (:font-weight full-style) :font-style (:font-style full-style)}} - (t locale "workspace.assets.typography.sample")] + (tr "workspace.assets.typography.sample")] [:& copy-button {:data (copy-style-data style)}]]) [:div.attributes-content-row @@ -117,78 +116,83 @@ (when (:font-id style) [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.font-family")] + [:div.attributes-label (tr "handoff.attributes.typography.font-family")] [:div.attributes-value (-> style :font-id fonts/get-font-data :name)] [:& copy-button {:data (copy-style-data style :font-family)}]]) (when (:font-style style) [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.font-style")] + [:div.attributes-label (tr "handoff.attributes.typography.font-style")] [:div.attributes-value (str (:font-style style))] [:& copy-button {:data (copy-style-data style :font-style)}]]) (when (:font-size style) [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.font-size")] + [:div.attributes-label (tr "handoff.attributes.typography.font-size")] [:div.attributes-value (str (:font-size style)) "px"] [:& copy-button {:data (copy-style-data style :font-size)}]]) (when (:line-height style) [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.line-height")] + [:div.attributes-label (tr "handoff.attributes.typography.line-height")] [:div.attributes-value (str (:line-height style)) "px"] [:& copy-button {:data (copy-style-data style :line-height)}]]) (when (:letter-spacing style) [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.letter-spacing")] + [:div.attributes-label (tr "handoff.attributes.typography.letter-spacing")] [:div.attributes-value (str (:letter-spacing style)) "px"] [:& copy-button {:data (copy-style-data style :letter-spacing)}]]) (when (:text-decoration style) [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.text-decoration")] - [:div.attributes-value (->> style :text-decoration (str "handoff.attributes.typography.text-decoration.") (t locale))] + [:div.attributes-label (tr "handoff.attributes.typography.text-decoration")] + [:div.attributes-value (->> style :text-decoration (str "handoff.attributes.typography.text-decoration.") (tr))] [:& copy-button {:data (copy-style-data style :text-decoration)}]]) (when (:text-transform style) [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.text-transform")] - [:div.attributes-value (->> style :text-transform (str "handoff.attributes.typography.text-transform.") (t locale))] + [:div.attributes-label (tr "handoff.attributes.typography.text-transform")] + [:div.attributes-value (->> style :text-transform (str "handoff.attributes.typography.text-transform.") (tr))] [:& copy-button {:data (copy-style-data style :text-transform)}]])])) -(mf/defc text-block [{:keys [shape locale]}] - (let [font (ut/search-text-attrs (:content shape) - (keys ut/default-text-attrs)) - style-text-blocks (->> (keys ut/default-text-attrs) - (ut/parse-style-text-blocks (:content shape)) - (remove (fn [[style text]] (str/empty? (str/trim text)))) - (mapv (fn [[style text]] (vector (merge ut/default-text-attrs style) text)))) +(defn- remove-equal-values + [m1 m2] + (if (and (map? m1) (map? m2) (not (nil? m1)) (not (nil? m2))) + (->> m1 + (remove (fn [[k v]] (= (k m2) v))) + (into {})) + m1)) - font (merge ut/default-text-attrs font)] +(mf/defc text-block [{:keys [shape]}] + (let [font (cg/search-text-attrs (:content shape) + (keys txt/default-text-attrs)) + style-text-blocks (->> (keys txt/default-text-attrs) + (cg/parse-style-text-blocks (:content shape)) + (remove (fn [[style text]] (str/empty? (str/trim text)))) + (mapv (fn [[style text]] (vector (merge txt/default-text-attrs style) text)))) + + font (merge txt/default-text-attrs font)] (for [[idx [full-style text]] (map-indexed vector style-text-blocks)] (let [previus-style (first (nth style-text-blocks (dec idx) nil)) - style (d/remove-equal-values full-style previus-style) + style (remove-equal-values full-style previus-style) ;; If the color is set we need to add opacity otherwise the display will not work style (cond-> style (:fill-color style) (assoc :fill-opacity (:fill-opacity full-style)))] [:& typography-block {:shape shape - :locale locale :full-style full-style :style style :text text}])))) -(mf/defc text-panel [{:keys [shapes locale]}] - (let [shapes (->> shapes (filter has-text?))] - (when (seq shapes) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (t locale "handoff.attributes.typography")]] - - (for [shape shapes] - [:& text-block {:shape shape - :locale locale}])]))) +(mf/defc text-panel + [{:keys [shapes]}] + (when-let [shapes (seq (filter has-text? shapes))] + [:div.attributes-block + [:div.attributes-block-title + [:div.attributes-block-title-text (tr "handoff.attributes.typography")]] + (for [shape shapes] + [:& text-block {:shape shape}])])) diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index d3d9b769c2..81f2e0d95c 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -5,103 +5,86 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) 2020-2021 UXBOX Labs SL (ns app.main.ui.shapes.text (:require - [cuerdas.core :as str] - [rumext.alpha :as mf] - [app.main.ui.context :as muc] [app.common.data :as d] [app.common.geom.shapes :as geom] - [app.common.geom.matrix :as gmt] - [app.util.object :as obj] - [app.util.color :as uc] - [app.main.ui.shapes.text.styles :as sts] + [app.main.ui.context :as muc] [app.main.ui.shapes.text.embed :as ste] - [app.util.perf :as perf])) + [app.main.ui.shapes.text.styles :as sts] + [app.util.color :as uc] + [app.util.object :as obj] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (mf/defc render-text {::mf/wrap-props false} [props] - (let [node (obj/get props "node") - text (:text node) - style (sts/generate-text-styles props)] - [:span {:style style - :className (when (:fill-color-gradient node) "gradient")} + (let [node (obj/get props "node") + text (:text node) + style (sts/generate-text-styles node)] + [:span {:style style} (if (= text "") "\u00A0" text)])) (mf/defc render-root {::mf/wrap-props false} [props] - (let [node (obj/get props "node") - embed-fonts? (obj/get props "embed-fonts?") + (let [node (obj/get props "node") + embed? (obj/get props "embed-fonts?") children (obj/get props "children") - style (sts/generate-root-styles props)] + shape (obj/get props "shape") + style (sts/generate-root-styles shape node)] [:div.root.rich-text {:style style :xmlns "http://www.w3.org/1999/xhtml"} - [:* - [:style ".gradient { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"] - (when embed-fonts? - [ste/embed-fontfaces-style {:node node}])] + (when embed? + [ste/embed-fontfaces-style {:node node}]) children])) (mf/defc render-paragraph-set {::mf/wrap-props false} [props] - (let [node (obj/get props "node") + (let [node (obj/get props "node") children (obj/get props "children") - style (sts/generate-paragraph-set-styles props)] + shape (obj/get props "shape") + style (sts/generate-paragraph-set-styles shape)] [:div.paragraph-set {:style style} children])) (mf/defc render-paragraph {::mf/wrap-props false} [props] - (let [node (obj/get props "node") + (let [node (obj/get props "node") + shape (obj/get props "shape") children (obj/get props "children") - style (sts/generate-paragraph-styles props)] - [:p.paragraph {:style style} children])) + style (sts/generate-paragraph-styles shape node)] + [:p.paragraph {:style style :dir "auto"} children])) ;; -- Text nodes (mf/defc render-node {::mf/wrap-props false} [props] - (let [node (obj/get props "node") - index (obj/get props "index") - {:keys [type text children]} node] + (let [{:keys [type text children] :as node} (obj/get props "node")] (if (string? text) [:> render-text props] - (let [component (case type "root" render-root "paragraph-set" render-paragraph-set "paragraph" render-paragraph nil)] (when component - [:> component (obj/set! props "key" index) - (for [[index child] (d/enumerate children)] + [:> component props + (for [[index node] (d/enumerate children)] (let [props (-> (obj/clone props) - (obj/set! "node" child) + (obj/set! "node" node) (obj/set! "index" index) (obj/set! "key" index))] [:> render-node props]))]))))) -(mf/defc text-content - {::mf/wrap-props false} - [props] - (let [root (obj/get props "content") - shape (obj/get props "shape") - embed-fonts? (obj/get props "embed-fonts?")] - [:& render-node {:index 0 - :node root - :shape shape - :embed-fonts? embed-fonts?}])) - (defn- retrieve-colors [shape] - (let [colors (->> shape - :content + (let [colors (->> (:content shape) (tree-seq map? :children) (into #{} (comp (map :fill-color) (filter string?))))] (if (empty? colors) @@ -112,20 +95,20 @@ {::mf/wrap-props false ::mf/forward-ref true} [props ref] - (let [shape (unchecked-get props "shape") - grow-type (unchecked-get props "grow-type") + (let [{:keys [id x y width height content grow-type] :as shape} (obj/get props "shape") embed-fonts? (mf/use-ctx muc/embed-ctx) - {:keys [id x y width height content]} shape ;; We add 8px to add a padding for the exporter - width (+ width 8)] + ;; width (+ width 8) + ] [:foreignObject {:x x :y y - :id (:id shape) + :id id :data-colors (retrieve-colors shape) :transform (geom/transform-matrix shape) :width (if (#{:auto-width} grow-type) 100000 width) :height (if (#{:auto-height :auto-width} grow-type) 100000 height) :ref ref} - [:& text-content {:shape shape - :content (:content shape) - :embed-fonts? embed-fonts?}]])) + [:& render-node {:index 0 + :shape shape + :node content + :embed-fonts? embed-fonts?}]])) diff --git a/frontend/src/app/main/ui/shapes/text/embed.cljs b/frontend/src/app/main/ui/shapes/text/embed.cljs index 9d41810f03..8e6a0b46fb 100644 --- a/frontend/src/app/main/ui/shapes/text/embed.cljs +++ b/frontend/src/app/main/ui/shapes/text/embed.cljs @@ -5,43 +5,46 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) 2020-2021 UXBOX Labs SL (ns app.main.ui.shapes.text.embed (:require - [clojure.set :as set] - [promesa.core :as p] - [cuerdas.core :as str] - [rumext.alpha :as mf] + [app.common.data :as d] + [app.common.text :as txt] [app.main.data.fetch :as df] [app.main.fonts :as fonts] - [app.util.text :as ut])) + [app.util.object :as obj] + [clojure.set :as set] + [cuerdas.core :as str] + [promesa.core :as p] + [rumext.alpha :as mf])) -(defonce font-face-template " +(def font-face-template " /* latin */ @font-face { - font-family: '$0'; - font-style: $3; - font-weight: $2; + font-family: '%(family)s'; + font-style: %(style)s; + font-weight: %(weight)s; font-display: block; - src: url(/fonts/%(0)s-$1.woff) format('woff'); + src: url(/fonts/%(family)s-%(style)s.woff) format('woff'); } ") ;; -- Embed fonts into styles -(defn get-node-fonts [node] +(defn get-node-fonts + [node] (let [current-font (if (not (nil? (:font-id node))) #{(select-keys node [:font-id :font-variant-id])} #{}) children-font (map get-node-fonts (:children node))] (reduce set/union (conj children-font current-font)))) - -(defn get-local-font-css [font-id font-variant-id] - (let [{:keys [family variants]} (get @fonts/fontsdb font-id) - {:keys [name weight style]} (->> variants (filter #(= (:id %) font-variant-id)) first) - css-str (str/format font-face-template [family name weight style])] - (p/resolved css-str))) +(defn get-local-font-css + [font-id font-variant-id] + (let [{:keys [family variants] :as font} (get @fonts/fontsdb font-id) + {:keys [name weight style] :as variant} (d/seek #(= (:id %) font-variant-id) variants)] + (-> (str/format font-face-template {:family family :style style :width weight}) + (p/resolved)))) (defn get-text-font-data [text] (->> text @@ -59,17 +62,19 @@ replace-text (fn [text [url data]] (str/replace text url data))] (reduce replace-text font-text url-to-data)))) -(mf/defc embed-fontfaces-style [{:keys [node]}] - (let [embeded-fonts (mf/use-state nil)] +(mf/defc embed-fontfaces-style + {::mf/wrap-props false} + [props] + (let [node (obj/get props "node") + style (mf/use-state nil)] (mf/use-effect (mf/deps node) (fn [] (let [font-to-embed (get-node-fonts node) - font-to-embed (if (empty? font-to-embed) #{ut/default-text-attrs} font-to-embed) - embeded (map embed-font font-to-embed)] + font-to-embed (if (empty? font-to-embed) #{txt/default-text-attrs} font-to-embed) + embeded (map embed-font font-to-embed)] (-> (p/all embeded) - (p/then (fn [result] (reset! embeded-fonts (str/join "\n" result)))))))) + (p/then (fn [result] (reset! style (str/join "\n" result)))))))) - - (when (not (nil? @embeded-fonts)) - [:style @embeded-fonts]))) + (when (some? @style) + [:style @style]))) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 208dbf65b8..93f6cde2eb 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -5,135 +5,120 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) 2020-2021 UXBOX Labs SL (ns app.main.ui.shapes.text.styles (:require - [cuerdas.core :as str] - [app.main.fonts :as fonts] [app.common.data :as d] - [app.util.object :as obj] + [app.common.text :as txt] + [app.main.fonts :as fonts] [app.util.color :as uc] - [app.util.text :as ut])) + [app.util.object :as obj] + [cuerdas.core :as str])) (defn generate-root-styles - ([props] (generate-root-styles (clj->js (obj/get props "node")) props)) - ([data props] - (let [valign (obj/get data "vertical-align" "top") - shape (obj/get props "shape") - base #js {:height (or (:height shape) "100%") - :width (or (:width shape) "100%")}] - (cond-> base - (= valign "top") (obj/set! "justifyContent" "flex-start") - (= valign "center") (obj/set! "justifyContent" "center") - (= valign "bottom") (obj/set! "justifyContent" "flex-end") - )))) + [shape node] + (let [valign (or (:vertical-align node "top")) + base #js {:height (or (:height shape) "100%") + :width (or (:width shape) "100%")}] + (cond-> base + (= valign "top") (obj/set! "justifyContent" "flex-start") + (= valign "center") (obj/set! "justifyContent" "center") + (= valign "bottom") (obj/set! "justifyContent" "flex-end")))) (defn generate-paragraph-set-styles - ([props] (generate-paragraph-set-styles (clj->js (obj/get props "node")) props)) - ([data props] - ;; This element will control the auto-width/auto-height size for the - ;; shape. The properties try to adjust to the shape and "overflow" if - ;; the shape is not big enough. - ;; We `inherit` the property `justify-content` so it's set by the root where - ;; the property it's known. - ;; `inline-flex` is similar to flex but `overflows` outside the bounds of the - ;; parent - (let [shape (obj/get props "shape") - grow-type (:grow-type shape) - auto-width? (= grow-type :auto-width) - auto-height? (= grow-type :auto-height) - - base #js {:display "inline-flex" - :flexDirection "column" - :justifyContent "inherit" - :minHeight (when-not (or auto-width? auto-height?) "100%") - :minWidth (when-not auto-width? "100%") - :verticalAlign "top"}] - base))) + [{:keys [grow-type] :as shape}] + ;; This element will control the auto-width/auto-height size for the + ;; shape. The properties try to adjust to the shape and "overflow" if + ;; the shape is not big enough. + ;; We `inherit` the property `justify-content` so it's set by the root where + ;; the property it's known. + ;; `inline-flex` is similar to flex but `overflows` outside the bounds of the + ;; parent + (let [auto-width? (= grow-type :auto-width) + auto-height? (= grow-type :auto-height)] + #js {:display "inline-flex" + :flexDirection "column" + :justifyContent "inherit" + :minHeight (when-not (or auto-width? auto-height?) "100%") + :minWidth (when-not auto-width? "100%") + :verticalAlign "top"})) (defn generate-paragraph-styles - ([props] (generate-paragraph-styles (clj->js (obj/get props "node")) props)) - ([data props] - (let [shape (obj/get props "shape") - grow-type (:grow-type shape) - base #js {:fontSize "14px" - :margin "inherit" - :lineHeight "1.2"} - lh (obj/get data "line-height") - ta (obj/get data "text-align")] - (cond-> base - ta (obj/set! "textAlign" ta) - lh (obj/set! "lineHeight" lh) - (= grow-type :auto-width) (obj/set! "whiteSpace" "pre"))))) + [shape data] + (let [line-height (:line-height data) + text-align (:text-align data) + grow-type (:grow-type shape) + + base #js {:fontSize (str (:font-size txt/default-text-attrs) "px") + :lineHeight (:line-height txt/default-text-attrs) + :margin "inherit"}] + (cond-> base + (some? line-height) (obj/set! "lineHeight" line-height) + (some? text-align) (obj/set! "textAlign" text-align) + (= grow-type :auto-width) (obj/set! "whiteSpace" "pre")))) (defn generate-text-styles - ([props] (generate-text-styles (clj->js (obj/get props "node")) props)) - ([data props] - (let [letter-spacing (obj/get data "letter-spacing") - text-decoration (obj/get data "text-decoration") - text-transform (obj/get data "text-transform") - line-height (obj/get data "line-height") + [data] + (let [letter-spacing (:letter-spacing data) + text-decoration (:text-decoration data) + text-transform (:text-transform data) + line-height (:line-height data) - font-id (obj/get data "font-id" (:font-id ut/default-text-attrs)) - font-variant-id (obj/get data "font-variant-id") + font-id (:font-id data (:font-id txt/default-text-attrs)) + font-variant-id (:font-variant-id data) - font-family (obj/get data "font-family") - font-size (obj/get data "font-size") + font-family (:font-family data) + font-size (:font-size data) - ;; Old properties for backwards compatibility - fill (obj/get data "fill") - opacity (obj/get data "opacity" 1) + fill-color (:fill-color data) + fill-opacity (:fill-opacity data) - fill-color (obj/get data "fill-color" fill) - fill-opacity (obj/get data "fill-opacity" opacity) - fill-color-gradient (obj/get data "fill-color-gradient" nil) - fill-color-gradient (when fill-color-gradient - (-> (js->clj fill-color-gradient :keywordize-keys true) - (update :type keyword))) + ;; Uncomment this to allow to remove text colors. This could break the texts that already exist + ;;[r g b a] (if (nil? fill-color) + ;; [0 0 0 0] ;; Transparent color + ;; (uc/hex->rgba fill-color fill-opacity)) - ;; Uncomment this to allow to remove text colors. This could break the texts that already exist - ;;[r g b a] (if (nil? fill-color) - ;; [0 0 0 0] ;; Transparent color - ;; (uc/hex->rgba fill-color fill-opacity)) + [r g b a] (uc/hex->rgba fill-color fill-opacity) + text-color (str/format "rgba(%s, %s, %s, %s)" r g b a) + fontsdb (deref fonts/fontsdb) - [r g b a] (uc/hex->rgba fill-color fill-opacity) + base #js {:textDecoration text-decoration + :textTransform text-transform + :lineHeight (or line-height "inherit") + :color text-color}] - text-color (if fill-color-gradient - (uc/gradient->css (js->clj fill-color-gradient)) - (str/format "rgba(%s, %s, %s, %s)" r g b a)) + (when-let [gradient (:fill-color-gradient data)] + (let [text-color (-> (update gradient :type keyword) + (uc/gradient->css))] + (-> base + (obj/set! "background" "var(--text-color)") + (obj/set! "WebkitTextFillColor" "transparent") + (obj/set! "WebkitBackgroundClip" "text") + (obj/set! "--text-color" text-color)))) - fontsdb (deref fonts/fontsdb) + (when (and (string? letter-spacing) + (pos? (alength letter-spacing))) + (obj/set! base "letterSpacing" (str letter-spacing "px"))) - base #js {:textDecoration text-decoration - :textTransform text-transform - :lineHeight (or line-height "inherit") - :color text-color - "--text-color" text-color}] + (when (and (string? font-size) + (pos? (alength font-size))) + (obj/set! base "fontSize" (str font-size "px"))) - (when (and (string? letter-spacing) - (pos? (alength letter-spacing))) - (obj/set! base "letterSpacing" (str letter-spacing "px"))) + (when (and (string? font-id) + (pos? (alength font-id))) + (fonts/ensure-loaded! font-id) + (let [font (get fontsdb font-id)] + (let [font-family (or (:family font) + (obj/get data "fontFamily")) + font-variant (d/seek #(= font-variant-id (:id %)) + (:variants font)) + font-style (or (:style font-variant) + (obj/get data "fontStyle")) + font-weight (or (:weight font-variant) + (obj/get data "fontWeight"))] + (obj/set! base "fontFamily" font-family) + (obj/set! base "fontStyle" font-style) + (obj/set! base "fontWeight" font-weight)))) - (when (and (string? font-size) - (pos? (alength font-size))) - (obj/set! base "fontSize" (str font-size "px"))) - - (when (and (string? font-id) - (pos? (alength font-id))) - (fonts/ensure-loaded! font-id) - (let [font (get fontsdb font-id)] - (let [font-family (or (:family font) - (obj/get data "fontFamily")) - font-variant (d/seek #(= font-variant-id (:id %)) - (:variants font)) - font-style (or (:style font-variant) - (obj/get data "fontStyle")) - font-weight (or (:weight font-variant) - (obj/get data "fontWeight"))] - (obj/set! base "fontFamily" font-family) - (obj/set! base "fontStyle" font-style) - (obj/set! base "fontWeight" font-weight)))) - - - base))) + base)) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index acc321f3bb..2fd32a5ddd 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -141,7 +141,6 @@ [:& (mf/provider ctx/current-team-id) {:value (:team-id project)} [:& (mf/provider ctx/current-project-id) {:value (:id project)} [:& (mf/provider ctx/current-page-id) {:value page-id} - [:section#workspace [:& header {:file file :page-id page-id diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 212979b814..fcb1c88b61 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -5,7 +5,7 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) 2020-2021 UXBOX Labs SL (ns app.main.ui.workspace.shapes.text (:require @@ -26,6 +26,7 @@ [app.util.logging :as log] [app.util.object :as obj] [app.util.timers :as timers] + [app.util.text-editor :as ted] [beicon.core :as rx] [rumext.alpha :as mf])) @@ -52,8 +53,17 @@ (mf/defc text-resize-content {::mf/wrap-props false} [props] - (let [shape (obj/get props "shape") - {:keys [id name x y grow-type]} shape + (let [{:keys [id name x y grow-type] :as shape} (obj/get props "shape") + + state-map (mf/deref refs/workspace-editor-state) + editor-state (get state-map id) + + shape (cond-> shape + (some? editor-state) + (assoc :content (-> editor-state + (ted/get-editor-current-content) + (ted/export-content)))) + paragraph-ref (mf/use-state nil) handle-resize-text @@ -91,8 +101,7 @@ #(.disconnect observer))))) [:& text/text-shape {:ref text-ref-cb - :shape shape - :grow-type (:grow-type shape)}])) + :shape shape}])) (mf/defc text-wrapper {::mf/wrap-props false} @@ -118,7 +127,6 @@ [:& text-static-content {:shape shape}] [:& text-resize-content {:shape shape}])] - (when (and (not ghost?) edition?) [:& editor/text-shape-edit {:key (str "editor" (:id shape)) :shape shape}]) @@ -136,4 +144,3 @@ :on-pointer-out handle-pointer-leave :on-double-click handle-double-click :transform (gsh/transform-matrix shape)}])])) - diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index 1e40979c94..f09baf2649 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -9,190 +9,96 @@ (ns app.main.ui.workspace.shapes.text.editor (:require - ["slate" :as slate] - ["slate-react" :as rslate] - [goog.events :as events] - [rumext.alpha :as mf] + ["draft-js" :as draft] [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.util.dom :as dom] - [app.util.text :as ut] - [app.util.object :as obj] - [app.main.refs :as refs] - [app.main.store :as st] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.texts :as dwt] + [app.main.refs :as refs] + [app.main.store :as st] [app.main.ui.cursors :as cur] - [app.main.ui.shapes.text.styles :as sts]) + [app.main.ui.shapes.text.styles :as sts] + [app.util.dom :as dom] + [app.util.object :as obj] + [app.util.text-editor :as ted] + [cuerdas.core :as str] + [goog.events :as events] + [okulary.core :as l] + [rumext.alpha :as mf]) (:import goog.events.EventType goog.events.KeyCodes)) ;; --- Data functions -(defn- initial-text - [text] - (clj->js - [{:type "root" - :children [{:type "paragraph-set" - :children [{:type "paragraph" - :children [{:fill-color "#000000" - :fill-opacity 1 - :text (or text "")}]}]}]}])) -(defn- parse-content - [content] - (cond - (string? content) (initial-text content) - (map? content) (clj->js [content]) - :else (initial-text ""))) - -(defn- content-size - [node] - (let [current (count (:text node)) - children-count (->> node :children (map content-size) (reduce +))] - (+ current children-count))) - -(defn- fix-gradients - "Fix for the gradient types that need to be keywords" - [content] - (let [fix-node - (fn [node] - (d/update-in-when node [:fill-color-gradient :type] keyword))] - (ut/map-node fix-node content))) +;; TODO: why we need this? +;; (defn- fix-gradients +;; "Fix for the gradient types that need to be keywords" +;; [content] +;; (let [fix-node +;; (fn [node] +;; (d/update-in-when node [:fill-color-gradient :type] keyword))] +;; (txt/map-node fix-node content))) ;; --- Text Editor Rendering -(mf/defc editor-root-node - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [props] - (let [ - childs (obj/get props "children") - data (obj/get props "element") - type (obj/get data "type") - style (sts/generate-root-styles data props) - attrs (-> (obj/get props "attributes") - (obj/set! "style" style) - (obj/set! "className" type))] - [:> :div attrs childs])) - -(mf/defc editor-paragraph-set-node +(mf/defc block-component {::mf/wrap-props false} [props] - (let [childs (obj/get props "children") - data (obj/get props "element") - type (obj/get data "type") - shape (obj/get props "shape") - style (sts/generate-paragraph-set-styles data props) - attrs (-> (obj/get props "attributes") - (obj/set! "style" style) - (obj/set! "className" type))] - [:> :div attrs childs])) + (let [children (obj/get props "children") + bprops (obj/get props "blockProps") + style (sts/generate-paragraph-styles (obj/get bprops "shape") + (obj/get bprops "data"))] -(mf/defc editor-paragraph-node - {::mf/wrap-props false} - [props] - (let [ - childs (obj/get props "children") - data (obj/get props "element") - type (obj/get data "type") - style (sts/generate-paragraph-styles data props) - attrs (-> (obj/get props "attributes") - (obj/set! "style" style) - (obj/set! "className" type))] - [:> :p attrs childs])) + [:div {:style style :dir "auto"} + [:> draft/EditorBlock props]])) -(mf/defc editor-text-node - {::mf/wrap-props false} - [props] - (let [childs (obj/get props "children") - data (obj/get props "leaf") - type (obj/get data "type") - style (sts/generate-text-styles data props) - attrs (-> (obj/get props "attributes") - (obj/set! "style" style)) - gradient (obj/get data "fill-color-gradient" nil)] - (if gradient - (obj/set! attrs "className" (str type " gradient")) - (obj/set! attrs "className" type)) - [:> :span attrs childs])) +(defn render-block + [block shape] + (let [type (ted/get-editor-block-type block)] + (case type + "unstyled" + #js {:editable true + :component block-component + :props #js {:data (ted/get-editor-block-data block) + :shape shape}} + nil))) -(defn- render-element - [shape props] - (mf/html - (let [element (obj/get props "element") - type (obj/get element "type") - props (obj/merge! props #js {:shape shape}) - props (cond-> props - (= type "root") (obj/set! "key" "root") - (= type "paragraph-set") (obj/set! "key" "paragraph-set"))] - - (case type - "root" [:> editor-root-node props] - "paragraph-set" [:> editor-paragraph-set-node props] - "paragraph" [:> editor-paragraph-node props] - nil)))) - -(defn- render-text - [props] - (mf/html - [:> editor-text-node props])) - -;; --- Text Shape Edit +(def empty-editor-state + (ted/create-editor-state)) (mf/defc text-shape-edit-html {::mf/wrap [mf/memo] ::mf/wrap-props false ::mf/forward-ref true} [props ref] - (let [shape (unchecked-get props "shape") - node-ref (unchecked-get props "node-ref") + (let [{:keys [id x y width height grow-type content] :as shape} (obj/get props "shape") + + zoom (mf/deref refs/selected-zoom) + state-map (mf/deref refs/workspace-editor-state) + state (get state-map id empty-editor-state) - {:keys [id x y width height content grow-type]} shape - zoom (mf/deref refs/selected-zoom) - state (mf/use-state #(parse-content content)) - editor (mf/use-memo #(dwt/create-editor)) self-ref (mf/use-ref) - selecting-ref (mf/use-ref) - measure-ref (mf/use-ref) - - content-var (mf/use-var content) - - on-close - (fn [] - (st/emit! dw/clear-edition-mode) - (when (= 0 (content-size @content-var)) - (st/emit! (dws/deselect-shape id) - (dw/delete-shapes [id])))) on-click-outside (fn [event] - (let [target (dom/get-target event) - options (dom/get-element-by-class "element-options") - assets (dom/get-element-by-class "assets-bar") - cpicker (dom/get-element-by-class "colorpicker-tooltip") - palette (dom/get-element-by-class "color-palette") - self (mf/ref-val self-ref) - selecting? (mf/ref-val selecting-ref)] + (let [target (dom/get-target event) + options (dom/get-element-by-class "element-options") + assets (dom/get-element-by-class "assets-bar") + cpicker (dom/get-element-by-class "colorpicker-tooltip") + palette (dom/get-element-by-class "color-palette") - (when-not (or (and options (.contains options target)) - (and assets (.contains assets target)) - (and self (.contains self target)) - (and cpicker (.contains cpicker target)) - (and palette (.contains palette target))) - (if selecting? - (mf/set-ref-val! selecting-ref false) - (on-close))))) - - on-mouse-down - (fn [event] - (mf/set-ref-val! selecting-ref true)) - - on-mouse-up - (fn [event] - (mf/set-ref-val! selecting-ref false)) + self (mf/ref-val self-ref)] + (if (or (and options (.contains options target)) + (and assets (.contains assets target)) + (and self (.contains self target)) + (and cpicker (.contains cpicker target)) + (and palette (.contains palette target)) + (= "foreignObject" (.-tagName ^js target))) + (dom/stop-propagation event) + (st/emit! dw/clear-edition-mode)))) on-key-up (fn [event] @@ -200,86 +106,71 @@ (when (= (.-keyCode event) 27) ; ESC (do (st/emit! :interrupt) - (on-close)))) + (st/emit! dw/clear-edition-mode)))) on-mount (fn [] (let [keys [(events/listen js/document EventType.MOUSEDOWN on-click-outside) (events/listen js/document EventType.CLICK on-click-outside) (events/listen js/document EventType.KEYUP on-key-up)]] - (st/emit! (dwt/assign-editor id editor) - (dwc/start-undo-transaction)) - + (st/emit! (dwt/initialize-editor-state shape) + (dwt/select-all shape)) #(do - (st/emit! (dwt/assign-editor id nil) - (dwc/commit-undo-transaction)) + (st/emit! (dwt/finalize-editor-state shape)) (doseq [key keys] (events/unlistenByKey key))))) - on-focus + on-blur (fn [event] - (dwt/editor-select-all! editor)) - - on-composition-start - (mf/use-callback - (fn [event] - (.insertText slate/Editor editor ""))) + (dom/stop-propagation event) + (dom/prevent-default event)) on-change (mf/use-callback (fn [val] - (let [content (js->clj val :keywordize-keys true) - content (first content) - content (fix-gradients content)] - ;; Append timestamp so we can react to cursor change events - (st/emit! (dw/update-shape id {:content (assoc content :ts (js->clj (.now js/Date)))})) - (reset! state val) - (reset! content-var content))))] + (st/emit! (dwt/update-editor-state shape val)))) - (mf/use-effect on-mount) + on-editor + (mf/use-callback + (fn [editor] + (st/emit! (dwt/update-editor editor)) + (when editor + (.focus ^js editor)))) - (mf/use-effect - (mf/deps content) - (fn [] - (reset! state (parse-content content)) - (reset! content-var content))) + handle-return + (mf/use-callback + (fn [event state] + (st/emit! (dwt/update-editor-state shape (ted/editor-split-block state))) + "handled")) + ] - [:div.text-editor {:ref self-ref} - [:style "span { line-height: inherit; } - .gradient { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"] - [:> rslate/Slate {:editor editor - :value @state - :on-change on-change} - [:> rslate/Editable - {:auto-focus "true" - :spell-check "false" - :on-focus on-focus - :class "rich-text" - :style {:cursor cur/text - :width (:width shape)} - :render-element #(render-element shape %) - :render-leaf render-text - :on-mouse-up on-mouse-up - :on-mouse-down on-mouse-down - :on-blur (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - ;; WARN: monky patch - (obj/set! slate/Transforms "deselect" (constantly nil))) - :on-composition-start on-composition-start - ;; :placeholder (when (= :fixed grow-type) "Type some text here...") - }]]])) + (mf/use-layout-effect on-mount) + + [:div.text-editor {:ref self-ref + :class (dom/classnames + :align-top (= (:vertical-align content "top") "top") + :align-center (= (:vertical-align content) "center") + :align-bottom (= (:vertical-align content) "bottom"))} + [:> draft/Editor + {:on-change on-change + :on-blur on-blur + :handle-return handle-return + :custom-style-fn (fn [styles _] + (-> (ted/styles-to-attrs styles) + (sts/generate-text-styles))) + :block-renderer-fn #(render-block % shape) + :ref on-editor + :editor-state state}]])) (mf/defc text-shape-edit {::mf/wrap [mf/memo] ::mf/wrap-props false ::mf/forward-ref true} [props ref] - (let [shape (unchecked-get props "shape") - {:keys [x y width height grow-type]} shape] + (let [{:keys [id x y width height grow-type] :as shape} (obj/get props "shape")] [:foreignObject {:transform (gsh/transform-matrix shape) :x x :y y :width (if (#{:auto-width} grow-type) 100000 width) :height (if (#{:auto-height :auto-width} grow-type) 100000 height)} - [:& text-shape-edit-html {:shape shape}]])) + [:& text-shape-edit-html {:shape shape :key (str id)}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 7ce343ebce..7b86944073 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -14,6 +14,7 @@ [app.common.geom.shapes :as geom] [app.common.media :as cm] [app.common.pages :as cp] + [app.common.text :as txt] [app.common.uuid :as uuid] [app.config :as cfg] [app.main.data.colors :as dc] @@ -38,7 +39,6 @@ [app.util.i18n :as i18n :refer [tr t]] [app.util.keyboard :as kbd] [app.util.router :as rt] - [app.util.text :as ut] [app.util.timers :as timers] [cuerdas.core :as str] [okulary.core :as l] @@ -431,7 +431,7 @@ (mf/use-callback (mf/deps file-id) (fn [value opacity] - (st/emit! (dwl/add-typography ut/default-typography)))) + (st/emit! (dwl/add-typography txt/default-typography)))) handle-change (mf/use-callback diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 3a5491aa2b..d2e8725e02 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -30,8 +30,8 @@ :fill-color-gradient]) (mf/defc fill-menu - {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "editor" "values"]))]} - [{:keys [ids type values editor] :as props}] + {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values"]))]} + [{:keys [ids type values] :as props}] (let [locale (mf/deref i18n/locale) show? (or (not (nil? (:fill-color values))) (not (nil? (:fill-color-gradient values)))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index aaaddf6299..c1c4c780ec 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -11,6 +11,7 @@ (:require [app.common.data :as d] [app.common.uuid :as uuid] + [app.common.text :as txt] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.texts :as dwt] @@ -22,7 +23,6 @@ [app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry typography-options]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.text :as ut] [cuerdas.core :as str] [rumext.alpha :as mf])) @@ -49,7 +49,7 @@ (def attrs (d/concat #{} shape-attrs root-attrs paragraph-attrs text-attrs)) (mf/defc text-align-options - [{:keys [editor ids values on-change] :as props}] + [{:keys [ids values on-change] :as props}] (let [{:keys [text-align]} values text-align (or text-align "left") @@ -83,7 +83,7 @@ (mf/defc vertical-align - [{:keys [shapes editor ids values on-change] :as props}] + [{:keys [shapes ids values on-change] :as props}] (let [{:keys [vertical-align]} values vertical-align (or vertical-align "top") handle-change @@ -108,7 +108,7 @@ i/align-bottom]])) (mf/defc grow-options - [{:keys [editor ids values on-change] :as props}] + [{:keys [ids values on-change] :as props}] (let [to-single-value (fn [coll] (if (> (count coll) 1) nil (first coll))) grow-type (->> values :grow-type) handle-change-grow @@ -133,7 +133,7 @@ i/auto-height]])) (mf/defc text-decoration-options - [{:keys [editor ids values on-change] :as props}] + [{:keys [ids values on-change] :as props}] (let [{:keys [text-decoration]} values text-decoration (or text-decoration "none") @@ -160,14 +160,14 @@ :on-click #(handle-change % "line-through")} i/strikethrough]])) -(defn generate-typography-name [{:keys [font-id font-variant-id] :as typography}] +(defn generate-typography-name + [{:keys [font-id font-variant-id] :as typography}] (let [{:keys [name]} (fonts/get-font-data font-id)] - (-> typography - (assoc :name (str name " " (str/title font-variant-id))))) ) + (assoc typography :name (str name " " (str/title font-variant-id))))) (mf/defc text-menu {::mf/wrap [mf/memo]} - [{:keys [ids type editor values] :as props}] + [{:keys [ids type values] :as props}] (let [current-file-id (mf/use-ctx ctx/current-file-id) typographies (mf/deref refs/workspace-file-typography) @@ -181,15 +181,15 @@ (fn [id attrs] (let [attrs (select-keys attrs root-attrs)] (when-not (empty? attrs) - (st/emit! (dwt/update-root-attrs {:id id :editor editor :attrs attrs})))) + (st/emit! (dwt/update-root-attrs {:id id :attrs attrs})))) (let [attrs (select-keys attrs paragraph-attrs)] (when-not (empty? attrs) - (st/emit! (dwt/update-paragraph-attrs {:id id :editor editor :attrs attrs})))) + (st/emit! (dwt/update-paragraph-attrs {:id id :attrs attrs})))) (let [attrs (select-keys attrs text-attrs)] (when-not (empty? attrs) - (st/emit! (dwt/update-text-attrs {:id id :editor editor :attrs attrs}))))) + (st/emit! (dwt/update-text-attrs {:id id :attrs attrs}))))) typography (cond (and (:typography-ref-id values) @@ -213,7 +213,7 @@ (d/concat text-font-attrs text-spacing-attrs text-transform-attrs))) - typography (merge ut/default-typography setted-values) + typography (merge txt/default-typography setted-values) typography (generate-typography-name typography)] (let [id (uuid/next)] (st/emit! (dwl/add-typography (assoc typography :id id) false)) @@ -230,8 +230,7 @@ (fn [changes] (st/emit! (dwl/update-typography (merge typography changes) current-file-id))) - opts #js {:editor editor - :ids ids + opts #js {:ids ids :values values :on-change (fn [attrs] (run! #(emit-update! % attrs) ids))}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 42daf38206..2ccde0b281 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -5,25 +5,25 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) 2020-2021 UXBOX Labs SL (ns app.main.ui.workspace.sidebar.options.menus.typography (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [app.main.ui.icons :as i] + [app.common.data :as d] + [app.common.text :as txt] + [app.main.data.workspace.texts :as dwt] + [app.main.fonts :as fonts] [app.main.refs :as refs] [app.main.store :as st] - [app.common.data :as d] - [app.main.data.workspace.texts :as dwt] [app.main.ui.components.editable-select :refer [editable-select]] + [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] - [app.main.fonts :as fonts] [app.util.dom :as dom] - [app.util.text :as ut] - [app.util.timers :as ts] [app.util.i18n :as i18n :refer [t]] - [app.util.router :as rt])) + [app.util.router :as rt] + [app.util.timers :as ts] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (defn- attr->string [value] (if (= value :multiple) @@ -51,9 +51,9 @@ font-size font-variant-id]} values - font-id (or font-id (:font-id ut/default-text-attrs)) - font-size (or font-size (:font-size ut/default-text-attrs)) - font-variant-id (or font-variant-id (:font-variant-id ut/default-text-attrs)) + font-id (or font-id (:font-id txt/default-text-attrs)) + font-size (or font-size (:font-size txt/default-text-attrs)) + font-variant-id (or font-variant-id (:font-variant-id txt/default-text-attrs)) fonts (mf/deref fonts/fontsdb) font (get fonts font-id) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 0e4a47ab27..a191e7b41d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -9,17 +9,17 @@ (ns app.main.ui.workspace.sidebar.options.shapes.multiple (:require - [app.common.data :as d] - [rumext.alpha :as mf] [app.common.attrs :as attrs] - [app.util.text :as ut] - [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] - [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] - [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-attrs shadow-menu]] + [app.common.data :as d] + [app.common.text :as txt] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] + [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] + [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-attrs shadow-menu]] [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] [app.main.ui.workspace.sidebar.options.menus.text :as ot] - [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]])) + [rumext.alpha :as mf])) ;; We define a map that goes from type to ;; attribute and how to handle them @@ -161,7 +161,7 @@ :text [(conj ids id) (-> values (merge-attrs (select-keys shape attrs)) - (merge-attrs (ut/get-text-attrs-multi content attrs)))] + (merge-attrs (attrs/get-attrs-multi (txt/node-seq content) attrs)))] :children (let [children (->> (:shapes shape []) (map #(get objects %))) [new-ids new-values] (get-attrs children objects attr-type)] [(d/concat ids new-ids) (merge-attrs values new-values)]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 7ff4071ad3..b6304eeb1b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -21,18 +21,16 @@ (mf/defc options [{:keys [shape] :as props}] - (let [ids [(:id shape)] - type (:type shape) + (let [ids [(:id shape)] + type (:type shape) - editors (mf/deref refs/editors) - editor (get editors (:id shape)) + state-map (mf/deref refs/workspace-editor-state) + editor-state (get state-map (:id shape)) - measure-values (select-keys shape measure-attrs) - - fill-values (dwt/current-text-values - {:editor editor - :shape shape - :attrs text-fill-attrs}) + fill-values (dwt/current-text-values + {:editor-state editor-state + :shape shape + :attrs text-fill-attrs}) fill-values (d/update-in-when fill-values [:fill-color-gradient :type] keyword) @@ -41,32 +39,42 @@ (:fill fill-values) (assoc :fill-color (:fill fill-values)) (:opacity fill-values) (assoc :fill-opacity (:fill fill-values))) - text-values (merge - (select-keys shape [:grow-type]) - (dwt/current-root-values - {:editor editor :shape shape - :attrs root-attrs}) - (dwt/current-text-values - {:editor editor :shape shape + (select-keys shape [:grow-type :vertical-align :text-align]) + #_(dwt/current-root-values + {:editor-state editor-state + :shape shape + :attrs root-attrs}) + (dwt/current-paragraph-values + {:editor-state editor-state + :shape shape :attrs paragraph-attrs}) (dwt/current-text-values - {:editor editor :shape shape + {:editor-state editor-state + :shape shape :attrs text-attrs}))] [:* - [:& measures-menu {:ids ids - :type type - :values measure-values}] - [:& fill-menu {:ids ids - :type type - :values fill-values - :editor editor}] - [:& shadow-menu {:ids ids - :values (select-keys shape [:shadow])}] - [:& blur-menu {:ids ids - :values (select-keys shape [:blur])}] - [:& text-menu {:ids ids - :type type - :values text-values - :editor editor}]])) + + [:& measures-menu + {:ids ids + :type type + :values (select-keys shape measure-attrs)}] + + [:& fill-menu + {:ids ids + :type type + :values fill-values}] + + [:& shadow-menu + {:ids ids + :values (select-keys shape [:shadow])}] + + [:& blur-menu + {:ids ids + :values (select-keys shape [:blur])}] + + [:& text-menu + {:ids ids + :type type + :values text-values}]])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index fb27888cec..32c2148147 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -434,11 +434,13 @@ on-pointer-down (mf/use-callback (fn [event] - (let [target (dom/get-target event)] - ; Capture mouse pointer to detect the movements even if cursor - ; leaves the viewport or the browser itself - ; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture - (.setPointerCapture target (.-pointerId event))))) + (let [target (dom/get-target event) + closest (.closest target ".public-DraftStyleDefault-block")] + (when-not closest + ;; Capture mouse pointer to detect the movements even if cursor + ;; leaves the viewport or the browser itself + ;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture + (.setPointerCapture target (.-pointerId event)))))) on-pointer-up (mf/use-callback diff --git a/frontend/src/app/util/code_gen.cljs b/frontend/src/app/util/code_gen.cljs index 86e0bd8f5e..8338fd621f 100644 --- a/frontend/src/app/util/code_gen.cljs +++ b/frontend/src/app/util/code_gen.cljs @@ -5,14 +5,15 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2020 UXBOX Labs SL +;; Copyright (c) 2020-2021 UXBOX Labs SL (ns app.util.code-gen (:require - [cuerdas.core :as str] + [app.common.data :as d] [app.common.math :as mth] - [app.util.text :as ut] - [app.util.color :as uc])) + [app.common.text :as txt] + [app.util.color :as uc] + [cuerdas.core :as str])) (defn shadow->css [shadow] (let [{:keys [style offset-x offset-y blur spread]} shadow @@ -136,17 +137,55 @@ :format format :multi multi :tab-size 2}))) + +(defn search-text-attrs + [node attrs] + (->> (txt/node-seq node) + (map #(select-keys % attrs)) + (reduce d/merge))) + + +;; TODO: used on handoff +(defn parse-style-text-blocks + [node attrs] + (letfn + [(rec-style-text-map [acc node style] + (let [node-style (merge style (select-keys node attrs)) + head (or (-> acc first) [{} ""]) + [head-style head-text] head + + new-acc + (cond + (:children node) + (reduce #(rec-style-text-map %1 %2 node-style) acc (:children node)) + + (not= head-style node-style) + (cons [node-style (:text node "")] acc) + + :else + (cons [node-style (str head-text "" (:text node))] (rest acc))) + + ;; We add an end-of-line when finish a paragraph + new-acc + (if (= (:type node) "paragraph") + (let [[hs ht] (first new-acc)] + (cons [hs (str ht "\n")] (rest new-acc))) + new-acc)] + new-acc))] + + (-> (rec-style-text-map [] node {}) + reverse))) + (defn text->properties [shape] (let [text-shape-style (select-keys styles-data [:layout :shadow :blur]) - shape-props (->> text-shape-style vals (mapcat :props)) - shape-to-prop (->> text-shape-style vals (map :to-prop) (reduce merge)) - shape-format (->> text-shape-style vals (map :format) (reduce merge)) + shape-props (->> text-shape-style vals (mapcat :props)) + shape-to-prop (->> text-shape-style vals (map :to-prop) (reduce merge)) + shape-format (->> text-shape-style vals (map :format) (reduce merge)) - text-values (->> (ut/search-text-attrs (:content shape) (conj (:props style-text) :fill-color-gradient)) - (merge ut/default-text-attrs))] - + text-values (->> (search-text-attrs (:content shape) (conj (:props style-text) :fill-color-gradient)) + (d/merge txt/default-text-attrs))] (str/join "\n" [(generate-css-props shape diff --git a/frontend/src/app/util/text.cljs b/frontend/src/app/util/text.cljs deleted file mode 100644 index ef75f65353..0000000000 --- a/frontend/src/app/util/text.cljs +++ /dev/null @@ -1,123 +0,0 @@ -(ns app.util.text - (:require - [cuerdas.core :as str] - [app.common.attrs :refer [get-attrs-multi]])) - -(defonce default-text-attrs - {:typography-ref-file nil - :typography-ref-id nil - :font-id "sourcesanspro" - :font-family "sourcesanspro" - :font-variant-id "regular" - :font-size "14" - :font-weight "400" - :font-style "normal" - :line-height "1.2" - :letter-spacing "0" - :text-transform "none" - :text-align "left" - :text-decoration "none" - :fill-color nil - :fill-opacity 1}) - -(def typography-fields - [:font-id - :font-family - :font-variant-id - :font-size - :font-weight - :font-style - :line-height - :letter-spacing - :text-transform]) - -(def default-typography - (merge - {:name "Source Sans Pro Regular"} - (select-keys default-text-attrs typography-fields))) - -(defn some-node - [predicate node] - (or (predicate node) - (some #(some-node predicate %) (:children node)))) - -(defn map-node - [map-fn node] - (cond-> (map-fn node) - (:children node) (update :children (fn [children] (mapv #(map-node map-fn %) children))))) - -(defn content->text - [node] - (str - (if (:children node) - (str/join (if (= "paragraph-set" (:type node)) "\n" "") (map content->text (:children node))) - (:text node "")))) - -(defn parse-style-text-blocks - [node attrs] - (letfn - [(rec-style-text-map [acc node style] - (let [node-style (merge style (select-keys node attrs)) - head (or (-> acc first) [{} ""]) - [head-style head-text] head - - new-acc - (cond - (:children node) - (reduce #(rec-style-text-map %1 %2 node-style) acc (:children node)) - - (not= head-style node-style) - (cons [node-style (:text node "")] acc) - - :else - (cons [node-style (str head-text "" (:text node))] (rest acc))) - - ;; We add an end-of-line when finish a paragraph - new-acc - (if (= (:type node) "paragraph") - (let [[hs ht] (first new-acc)] - (cons [hs (str ht "\n")] (rest new-acc))) - new-acc)] - new-acc))] - - (-> (rec-style-text-map [] node {}) - reverse))) - -(defn search-text-attrs - [node attrs] - (let [rec-fn - (fn rec-fn [current node] - (let [current (reduce rec-fn current (:children node []))] - (merge current - (select-keys node attrs))))] - (rec-fn {} node))) - - -(defn content->nodes [node] - (loop [result (transient []) - curr node - pending (transient [])] - - (let [result (conj! result curr)] - ;; Adds children to the pending list - (let [children (:children curr) - pending (loop [child (first children) - children (rest children) - pending pending] - (if child - (recur (first children) - (rest children) - (conj! pending child)) - pending))] - - (if (= 0 (count pending)) - (persistent! result) - ;; Iterates with the next value in pending - (let [next (get pending (dec (count pending)))] - (recur result next (pop! pending)))))))) - -(defn get-text-attrs-multi - [node attrs] - (let [nodes (content->nodes node)] - (get-attrs-multi nodes attrs))) - diff --git a/frontend/src/app/util/text_editor.cljs b/frontend/src/app/util/text_editor.cljs new file mode 100644 index 0000000000..88e8e5253c --- /dev/null +++ b/frontend/src/app/util/text_editor.cljs @@ -0,0 +1,298 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020-2021 UXBOX Labs SL + +(ns app.util.text-editor + "Draft related abstraction functions." + (:require + ["draft-js" :as draft] + [app.common.attrs :as attrs] + [app.common.text :as txt] + [app.common.data :as d] + [app.util.transit :as t] + [app.util.array :as arr] + [app.util.object :as obj] + [clojure.walk :as walk] + [cuerdas.core :as str])) + +;; --- INLINE STYLES ENCODING + +(defn encode-style-value + [v] + (cond + (string? v) (str "s:" v) + (number? v) (str "n:" v) + (keyword? v) (str "k:" (name v)) + (map? v) (str "m:" (t/encode v)) + + :else (str "o:" v))) + +(defn decode-style-value + [v] + (let [prefix (subs v 0 2)] + (case prefix + "s:" (subs v 2) + "n:" (js/Number (subs v 2)) + "k:" (keyword (subs v 2)) + "m:" (t/decode (subs v 2)) + "o:" (subs v 2) + v))) + +(defn encode-style + [key val] + (let [k (d/name key) + v (encode-style-value val)] + (str "PENPOT$$$" k "$$$" v))) + +(defn attrs-to-styles + [attrs] + (reduce-kv (fn [res k v] + (conj res (encode-style k v))) + #{} + attrs)) + +(defn styles-to-attrs + [styles] + (persistent! + (reduce (fn [result style] + (let [[_ k v] (str/split style "$$$" 3)] + (assoc! result (keyword k) (decode-style-value v)))) + (transient {}) + (seq styles)))) + +;; --- CONVERSION + +(defn- parse-draft-styles + "Parses draft-js style ranges, converting encoded style name into a + key/val pair of data." + [styles] + (map (fn [item] + (let [[_ k v] (-> (obj/get item "style") + (str/split "$$$" 3))] + {:key (keyword k) + :val (decode-style-value v) + :offset (obj/get item "offset") + :length (obj/get item "length")})) + styles)) + +(defn- build-style-index + "Generates a character based index with associated styles map." + [text ranges] + (loop [result (->> (range (count text)) + (mapv (constantly {})) + (transient)) + ranges (seq ranges)] + (if-let [{:keys [offset length] :as item} (first ranges)] + (recur (reduce (fn [result index] + (let [prev (get result index)] + (assoc! result index (assoc prev (:key item) (:val item))))) + result + (range offset (+ offset length))) + (rest ranges)) + (persistent! result)))) + +(defn- convert-from-draft + [content] + (letfn [(build-text [text part] + (let [start (ffirst part) + end (inc (first (last part)))] + (-> (second (first part)) + (assoc :text (subs text start end))))) + + (split-texts [text styles] + (->> (parse-draft-styles styles) + (build-style-index text) + (d/enumerate) + (partition-by second) + (mapv #(build-text text %)))) + + (build-paragraph [block] + (let [key (obj/get block "key") + text (obj/get block "text") + styles (obj/get block "inlineStyleRanges") + data (obj/get block "data")] + (-> (js->clj data :keywordize-keys true) + (assoc :key key) + (assoc :type "paragraph") + (assoc :children (split-texts text styles)))))] + + {:type "root" + :children + [{:type "paragraph-set" + :children (->> (obj/get content "blocks") + (mapv build-paragraph))}]})) + +(defn- convert-to-draft + [root] + (letfn [(process-attr [children ranges [k v]] + (loop [children (seq children) + start nil + offset 0 + ranges ranges] + (if-let [{:keys [text] :as item} (first children)] + (if (= v (get item k ::novalue)) + (recur (rest children) + (if (nil? start) offset start) + (+ offset (alength text)) + ranges) + (if (some? start) + (recur (rest children) + nil + (+ offset (alength text)) + (arr/conj! ranges #js {:offset start + :length (- offset start) + :style (encode-style k v)})) + (recur (rest children) + start + (+ offset (alength text)) + ranges))) + (cond-> ranges + (some? start) + (arr/conj! #js {:offset start + :length (- offset start) + :style (encode-style k v)}))))) + + (calc-ranges [{:keys [children] :as blok}] + (let [xform (comp (map #(dissoc % :key :text)) + (remove empty?) + (mapcat vec) + (distinct)) + proc #(process-attr children %1 %2)] + (transduce xform proc #js [] children))) + + (build-block [result {:keys [key children] :as paragraph}] + (->> #js {:key key + :depth 0 + :text (apply str (map :text children)) + :data (-> (dissoc paragraph :key :children :type) + (clj->js)) + :type "unstyled" + :entityRanges #js [] + :inlineStyleRanges (calc-ranges paragraph)} + (arr/conj! result)))] + + #js {:blocks (reduce build-block #js [] (txt/node-seq #(= (:type %) "paragraph") root)) + :entityMap #js {}})) + +(defn immutable-map->map + [obj] + (into {} (map (fn [[k v]] [(keyword k) v])) (seq obj))) + + +;; --- DRAFT-JS HELPERS + +(defn create-editor-state + ([] + (.createEmpty ^js draft/EditorState)) + ([content] + (if (some? content) + (.createWithContent ^js draft/EditorState content) + (.createEmpty ^js draft/EditorState)))) + +(defn import-content + [content] + (-> content convert-to-draft draft/convertFromRaw)) + +(defn export-content + [content] + (-> content + (draft/convertToRaw) + (convert-from-draft))) + +(defn get-editor-current-content + [state] + (.getCurrentContent ^js state)) + +(defn ^boolean content-has-text? + [content] + (.hasText ^js content)) + +(defn editor-select-all + [state] + (let [content (get-editor-current-content state) + fblock (.. ^js content getBlockMap first) + lblock (.. ^js content getBlockMap last) + fbk (.getKey ^js fblock) + lbk (.getKey ^js lblock) + lbl (.getLength ^js lblock) + params #js {:anchorKey fbk + :anchorOffset 0 + :focusKey lbk + :focusOffset lbl} + selection (draft/SelectionState. params)] + (.forceSelection ^js draft/EditorState state selection))) + +(defn get-editor-block-data + [block] + (-> (.getData ^js block) + (immutable-map->map))) + +(defn get-editor-block-type + [block] + (.getType ^js block)) + +(defn get-editor-current-block-data + [state] + (let [content (.getCurrentContent ^js state) + key (.. ^js state getSelection getStartKey) + block (.getBlockForKey ^js content key)] + (get-editor-block-data block))) + +(defn get-editor-current-inline-styles + [state] + (-> (.getCurrentInlineStyle ^js state) + (styles-to-attrs))) + +(defn update-editor-current-block-data + [state attrs] + (loop [selection (.getSelection ^js state) + start-key (.getStartKey ^js selection) + end-key (.getEndKey ^js selection) + content (.getCurrentContent ^js state) + target selection] + (if (and (not= start-key end-key) + (zero? (.getEndOffset ^js selection))) + (let [before-block (.getBlockBefore ^js content end-key)] + (recur selection + start-key + (.getKey ^js before-block) + content + (.merge ^js target + #js {:anchorKey start-key + :anchorOffset (.getStartOffset ^js selection) + :focusKey end-key + :focusOffset (.getLength ^js before-block) + :isBackward false}))) + (.push ^js draft/EditorState + state + (.mergeBlockData ^js draft/Modifier content target (clj->js attrs)) + "change-block-data")))) + +(defn update-editor-current-inline-styles + [state attrs] + (let [selection (.getSelection ^js state) + content (.getCurrentContent ^js state) + styles (attrs-to-styles attrs)] + (reduce (fn [state style] + (let [modifier (.applyInlineStyle draft/Modifier + (.getCurrentContent ^js state) + selection + style)] + (.push draft/EditorState state modifier "change-inline-style"))) + state + styles))) + +(defn editor-split-block + [state] + (let [content (.getCurrentContent ^js state) + selection (.getSelection ^js state) + content (.splitBlock ^js draft/Modifier content selection) + block-data (.. ^js content -blockMap (get (.. content -selectionBefore getStartKey)) getData) + block-key (.. ^js content -selectionAfter getStartKey) + block-map (.. ^js content -blockMap (update block-key (fn [block] (.set ^js block "data" block-data))))] + (.push ^js draft/EditorState state (.set ^js content "blockMap" block-map) "split-block"))) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index ecd93d7e41..17778502fd 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -255,6 +255,11 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + asn1.js@^5.2.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -996,6 +1001,11 @@ core-js-pure@^3.0.0: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.9.1.tgz#677b322267172bd490e4464696f790cbc355bec5" integrity sha512-laz3Zx0avrw9a4QEIdmIblnVuJz8W51leY9iLThatCsFawWxC3sE4guASC78JbCin+DkwMpCdp1AVAuzL/GN7A== +core-js@^3.6.4: + version "3.9.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae" + integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1042,6 +1052,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-fetch@^3.0.4: + version "3.0.6" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" + integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" @@ -1336,6 +1353,15 @@ domutils@^1.7.0: dom-serializer "0" domelementtype "1" +draft-js@^0.11.7: + version "0.11.7" + resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.11.7.tgz#be293aaa255c46d8a6647f3860aa4c178484a206" + integrity sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg== + dependencies: + fbjs "^2.0.0" + immutable "~3.7.4" + object-assign "^4.1.1" + duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" @@ -1661,6 +1687,25 @@ fast-safe-stringify@^2.0.4: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== +fbjs-css-vars@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" + integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== + +fbjs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-2.0.0.tgz#01fb812138d7e31831ed3e374afe27b9169ef442" + integrity sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ== + dependencies: + core-js "^3.6.4" + cross-fetch "^3.0.4" + fbjs-css-vars "^1.0.0" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.18" + fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -2316,6 +2361,11 @@ immer@^5.0.0: resolved "https://registry.yarnpkg.com/immer/-/immer-5.3.6.tgz#51eab8cbbeb13075fe2244250f221598818cac04" integrity sha512-pqWQ6ozVfNOUDjrLfm4Pt7q4Q12cGw2HUZgry4Q5+Myxu9nmHRkWBpI0J4+MK0AxbdFtdMTwEGVl7Vd+vEiK+A== +immutable@~3.7.4: + version "3.7.6" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" + integrity sha1-E7TTyxK++hVIKib+Gy665kAHHks= + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -3015,7 +3065,7 @@ logform@^2.2.0: ms "^2.1.1" triple-beam "^1.3.0" -loose-envify@^1.1.0: +loose-envify@^1.0.0, loose-envify@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -3045,11 +3095,6 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" -luxon@~1.25.0: - version "1.25.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.25.0.tgz#d86219e90bc0102c0eb299d65b2f5e95efe1fe72" - integrity sha512-hEgLurSH8kQRjY6i4YLey+mcKVAWXbDNlZRmM6AgWDJ1cY3atl8Ztf5wEY7VBReFbmGnwQPz7KYJblL8B2k0jQ== - make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -3341,6 +3386,11 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-fetch@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-gyp@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" @@ -3944,6 +3994,13 @@ progress@^1.1.8: resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -4423,7 +4480,7 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4: +setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= @@ -5125,6 +5182,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +ua-parser-js@^0.7.18: + version "0.7.24" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.24.tgz#8d3ecea46ed4f1f1d63ec25f17d8568105dc027c" + integrity sha512-yo+miGzQx5gakzVK3QFfN0/L9uVhosXBBO7qmnk7c2iw1IhL212wfA3zbnI54B0obGwC/5NWub/iT9sReMx+Fw== + ultron@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"