♻️ Replace slate editor with draft-js.

This commit is contained in:
Andrey Antukh 2021-03-15 08:43:23 +01:00
parent 439e5ee6a1
commit 3bef80932d
28 changed files with 1272 additions and 981 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 [_]

View file

@ -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))

View file

@ -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]))

View file

@ -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}])]))

View file

@ -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?}]]))

View file

@ -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])))

View file

@ -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))

View file

@ -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

View file

@ -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)}])]))

View file

@ -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)}]]))

View file

@ -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

View file

@ -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))))

View file

@ -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))}]

View file

@ -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)

View file

@ -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)])

View file

@ -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}]]))

View file

@ -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

View file

@ -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

View file

@ -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)))

View file

@ -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")))