♻️ 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

@ -7,7 +7,8 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; 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. ;; Extract some attributes of a list of shapes.
;; For each attribute, if the value is the same in all shapes, ;; For each attribute, if the value is the same in all shapes,
@ -48,7 +49,6 @@
(loop [attr (first attrs) (loop [attr (first attrs)
attrs (rest attrs) attrs (rest attrs)
result (transient {})] result (transient {})]
(if attr (if attr
(let [value (let [value
(loop [curr (first objs) (loop [curr (first objs)
@ -75,3 +75,12 @@
(persistent! result))))) (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))

View file

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

View file

@ -34,10 +34,10 @@
"shadow-cljs": "^2.11.20" "shadow-cljs": "^2.11.20"
}, },
"dependencies": { "dependencies": {
"humanize-duration": "~3.25.0",
"luxon": "~1.25.0",
"date-fns": "^2.19.0", "date-fns": "^2.19.0",
"draft-js": "^0.11.7",
"highlight.js": "^10.6.0", "highlight.js": "^10.6.0",
"humanize-duration": "~3.25.0",
"js-beautify": "^1.13.5", "js-beautify": "^1.13.5",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",

View file

@ -1,5 +1,69 @@
foreignObject .rich-text { foreignObject {
color: $color-black; .text-editor, .rich-text {
height: 100%; color: $color-black;
white-space: pre-wrap; 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;
}
}
} }

View file

@ -33,7 +33,6 @@
[app.main.data.workspace.notifications :as dwn] [app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.persistence :as dwp]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.transforms :as dwt]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
@ -603,22 +602,6 @@
(let [selected (get-in state [:workspace-local :selected])] (let [selected (get-in state [:workspace-local :selected])]
(rx/from (map #(update-shape % attrs) 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) ;; --- Shape Movement (using keyboard shorcuts)
(declare initial-selection-align) (declare initial-selection-align)
@ -649,119 +632,13 @@
;; --- Delete Selected ;; --- 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 (def delete-selected
"Deselect all and remove all selected shapes." "Deselect all and remove all selected shapes."
(ptk/reify ::delete-selected (ptk/reify ::delete-selected
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [selected (get-in state [:workspace-local :selected])] (let [selected (get-in state [:workspace-local :selected])]
(rx/of (delete-shapes selected) (rx/of (dwc/delete-shapes selected)
(dws/deselect-all)))))) (dws/deselect-all))))))
;; --- Shape Vertical Ordering ;; --- Shape Vertical Ordering

View file

@ -395,7 +395,6 @@
;; Shapes ;; Shapes
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn expand-all-parents (defn expand-all-parents
[ids objects] [ids objects]
(ptk/reify ::expand-all-parents (ptk/reify ::expand-all-parents
@ -672,6 +671,114 @@
:shapes [shape-id]})))] :shapes [shape-id]})))]
(rx/of (commit-changes rchanges uchanges {:commit-local? true})))))) (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 ;; --- Add shape to Workspace
(defn- viewport-center (defn- viewport-center

View file

@ -5,20 +5,20 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; 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 (ns app.main.data.workspace.libraries-helpers
(:require (:require
[cljs.spec.alpha :as s]
[clojure.set :as set]
[app.common.spec :as us]
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.spec :as us]
[app.common.text :as txt]
[app.main.data.workspace.groups :as dwg] [app.main.data.workspace.groups :as dwg]
[app.util.logging :as log] [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 ;; Change this to :info :debug or :trace to debug this module
(log/set-level! :warn) (log/set-level! :warn)
@ -317,11 +317,11 @@
(->> shape (->> shape
:content :content
;; Check if any node in the content has a reference for the library ;; Check if any node in the content has a reference for the library
(ut/some-node (txt/node-seq
#(or (and (some? (:stroke-color-ref-id %)) #(or (and (some? (:stroke-color-ref-id %))
(= library-id (:stroke-color-ref-file %))) (= library-id (:stroke-color-ref-file %)))
(and (some? (:fill-color-ref-id %)) (and (some? (:fill-color-ref-id %))
(= library-id (:fill-color-ref-file %)))))) (= library-id (:fill-color-ref-file %))))))
(some (some
#(let [attr (name %) #(let [attr (name %)
attr-ref-id (keyword (str attr "-ref-id")) attr-ref-id (keyword (str attr "-ref-id"))
@ -336,9 +336,9 @@
(->> shape (->> shape
:content :content
;; Check if any node in the content has a reference for the library ;; Check if any node in the content has a reference for the library
(ut/some-node (txt/node-seq
#(and (some? (:typography-ref-id %)) #(and (some? (:typography-ref-id %))
(= library-id (:typography-ref-file %))))))))) (= library-id (:typography-ref-file %)))))))))
(defmulti generate-sync-shape (defmulti generate-sync-shape
"Generate changes to synchronize one shape with all assets of the given type "Generate changes to synchronize one shape with all assets of the given type
@ -356,7 +356,7 @@
(defn- generate-sync-text-shape (defn- generate-sync-text-shape
[shape container update-node] [shape container update-node]
(let [old-content (:content shape) (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 rchanges [(make-change
container container
{:type :mod-obj {:type :mod-obj

View file

@ -5,199 +5,188 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; 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 (ns app.main.data.workspace.texts
(:require (:require
["slate" :as slate :refer [Editor Node Transforms Text]]
["slate-react" :as rslate]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.attrs :as attrs] [app.common.attrs :as attrs]
[app.common.text :as txt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.pages :as cp] [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.common :as dwc]
[app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.transforms :as dwt]
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.util.object :as obj] [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] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[clojure.walk :as walk]
[goog.object :as gobj] [goog.object :as gobj]
[cuerdas.core :as str]
[potok.core :as ptk])) [potok.core :as ptk]))
(defn create-editor (defn update-editor
[] [editor]
(rslate/withReact (slate/createEditor))) (ptk/reify ::update-editor
(defn assign-editor
[id editor]
(ptk/reify ::assign-editor
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (if (some? editor)
(assoc-in [:workspace-local :editors id] editor) (assoc state :workspace-editor editor)
(update-in [:workspace-local :editor-n] (fnil inc 0)))))) (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 ;; --- 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 (defn- shape-current-values
[shape pred attrs] [shape pred attrs]
(let [root (:content shape) (let [root (:content shape)
nodes (->> (nodes-seq pred root) nodes (->> (txt/node-seq pred root)
(map #(if (is-text-node? %) (map #(if (txt/is-text-node? %)
(merge ut/default-text-attrs %) (merge txt/default-text-attrs %)
%)))] %)))]
(attrs/get-attrs-multi nodes 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 (defn current-paragraph-values
[{:keys [editor attrs shape]}] [{:keys [editor-state attrs shape]}]
(if editor (if editor-state
(editor-current-values editor is-paragraph-node? attrs false) (-> (ted/get-editor-current-block-data editor-state)
(shape-current-values shape is-paragraph-node? attrs))) (select-keys attrs))
(shape-current-values shape txt/is-paragraph-node? attrs)))
(defn current-root-values (defn current-text-values
[{:keys [editor attrs shape]}] [{:keys [editor-state attrs shape]}]
(if editor (if editor-state
(editor-current-values editor is-root-node? attrs false) (-> (ted/get-editor-current-inline-styles editor-state)
(shape-current-values shape is-root-node? attrs))) (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 ;; --- TEXT EDITION IMPL
([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 %)))))
(defn update-attrs (defn- update-shape
[{:keys [id editor attrs pred split] [shape pred-fn attrs]
:or {pred is-text-node?}}] (let [merge-attrs #(attrs/merge % attrs)
(if editor transform #(txt/transform-nodes pred-fn merge-attrs %)]
(ptk/reify ::update-attrs (update shape :content transform)))
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-root-attrs (defn update-root-attrs
[options] [{:keys [id attrs]}]
(update-attrs (assoc options :pred is-root-node? :split false))) (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] (defn update-overflow-text [id value]
(ptk/reify ::update-overflow-text (ptk/reify ::update-overflow-text
@ -211,7 +200,7 @@
(ptk/reify ::start-edit-if-selected (ptk/reify ::start-edit-if-selected
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [objects (dwc/lookup-page-objects state) (let [objects (dwc/lookup-page-objects state)
selected (->> state :workspace-local :selected (map #(get objects %)))] selected (->> state :workspace-local :selected (map #(get objects %)))]
(cond-> state (cond-> state
(and (= 1 (count selected)) (and (= 1 (count selected))
@ -284,7 +273,8 @@
;; together. This improves the performance because we only re-render the ;; together. This improves the performance because we only re-render the
;; resized components once even if there are changes that applies to ;; resized components once even if there are changes that applies to
;; lots of texts like changing a font ;; 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 (ptk/reify ::resize-text
IDeref IDeref
(-deref [_] (-deref [_]

View file

@ -180,6 +180,12 @@
(def workspace-frames (def workspace-frames
(l/derived cp/select-frames workspace-page-objects)) (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 (defn object-by-id
[id] [id]
(l/derived #(get % id) workspace-page-objects)) (l/derived #(get % id) workspace-page-objects))

View file

@ -9,35 +9,35 @@
(ns app.main.ui (ns app.main.ui
(:require (:require
[app.config :as cfg]
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.uuid :as uuid]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.auth :refer [logout]] [app.main.data.auth :refer [logout]]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.auth :refer [auth]] [app.main.ui.auth :refer [auth]]
[app.main.ui.auth.verify-token :refer [verify-token]] [app.main.ui.auth.verify-token :refer [verify-token]]
[app.main.ui.cursors :as c]
[app.main.ui.context :as ctx] [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.dashboard :refer [dashboard]]
[app.main.ui.handoff :refer [handoff]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.main.ui.onboarding]
[app.main.ui.render :as render] [app.main.ui.render :as render]
[app.main.ui.settings :as settings] [app.main.ui.settings :as settings]
[app.main.ui.static :as static] [app.main.ui.static :as static]
[app.main.ui.viewer :refer [viewer-page]] [app.main.ui.viewer :refer [viewer-page]]
[app.main.ui.handoff :refer [handoff]]
[app.main.ui.workspace :as workspace] [app.main.ui.workspace :as workspace]
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[app.util.timers :as ts]
[app.util.router :as rt] [app.util.router :as rt]
[cuerdas.core :as str] [app.util.timers :as ts]
[cljs.spec.alpha :as s]
[cljs.pprint :refer [pprint]] [cljs.pprint :refer [pprint]]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[expound.alpha :as expound] [expound.alpha :as expound]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))

View file

@ -5,24 +5,23 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; 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 (ns app.main.ui.handoff.attributes.text
(:require (: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] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[app.util.data :as d] [rumext.alpha :as mf]))
[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]]))
(defn has-text? [shape] (defn has-text? [shape]
(:content shape)) (:content shape))
@ -72,7 +71,7 @@
([style & properties] ([style & properties]
(cg/generate-css-props style properties params))) (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 (let [typography-library-ref (mf/use-memo
(mf/deps (:typography-ref-file style)) (mf/deps (:typography-ref-file style))
(make-typographies-library-ref (:typography-ref-file style))) (make-typographies-library-ref (:typography-ref-file style)))
@ -93,7 +92,7 @@
{:style {:font-family (:font-family typography) {:style {:font-family (:font-family typography)
:font-weight (:font-weight typography) :font-weight (:font-weight typography)
:font-style (:font-style typography)}} :font-style (:font-style typography)}}
(t locale "workspace.assets.typography.sample")]] (tr "workspace.assets.typography.sample")]]
[:div.typography-entry-name (:name typography)] [:div.typography-entry-name (:name typography)]
[:& copy-button {:data (copy-style-data typography)}]] [:& copy-button {:data (copy-style-data typography)}]]
@ -102,7 +101,7 @@
{:style {:font-family (:font-family full-style) {:style {:font-family (:font-family full-style)
:font-weight (:font-weight full-style) :font-weight (:font-weight full-style)
:font-style (:font-style 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)}]]) [:& copy-button {:data (copy-style-data style)}]])
[:div.attributes-content-row [:div.attributes-content-row
@ -117,78 +116,83 @@
(when (:font-id style) (when (:font-id style)
[:div.attributes-unit-row [: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)] [:div.attributes-value (-> style :font-id fonts/get-font-data :name)]
[:& copy-button {:data (copy-style-data style :font-family)}]]) [:& copy-button {:data (copy-style-data style :font-family)}]])
(when (:font-style style) (when (:font-style style)
[:div.attributes-unit-row [: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))] [:div.attributes-value (str (:font-style style))]
[:& copy-button {:data (copy-style-data style :font-style)}]]) [:& copy-button {:data (copy-style-data style :font-style)}]])
(when (:font-size style) (when (:font-size style)
[:div.attributes-unit-row [: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"] [:div.attributes-value (str (:font-size style)) "px"]
[:& copy-button {:data (copy-style-data style :font-size)}]]) [:& copy-button {:data (copy-style-data style :font-size)}]])
(when (:line-height style) (when (:line-height style)
[:div.attributes-unit-row [: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"] [:div.attributes-value (str (:line-height style)) "px"]
[:& copy-button {:data (copy-style-data style :line-height)}]]) [:& copy-button {:data (copy-style-data style :line-height)}]])
(when (:letter-spacing style) (when (:letter-spacing style)
[:div.attributes-unit-row [: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"] [:div.attributes-value (str (:letter-spacing style)) "px"]
[:& copy-button {:data (copy-style-data style :letter-spacing)}]]) [:& copy-button {:data (copy-style-data style :letter-spacing)}]])
(when (:text-decoration style) (when (:text-decoration style)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.typography.text-decoration")] [:div.attributes-label (tr "handoff.attributes.typography.text-decoration")]
[:div.attributes-value (->> style :text-decoration (str "handoff.attributes.typography.text-decoration.") (t locale))] [:div.attributes-value (->> style :text-decoration (str "handoff.attributes.typography.text-decoration.") (tr))]
[:& copy-button {:data (copy-style-data style :text-decoration)}]]) [:& copy-button {:data (copy-style-data style :text-decoration)}]])
(when (:text-transform style) (when (:text-transform style)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.typography.text-transform")] [:div.attributes-label (tr "handoff.attributes.typography.text-transform")]
[:div.attributes-value (->> style :text-transform (str "handoff.attributes.typography.text-transform.") (t locale))] [:div.attributes-value (->> style :text-transform (str "handoff.attributes.typography.text-transform.") (tr))]
[:& copy-button {:data (copy-style-data style :text-transform)}]])])) [:& copy-button {:data (copy-style-data style :text-transform)}]])]))
(mf/defc text-block [{:keys [shape locale]}] (defn- remove-equal-values
(let [font (ut/search-text-attrs (:content shape) [m1 m2]
(keys ut/default-text-attrs)) (if (and (map? m1) (map? m2) (not (nil? m1)) (not (nil? m2)))
style-text-blocks (->> (keys ut/default-text-attrs) (->> m1
(ut/parse-style-text-blocks (:content shape)) (remove (fn [[k v]] (= (k m2) v)))
(remove (fn [[style text]] (str/empty? (str/trim text)))) (into {}))
(mapv (fn [[style text]] (vector (merge ut/default-text-attrs style) text)))) 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)] (for [[idx [full-style text]] (map-indexed vector style-text-blocks)]
(let [previus-style (first (nth style-text-blocks (dec idx) nil)) (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 ;; If the color is set we need to add opacity otherwise the display will not work
style (cond-> style style (cond-> style
(:fill-color style) (:fill-color style)
(assoc :fill-opacity (:fill-opacity full-style)))] (assoc :fill-opacity (:fill-opacity full-style)))]
[:& typography-block {:shape shape [:& typography-block {:shape shape
:locale locale
:full-style full-style :full-style full-style
:style style :style style
:text text}])))) :text text}]))))
(mf/defc text-panel [{:keys [shapes locale]}] (mf/defc text-panel
(let [shapes (->> shapes (filter has-text?))] [{:keys [shapes]}]
(when (seq shapes) (when-let [shapes (seq (filter has-text? shapes))]
[:div.attributes-block [:div.attributes-block
[:div.attributes-block-title [:div.attributes-block-title
[:div.attributes-block-title-text (t locale "handoff.attributes.typography")]] [:div.attributes-block-title-text (tr "handoff.attributes.typography")]]
(for [shape shapes]
[:& text-block {:shape shape
:locale locale}])])))
(for [shape shapes]
[:& text-block {:shape shape}])]))

View file

@ -5,103 +5,86 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; 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 (ns app.main.ui.shapes.text
(:require (:require
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.main.ui.context :as muc]
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.common.geom.matrix :as gmt] [app.main.ui.context :as muc]
[app.util.object :as obj]
[app.util.color :as uc]
[app.main.ui.shapes.text.styles :as sts]
[app.main.ui.shapes.text.embed :as ste] [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/defc render-text
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [node (obj/get props "node") (let [node (obj/get props "node")
text (:text node) text (:text node)
style (sts/generate-text-styles props)] style (sts/generate-text-styles node)]
[:span {:style style [:span {:style style}
:className (when (:fill-color-gradient node) "gradient")}
(if (= text "") "\u00A0" text)])) (if (= text "") "\u00A0" text)]))
(mf/defc render-root (mf/defc render-root
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [node (obj/get props "node") (let [node (obj/get props "node")
embed-fonts? (obj/get props "embed-fonts?") embed? (obj/get props "embed-fonts?")
children (obj/get props "children") 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 [:div.root.rich-text
{:style style {:style style
:xmlns "http://www.w3.org/1999/xhtml"} :xmlns "http://www.w3.org/1999/xhtml"}
[:* (when embed?
[:style ".gradient { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"] [ste/embed-fontfaces-style {:node node}])
(when embed-fonts?
[ste/embed-fontfaces-style {:node node}])]
children])) children]))
(mf/defc render-paragraph-set (mf/defc render-paragraph-set
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [node (obj/get props "node") (let [node (obj/get props "node")
children (obj/get props "children") 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])) [:div.paragraph-set {:style style} children]))
(mf/defc render-paragraph (mf/defc render-paragraph
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [node (obj/get props "node") (let [node (obj/get props "node")
shape (obj/get props "shape")
children (obj/get props "children") children (obj/get props "children")
style (sts/generate-paragraph-styles props)] style (sts/generate-paragraph-styles shape node)]
[:p.paragraph {:style style} children])) [:p.paragraph {:style style :dir "auto"} children]))
;; -- Text nodes ;; -- Text nodes
(mf/defc render-node (mf/defc render-node
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [node (obj/get props "node") (let [{:keys [type text children] :as node} (obj/get props "node")]
index (obj/get props "index")
{:keys [type text children]} node]
(if (string? text) (if (string? text)
[:> render-text props] [:> render-text props]
(let [component (case type (let [component (case type
"root" render-root "root" render-root
"paragraph-set" render-paragraph-set "paragraph-set" render-paragraph-set
"paragraph" render-paragraph "paragraph" render-paragraph
nil)] nil)]
(when component (when component
[:> component (obj/set! props "key" index) [:> component props
(for [[index child] (d/enumerate children)] (for [[index node] (d/enumerate children)]
(let [props (-> (obj/clone props) (let [props (-> (obj/clone props)
(obj/set! "node" child) (obj/set! "node" node)
(obj/set! "index" index) (obj/set! "index" index)
(obj/set! "key" index))] (obj/set! "key" index))]
[:> render-node props]))]))))) [:> 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 (defn- retrieve-colors
[shape] [shape]
(let [colors (->> shape (let [colors (->> (:content shape)
:content
(tree-seq map? :children) (tree-seq map? :children)
(into #{} (comp (map :fill-color) (filter string?))))] (into #{} (comp (map :fill-color) (filter string?))))]
(if (empty? colors) (if (empty? colors)
@ -112,20 +95,20 @@
{::mf/wrap-props false {::mf/wrap-props false
::mf/forward-ref true} ::mf/forward-ref true}
[props ref] [props ref]
(let [shape (unchecked-get props "shape") (let [{:keys [id x y width height content grow-type] :as shape} (obj/get props "shape")
grow-type (unchecked-get props "grow-type")
embed-fonts? (mf/use-ctx muc/embed-ctx) 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 ;; We add 8px to add a padding for the exporter
width (+ width 8)] ;; width (+ width 8)
]
[:foreignObject {:x x [:foreignObject {:x x
:y y :y y
:id (:id shape) :id id
:data-colors (retrieve-colors shape) :data-colors (retrieve-colors shape)
:transform (geom/transform-matrix shape) :transform (geom/transform-matrix shape)
:width (if (#{:auto-width} grow-type) 100000 width) :width (if (#{:auto-width} grow-type) 100000 width)
:height (if (#{:auto-height :auto-width} grow-type) 100000 height) :height (if (#{:auto-height :auto-width} grow-type) 100000 height)
:ref ref} :ref ref}
[:& text-content {:shape shape [:& render-node {:index 0
:content (:content shape) :shape shape
:embed-fonts? embed-fonts?}]])) :node content
:embed-fonts? embed-fonts?}]]))

View file

@ -5,43 +5,46 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; 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 (ns app.main.ui.shapes.text.embed
(:require (:require
[clojure.set :as set] [app.common.data :as d]
[promesa.core :as p] [app.common.text :as txt]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.main.data.fetch :as df] [app.main.data.fetch :as df]
[app.main.fonts :as fonts] [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 */ /* latin */
@font-face { @font-face {
font-family: '$0'; font-family: '%(family)s';
font-style: $3; font-style: %(style)s;
font-weight: $2; font-weight: %(weight)s;
font-display: block; 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 ;; -- Embed fonts into styles
(defn get-node-fonts [node] (defn get-node-fonts
[node]
(let [current-font (if (not (nil? (:font-id node))) (let [current-font (if (not (nil? (:font-id node)))
#{(select-keys node [:font-id :font-variant-id])} #{(select-keys node [:font-id :font-variant-id])}
#{}) #{})
children-font (map get-node-fonts (:children node))] children-font (map get-node-fonts (:children node))]
(reduce set/union (conj children-font current-font)))) (reduce set/union (conj children-font current-font))))
(defn get-local-font-css
(defn get-local-font-css [font-id font-variant-id] [font-id font-variant-id]
(let [{:keys [family variants]} (get @fonts/fontsdb font-id) (let [{:keys [family variants] :as font} (get @fonts/fontsdb font-id)
{:keys [name weight style]} (->> variants (filter #(= (:id %) font-variant-id)) first) {:keys [name weight style] :as variant} (d/seek #(= (:id %) font-variant-id) variants)]
css-str (str/format font-face-template [family name weight style])] (-> (str/format font-face-template {:family family :style style :width weight})
(p/resolved css-str))) (p/resolved))))
(defn get-text-font-data [text] (defn get-text-font-data [text]
(->> text (->> text
@ -59,17 +62,19 @@
replace-text (fn [text [url data]] (str/replace text url data))] replace-text (fn [text [url data]] (str/replace text url data))]
(reduce replace-text font-text url-to-data)))) (reduce replace-text font-text url-to-data))))
(mf/defc embed-fontfaces-style [{:keys [node]}] (mf/defc embed-fontfaces-style
(let [embeded-fonts (mf/use-state nil)] {::mf/wrap-props false}
[props]
(let [node (obj/get props "node")
style (mf/use-state nil)]
(mf/use-effect (mf/use-effect
(mf/deps node) (mf/deps node)
(fn [] (fn []
(let [font-to-embed (get-node-fonts node) (let [font-to-embed (get-node-fonts node)
font-to-embed (if (empty? font-to-embed) #{ut/default-text-attrs} 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)] embeded (map embed-font font-to-embed)]
(-> (p/all embeded) (-> (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 (some? @style)
(when (not (nil? @embeded-fonts)) [:style @style])))
[:style @embeded-fonts])))

View file

@ -5,135 +5,120 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; 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 (ns app.main.ui.shapes.text.styles
(:require (:require
[cuerdas.core :as str]
[app.main.fonts :as fonts]
[app.common.data :as d] [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.color :as uc]
[app.util.text :as ut])) [app.util.object :as obj]
[cuerdas.core :as str]))
(defn generate-root-styles (defn generate-root-styles
([props] (generate-root-styles (clj->js (obj/get props "node")) props)) [shape node]
([data props] (let [valign (or (:vertical-align node "top"))
(let [valign (obj/get data "vertical-align" "top") base #js {:height (or (:height shape) "100%")
shape (obj/get props "shape") :width (or (:width shape) "100%")}]
base #js {:height (or (:height shape) "100%") (cond-> base
:width (or (:width shape) "100%")}] (= valign "top") (obj/set! "justifyContent" "flex-start")
(cond-> base (= valign "center") (obj/set! "justifyContent" "center")
(= valign "top") (obj/set! "justifyContent" "flex-start") (= valign "bottom") (obj/set! "justifyContent" "flex-end"))))
(= valign "center") (obj/set! "justifyContent" "center")
(= valign "bottom") (obj/set! "justifyContent" "flex-end")
))))
(defn generate-paragraph-set-styles (defn generate-paragraph-set-styles
([props] (generate-paragraph-set-styles (clj->js (obj/get props "node")) props)) [{:keys [grow-type] :as shape}]
([data props] ;; This element will control the auto-width/auto-height size for the
;; This element will control the auto-width/auto-height size for the ;; shape. The properties try to adjust to the shape and "overflow" if
;; shape. The properties try to adjust to the shape and "overflow" if ;; the shape is not big enough.
;; the shape is not big enough. ;; We `inherit` the property `justify-content` so it's set by the root where
;; We `inherit` the property `justify-content` so it's set by the root where ;; the property it's known.
;; the property it's known. ;; `inline-flex` is similar to flex but `overflows` outside the bounds of the
;; `inline-flex` is similar to flex but `overflows` outside the bounds of the ;; parent
;; parent (let [auto-width? (= grow-type :auto-width)
(let [shape (obj/get props "shape") auto-height? (= grow-type :auto-height)]
grow-type (:grow-type shape) #js {:display "inline-flex"
auto-width? (= grow-type :auto-width) :flexDirection "column"
auto-height? (= grow-type :auto-height) :justifyContent "inherit"
:minHeight (when-not (or auto-width? auto-height?) "100%")
base #js {:display "inline-flex" :minWidth (when-not auto-width? "100%")
:flexDirection "column" :verticalAlign "top"}))
:justifyContent "inherit"
:minHeight (when-not (or auto-width? auto-height?) "100%")
:minWidth (when-not auto-width? "100%")
:verticalAlign "top"}]
base)))
(defn generate-paragraph-styles (defn generate-paragraph-styles
([props] (generate-paragraph-styles (clj->js (obj/get props "node")) props)) [shape data]
([data props] (let [line-height (:line-height data)
(let [shape (obj/get props "shape") text-align (:text-align data)
grow-type (:grow-type shape) grow-type (:grow-type shape)
base #js {:fontSize "14px"
:margin "inherit" base #js {:fontSize (str (:font-size txt/default-text-attrs) "px")
:lineHeight "1.2"} :lineHeight (:line-height txt/default-text-attrs)
lh (obj/get data "line-height") :margin "inherit"}]
ta (obj/get data "text-align")] (cond-> base
(cond-> base (some? line-height) (obj/set! "lineHeight" line-height)
ta (obj/set! "textAlign" ta) (some? text-align) (obj/set! "textAlign" text-align)
lh (obj/set! "lineHeight" lh) (= grow-type :auto-width) (obj/set! "whiteSpace" "pre"))))
(= grow-type :auto-width) (obj/set! "whiteSpace" "pre")))))
(defn generate-text-styles (defn generate-text-styles
([props] (generate-text-styles (clj->js (obj/get props "node")) props)) [data]
([data props] (let [letter-spacing (:letter-spacing data)
(let [letter-spacing (obj/get data "letter-spacing") text-decoration (:text-decoration data)
text-decoration (obj/get data "text-decoration") text-transform (:text-transform data)
text-transform (obj/get data "text-transform") line-height (:line-height data)
line-height (obj/get data "line-height")
font-id (obj/get data "font-id" (:font-id ut/default-text-attrs)) font-id (:font-id data (:font-id txt/default-text-attrs))
font-variant-id (obj/get data "font-variant-id") font-variant-id (:font-variant-id data)
font-family (obj/get data "font-family") font-family (:font-family data)
font-size (obj/get data "font-size") font-size (:font-size data)
;; Old properties for backwards compatibility fill-color (:fill-color data)
fill (obj/get data "fill") fill-opacity (:fill-opacity data)
opacity (obj/get data "opacity" 1)
fill-color (obj/get data "fill-color" fill) ;; Uncomment this to allow to remove text colors. This could break the texts that already exist
fill-opacity (obj/get data "fill-opacity" opacity) ;;[r g b a] (if (nil? fill-color)
fill-color-gradient (obj/get data "fill-color-gradient" nil) ;; [0 0 0 0] ;; Transparent color
fill-color-gradient (when fill-color-gradient ;; (uc/hex->rgba fill-color fill-opacity))
(-> (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] (uc/hex->rgba fill-color fill-opacity)
;;[r g b a] (if (nil? fill-color) text-color (str/format "rgba(%s, %s, %s, %s)" r g b a)
;; [0 0 0 0] ;; Transparent color fontsdb (deref fonts/fontsdb)
;; (uc/hex->rgba fill-color fill-opacity))
[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 (when-let [gradient (:fill-color-gradient data)]
(uc/gradient->css (js->clj fill-color-gradient)) (let [text-color (-> (update gradient :type keyword)
(str/format "rgba(%s, %s, %s, %s)" r g b a)) (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 (when (and (string? font-size)
:textTransform text-transform (pos? (alength font-size)))
:lineHeight (or line-height "inherit") (obj/set! base "fontSize" (str font-size "px")))
:color text-color
"--text-color" text-color}]
(when (and (string? letter-spacing) (when (and (string? font-id)
(pos? (alength letter-spacing))) (pos? (alength font-id)))
(obj/set! base "letterSpacing" (str letter-spacing "px"))) (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) base))
(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)))

View file

@ -141,7 +141,6 @@
[:& (mf/provider ctx/current-team-id) {:value (:team-id project)} [:& (mf/provider ctx/current-team-id) {:value (:team-id project)}
[:& (mf/provider ctx/current-project-id) {:value (:id project)} [:& (mf/provider ctx/current-project-id) {:value (:id project)}
[:& (mf/provider ctx/current-page-id) {:value page-id} [:& (mf/provider ctx/current-page-id) {:value page-id}
[:section#workspace [:section#workspace
[:& header {:file file [:& header {:file file
:page-id page-id :page-id page-id

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; 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 (ns app.main.ui.workspace.shapes.text
(:require (:require
@ -26,6 +26,7 @@
[app.util.logging :as log] [app.util.logging :as log]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.timers :as timers] [app.util.timers :as timers]
[app.util.text-editor :as ted]
[beicon.core :as rx] [beicon.core :as rx]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
@ -52,8 +53,17 @@
(mf/defc text-resize-content (mf/defc text-resize-content
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [shape (obj/get props "shape") (let [{:keys [id name x y grow-type] :as shape} (obj/get props "shape")
{:keys [id name x y grow-type]} 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) paragraph-ref (mf/use-state nil)
handle-resize-text handle-resize-text
@ -91,8 +101,7 @@
#(.disconnect observer))))) #(.disconnect observer)))))
[:& text/text-shape {:ref text-ref-cb [:& text/text-shape {:ref text-ref-cb
:shape shape :shape shape}]))
:grow-type (:grow-type shape)}]))
(mf/defc text-wrapper (mf/defc text-wrapper
{::mf/wrap-props false} {::mf/wrap-props false}
@ -118,7 +127,6 @@
[:& text-static-content {:shape shape}] [:& text-static-content {:shape shape}]
[:& text-resize-content {:shape shape}])] [:& text-resize-content {:shape shape}])]
(when (and (not ghost?) edition?) (when (and (not ghost?) edition?)
[:& editor/text-shape-edit {:key (str "editor" (:id shape)) [:& editor/text-shape-edit {:key (str "editor" (:id shape))
:shape shape}]) :shape shape}])
@ -136,4 +144,3 @@
:on-pointer-out handle-pointer-leave :on-pointer-out handle-pointer-leave
:on-double-click handle-double-click :on-double-click handle-double-click
:transform (gsh/transform-matrix shape)}])])) :transform (gsh/transform-matrix shape)}])]))

View file

@ -9,190 +9,96 @@
(ns app.main.ui.workspace.shapes.text.editor (ns app.main.ui.workspace.shapes.text.editor
(:require (:require
["slate" :as slate] ["draft-js" :as draft]
["slate-react" :as rslate]
[goog.events :as events]
[rumext.alpha :as mf]
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.shapes :as gsh] [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 :as dw]
[app.main.data.workspace.common :as dwc] [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.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.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 (:import
goog.events.EventType goog.events.EventType
goog.events.KeyCodes)) goog.events.KeyCodes))
;; --- Data functions ;; --- Data functions
(defn- initial-text ;; TODO: why we need this?
[text] ;; (defn- fix-gradients
(clj->js ;; "Fix for the gradient types that need to be keywords"
[{:type "root" ;; [content]
:children [{:type "paragraph-set" ;; (let [fix-node
:children [{:type "paragraph" ;; (fn [node]
:children [{:fill-color "#000000" ;; (d/update-in-when node [:fill-color-gradient :type] keyword))]
:fill-opacity 1 ;; (txt/map-node fix-node content)))
: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)))
;; --- Text Editor Rendering ;; --- Text Editor Rendering
(mf/defc editor-root-node (mf/defc block-component
{::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/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [childs (obj/get props "children") (let [children (obj/get props "children")
data (obj/get props "element") bprops (obj/get props "blockProps")
type (obj/get data "type") style (sts/generate-paragraph-styles (obj/get bprops "shape")
shape (obj/get props "shape") (obj/get bprops "data"))]
style (sts/generate-paragraph-set-styles data props)
attrs (-> (obj/get props "attributes")
(obj/set! "style" style)
(obj/set! "className" type))]
[:> :div attrs childs]))
(mf/defc editor-paragraph-node [:div {:style style :dir "auto"}
{::mf/wrap-props false} [:> draft/EditorBlock props]]))
[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]))
(mf/defc editor-text-node (defn render-block
{::mf/wrap-props false} [block shape]
[props] (let [type (ted/get-editor-block-type block)]
(let [childs (obj/get props "children") (case type
data (obj/get props "leaf") "unstyled"
type (obj/get data "type") #js {:editable true
style (sts/generate-text-styles data props) :component block-component
attrs (-> (obj/get props "attributes") :props #js {:data (ted/get-editor-block-data block)
(obj/set! "style" style)) :shape shape}}
gradient (obj/get data "fill-color-gradient" nil)] nil)))
(if gradient
(obj/set! attrs "className" (str type " gradient"))
(obj/set! attrs "className" type))
[:> :span attrs childs]))
(defn- render-element (def empty-editor-state
[shape props] (ted/create-editor-state))
(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
(mf/defc text-shape-edit-html (mf/defc text-shape-edit-html
{::mf/wrap [mf/memo] {::mf/wrap [mf/memo]
::mf/wrap-props false ::mf/wrap-props false
::mf/forward-ref true} ::mf/forward-ref true}
[props ref] [props ref]
(let [shape (unchecked-get props "shape") (let [{:keys [id x y width height grow-type content] :as shape} (obj/get props "shape")
node-ref (unchecked-get props "node-ref")
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) 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 on-click-outside
(fn [event] (fn [event]
(let [target (dom/get-target event) (let [target (dom/get-target event)
options (dom/get-element-by-class "element-options") options (dom/get-element-by-class "element-options")
assets (dom/get-element-by-class "assets-bar") assets (dom/get-element-by-class "assets-bar")
cpicker (dom/get-element-by-class "colorpicker-tooltip") cpicker (dom/get-element-by-class "colorpicker-tooltip")
palette (dom/get-element-by-class "color-palette") palette (dom/get-element-by-class "color-palette")
self (mf/ref-val self-ref)
selecting? (mf/ref-val selecting-ref)]
(when-not (or (and options (.contains options target)) self (mf/ref-val self-ref)]
(and assets (.contains assets target)) (if (or (and options (.contains options target))
(and self (.contains self target)) (and assets (.contains assets target))
(and cpicker (.contains cpicker target)) (and self (.contains self target))
(and palette (.contains palette target))) (and cpicker (.contains cpicker target))
(if selecting? (and palette (.contains palette target))
(mf/set-ref-val! selecting-ref false) (= "foreignObject" (.-tagName ^js target)))
(on-close))))) (dom/stop-propagation event)
(st/emit! dw/clear-edition-mode))))
on-mouse-down
(fn [event]
(mf/set-ref-val! selecting-ref true))
on-mouse-up
(fn [event]
(mf/set-ref-val! selecting-ref false))
on-key-up on-key-up
(fn [event] (fn [event]
@ -200,86 +106,71 @@
(when (= (.-keyCode event) 27) ; ESC (when (= (.-keyCode event) 27) ; ESC
(do (do
(st/emit! :interrupt) (st/emit! :interrupt)
(on-close)))) (st/emit! dw/clear-edition-mode))))
on-mount on-mount
(fn [] (fn []
(let [keys [(events/listen js/document EventType.MOUSEDOWN on-click-outside) (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.CLICK on-click-outside)
(events/listen js/document EventType.KEYUP on-key-up)]] (events/listen js/document EventType.KEYUP on-key-up)]]
(st/emit! (dwt/assign-editor id editor) (st/emit! (dwt/initialize-editor-state shape)
(dwc/start-undo-transaction)) (dwt/select-all shape))
#(do #(do
(st/emit! (dwt/assign-editor id nil) (st/emit! (dwt/finalize-editor-state shape))
(dwc/commit-undo-transaction))
(doseq [key keys] (doseq [key keys]
(events/unlistenByKey key))))) (events/unlistenByKey key)))))
on-focus on-blur
(fn [event] (fn [event]
(dwt/editor-select-all! editor)) (dom/stop-propagation event)
(dom/prevent-default event))
on-composition-start
(mf/use-callback
(fn [event]
(.insertText slate/Editor editor "")))
on-change on-change
(mf/use-callback (mf/use-callback
(fn [val] (fn [val]
(let [content (js->clj val :keywordize-keys true) (st/emit! (dwt/update-editor-state shape val))))
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))))]
(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 handle-return
(mf/deps content) (mf/use-callback
(fn [] (fn [event state]
(reset! state (parse-content content)) (st/emit! (dwt/update-editor-state shape (ted/editor-split-block state)))
(reset! content-var content))) "handled"))
]
[:div.text-editor {:ref self-ref} (mf/use-layout-effect on-mount)
[:style "span { line-height: inherit; }
.gradient { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"] [:div.text-editor {:ref self-ref
[:> rslate/Slate {:editor editor :class (dom/classnames
:value @state :align-top (= (:vertical-align content "top") "top")
:on-change on-change} :align-center (= (:vertical-align content) "center")
[:> rslate/Editable :align-bottom (= (:vertical-align content) "bottom"))}
{:auto-focus "true" [:> draft/Editor
:spell-check "false" {:on-change on-change
:on-focus on-focus :on-blur on-blur
:class "rich-text" :handle-return handle-return
:style {:cursor cur/text :custom-style-fn (fn [styles _]
:width (:width shape)} (-> (ted/styles-to-attrs styles)
:render-element #(render-element shape %) (sts/generate-text-styles)))
:render-leaf render-text :block-renderer-fn #(render-block % shape)
:on-mouse-up on-mouse-up :ref on-editor
:on-mouse-down on-mouse-down :editor-state state}]]))
: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/defc text-shape-edit (mf/defc text-shape-edit
{::mf/wrap [mf/memo] {::mf/wrap [mf/memo]
::mf/wrap-props false ::mf/wrap-props false
::mf/forward-ref true} ::mf/forward-ref true}
[props ref] [props ref]
(let [shape (unchecked-get props "shape") (let [{:keys [id x y width height grow-type] :as shape} (obj/get props "shape")]
{:keys [x y width height grow-type]} shape]
[:foreignObject {:transform (gsh/transform-matrix shape) [:foreignObject {:transform (gsh/transform-matrix shape)
:x x :y y :x x :y y
:width (if (#{:auto-width} grow-type) 100000 width) :width (if (#{:auto-width} grow-type) 100000 width)
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)} :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.geom.shapes :as geom]
[app.common.media :as cm] [app.common.media :as cm]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.text :as txt]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cfg] [app.config :as cfg]
[app.main.data.colors :as dc] [app.main.data.colors :as dc]
@ -38,7 +39,6 @@
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.text :as ut]
[app.util.timers :as timers] [app.util.timers :as timers]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
@ -431,7 +431,7 @@
(mf/use-callback (mf/use-callback
(mf/deps file-id) (mf/deps file-id)
(fn [value opacity] (fn [value opacity]
(st/emit! (dwl/add-typography ut/default-typography)))) (st/emit! (dwl/add-typography txt/default-typography))))
handle-change handle-change
(mf/use-callback (mf/use-callback

View file

@ -30,8 +30,8 @@
:fill-color-gradient]) :fill-color-gradient])
(mf/defc fill-menu (mf/defc fill-menu
{::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "editor" "values"]))]} {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values"]))]}
[{:keys [ids type values editor] :as props}] [{:keys [ids type values] :as props}]
(let [locale (mf/deref i18n/locale) (let [locale (mf/deref i18n/locale)
show? (or (not (nil? (:fill-color values))) show? (or (not (nil? (:fill-color values)))
(not (nil? (:fill-color-gradient values)))) (not (nil? (:fill-color-gradient values))))

View file

@ -11,6 +11,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.common.text :as txt]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.texts :as dwt] [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.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry typography-options]]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.text :as ut]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
@ -49,7 +49,7 @@
(def attrs (d/concat #{} shape-attrs root-attrs paragraph-attrs text-attrs)) (def attrs (d/concat #{} shape-attrs root-attrs paragraph-attrs text-attrs))
(mf/defc text-align-options (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 (let [{:keys [text-align]} values
text-align (or text-align "left") text-align (or text-align "left")
@ -83,7 +83,7 @@
(mf/defc vertical-align (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 (let [{:keys [vertical-align]} values
vertical-align (or vertical-align "top") vertical-align (or vertical-align "top")
handle-change handle-change
@ -108,7 +108,7 @@
i/align-bottom]])) i/align-bottom]]))
(mf/defc grow-options (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))) (let [to-single-value (fn [coll] (if (> (count coll) 1) nil (first coll)))
grow-type (->> values :grow-type) grow-type (->> values :grow-type)
handle-change-grow handle-change-grow
@ -133,7 +133,7 @@
i/auto-height]])) i/auto-height]]))
(mf/defc text-decoration-options (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 (let [{:keys [text-decoration]} values
text-decoration (or text-decoration "none") text-decoration (or text-decoration "none")
@ -160,14 +160,14 @@
:on-click #(handle-change % "line-through")} :on-click #(handle-change % "line-through")}
i/strikethrough]])) 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)] (let [{:keys [name]} (fonts/get-font-data font-id)]
(-> typography (assoc typography :name (str name " " (str/title font-variant-id)))))
(assoc :name (str name " " (str/title font-variant-id))))) )
(mf/defc text-menu (mf/defc text-menu
{::mf/wrap [mf/memo]} {::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) (let [current-file-id (mf/use-ctx ctx/current-file-id)
typographies (mf/deref refs/workspace-file-typography) typographies (mf/deref refs/workspace-file-typography)
@ -181,15 +181,15 @@
(fn [id attrs] (fn [id attrs]
(let [attrs (select-keys attrs root-attrs)] (let [attrs (select-keys attrs root-attrs)]
(when-not (empty? 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)] (let [attrs (select-keys attrs paragraph-attrs)]
(when-not (empty? 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)] (let [attrs (select-keys attrs text-attrs)]
(when-not (empty? 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 typography (cond
(and (:typography-ref-id values) (and (:typography-ref-id values)
@ -213,7 +213,7 @@
(d/concat text-font-attrs (d/concat text-font-attrs
text-spacing-attrs text-spacing-attrs
text-transform-attrs))) text-transform-attrs)))
typography (merge ut/default-typography setted-values) typography (merge txt/default-typography setted-values)
typography (generate-typography-name typography)] typography (generate-typography-name typography)]
(let [id (uuid/next)] (let [id (uuid/next)]
(st/emit! (dwl/add-typography (assoc typography :id id) false)) (st/emit! (dwl/add-typography (assoc typography :id id) false))
@ -230,8 +230,7 @@
(fn [changes] (fn [changes]
(st/emit! (dwl/update-typography (merge typography changes) current-file-id))) (st/emit! (dwl/update-typography (merge typography changes) current-file-id)))
opts #js {:editor editor opts #js {:ids ids
:ids ids
:values values :values values
:on-change (fn [attrs] :on-change (fn [attrs]
(run! #(emit-update! % attrs) ids))}] (run! #(emit-update! % attrs) ids))}]

View file

@ -5,25 +5,25 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; 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 (ns app.main.ui.workspace.sidebar.options.menus.typography
(:require (:require
[rumext.alpha :as mf] [app.common.data :as d]
[cuerdas.core :as str] [app.common.text :as txt]
[app.main.ui.icons :as i] [app.main.data.workspace.texts :as dwt]
[app.main.fonts :as fonts]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [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.components.editable-select :refer [editable-select]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]]
[app.main.fonts :as fonts]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.text :as ut]
[app.util.timers :as ts]
[app.util.i18n :as i18n :refer [t]] [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] (defn- attr->string [value]
(if (= value :multiple) (if (= value :multiple)
@ -51,9 +51,9 @@
font-size font-size
font-variant-id]} values font-variant-id]} values
font-id (or font-id (:font-id ut/default-text-attrs)) font-id (or font-id (:font-id txt/default-text-attrs))
font-size (or font-size (:font-size ut/default-text-attrs)) font-size (or font-size (:font-size txt/default-text-attrs))
font-variant-id (or font-variant-id (:font-variant-id ut/default-text-attrs)) font-variant-id (or font-variant-id (:font-variant-id txt/default-text-attrs))
fonts (mf/deref fonts/fontsdb) fonts (mf/deref fonts/fontsdb)
font (get fonts font-id) font (get fonts font-id)

View file

@ -9,17 +9,17 @@
(ns app.main.ui.workspace.sidebar.options.shapes.multiple (ns app.main.ui.workspace.sidebar.options.shapes.multiple
(:require (:require
[app.common.data :as d]
[rumext.alpha :as mf]
[app.common.attrs :as attrs] [app.common.attrs :as attrs]
[app.util.text :as ut] [app.common.data :as d]
[app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] [app.common.text :as txt]
[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.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]] [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.stroke :refer [stroke-attrs stroke-menu]]
[app.main.ui.workspace.sidebar.options.menus.text :as ot] [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 ;; We define a map that goes from type to
;; attribute and how to handle them ;; attribute and how to handle them
@ -161,7 +161,7 @@
:text [(conj ids id) :text [(conj ids id)
(-> values (-> values
(merge-attrs (select-keys shape attrs)) (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 %))) :children (let [children (->> (:shapes shape []) (map #(get objects %)))
[new-ids new-values] (get-attrs children objects attr-type)] [new-ids new-values] (get-attrs children objects attr-type)]
[(d/concat ids new-ids) (merge-attrs values new-values)]) [(d/concat ids new-ids) (merge-attrs values new-values)])

View file

@ -21,18 +21,16 @@
(mf/defc options (mf/defc options
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
(let [ids [(:id shape)] (let [ids [(:id shape)]
type (:type shape) type (:type shape)
editors (mf/deref refs/editors) state-map (mf/deref refs/workspace-editor-state)
editor (get editors (:id shape)) editor-state (get state-map (:id shape))
measure-values (select-keys shape measure-attrs) fill-values (dwt/current-text-values
{:editor-state editor-state
fill-values (dwt/current-text-values :shape shape
{:editor editor :attrs text-fill-attrs})
:shape shape
:attrs text-fill-attrs})
fill-values (d/update-in-when fill-values [:fill-color-gradient :type] keyword) 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)) (:fill fill-values) (assoc :fill-color (:fill fill-values))
(:opacity fill-values) (assoc :fill-opacity (:fill fill-values))) (:opacity fill-values) (assoc :fill-opacity (:fill fill-values)))
text-values (merge text-values (merge
(select-keys shape [:grow-type]) (select-keys shape [:grow-type :vertical-align :text-align])
(dwt/current-root-values #_(dwt/current-root-values
{:editor editor :shape shape {:editor-state editor-state
:attrs root-attrs}) :shape shape
(dwt/current-text-values :attrs root-attrs})
{:editor editor :shape shape (dwt/current-paragraph-values
{:editor-state editor-state
:shape shape
:attrs paragraph-attrs}) :attrs paragraph-attrs})
(dwt/current-text-values (dwt/current-text-values
{:editor editor :shape shape {:editor-state editor-state
:shape shape
:attrs text-attrs}))] :attrs text-attrs}))]
[:* [:*
[:& measures-menu {:ids ids
:type type [:& measures-menu
:values measure-values}] {:ids ids
[:& fill-menu {:ids ids :type type
:type type :values (select-keys shape measure-attrs)}]
:values fill-values
:editor editor}] [:& fill-menu
[:& shadow-menu {:ids ids {:ids ids
:values (select-keys shape [:shadow])}] :type type
[:& blur-menu {:ids ids :values fill-values}]
:values (select-keys shape [:blur])}]
[:& text-menu {:ids ids [:& shadow-menu
:type type {:ids ids
:values text-values :values (select-keys shape [:shadow])}]
:editor editor}]]))
[:& 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 on-pointer-down
(mf/use-callback (mf/use-callback
(fn [event] (fn [event]
(let [target (dom/get-target event)] (let [target (dom/get-target event)
; Capture mouse pointer to detect the movements even if cursor closest (.closest target ".public-DraftStyleDefault-block")]
; leaves the viewport or the browser itself (when-not closest
; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture ;; Capture mouse pointer to detect the movements even if cursor
(.setPointerCapture target (.-pointerId event))))) ;; 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 on-pointer-up
(mf/use-callback (mf/use-callback

View file

@ -5,14 +5,15 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; 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 (ns app.util.code-gen
(:require (:require
[cuerdas.core :as str] [app.common.data :as d]
[app.common.math :as mth] [app.common.math :as mth]
[app.util.text :as ut] [app.common.text :as txt]
[app.util.color :as uc])) [app.util.color :as uc]
[cuerdas.core :as str]))
(defn shadow->css [shadow] (defn shadow->css [shadow]
(let [{:keys [style offset-x offset-y blur spread]} shadow (let [{:keys [style offset-x offset-y blur spread]} shadow
@ -136,17 +137,55 @@
:format format :format format
:multi multi :multi multi
:tab-size 2}))) :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] (defn text->properties [shape]
(let [text-shape-style (select-keys styles-data [:layout :shadow :blur]) (let [text-shape-style (select-keys styles-data [:layout :shadow :blur])
shape-props (->> text-shape-style vals (mapcat :props)) shape-props (->> text-shape-style vals (mapcat :props))
shape-to-prop (->> text-shape-style vals (map :to-prop) (reduce merge)) shape-to-prop (->> text-shape-style vals (map :to-prop) (reduce merge))
shape-format (->> text-shape-style vals (map :format) (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)) text-values (->> (search-text-attrs (:content shape) (conj (:props style-text) :fill-color-gradient))
(merge ut/default-text-attrs))] (d/merge txt/default-text-attrs))]
(str/join (str/join
"\n" "\n"
[(generate-css-props shape [(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")))

View file

@ -255,6 +255,11 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= 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: asn1.js@^5.2.0:
version "5.4.1" version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" 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" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.9.1.tgz#677b322267172bd490e4464696f790cbc355bec5"
integrity sha512-laz3Zx0avrw9a4QEIdmIblnVuJz8W51leY9iLThatCsFawWxC3sE4guASC78JbCin+DkwMpCdp1AVAuzL/GN7A== 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: core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 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" safe-buffer "^5.0.1"
sha.js "^2.4.8" 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: cross-spawn@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" 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" dom-serializer "0"
domelementtype "1" 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: duplexify@^3.6.0:
version "3.7.1" version "3.7.1"
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" 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" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== 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: fd-slicer@~1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" 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" resolved "https://registry.yarnpkg.com/immer/-/immer-5.3.6.tgz#51eab8cbbeb13075fe2244250f221598818cac04"
integrity sha512-pqWQ6ozVfNOUDjrLfm4Pt7q4Q12cGw2HUZgry4Q5+Myxu9nmHRkWBpI0J4+MK0AxbdFtdMTwEGVl7Vd+vEiK+A== 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: import-cwd@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" 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" ms "^2.1.1"
triple-beam "^1.3.0" triple-beam "^1.3.0"
loose-envify@^1.1.0: loose-envify@^1.0.0, loose-envify@^1.1.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -3045,11 +3095,6 @@ lru-queue@^0.1.0:
dependencies: dependencies:
es5-ext "~0.10.2" 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: make-iterator@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" 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" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== 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: node-gyp@^3.8.0:
version "3.8.0" version "3.8.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" 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" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= 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: proto-list@~1.2.1:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" 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" is-plain-object "^2.0.3"
split-string "^3.0.1" split-string "^3.0.1"
setimmediate@^1.0.4: setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@ -5125,6 +5182,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= 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: ultron@~1.1.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"