diff --git a/common/src/app/common/text.cljc b/common/src/app/common/text.cljc index 8ee125e8a..c82f65c9b 100644 --- a/common/src/app/common/text.cljc +++ b/common/src/app/common/text.cljc @@ -361,7 +361,7 @@ new-acc (cond - (:children node) + (not (is-text-node? node)) (reduce #(rec-style-text-map %1 %2 node-style) acc (:children node)) (not= head-style node-style) @@ -381,6 +381,28 @@ (-> (rec-style-text-map [] node {}) reverse))) +(defn content-range->text+styles + "Given a root node of a text content extracts the texts with its associated styles" + [node start end] + (let [sss (content->text+styles node)] + (loop [styles (seq sss) + taking? false + acc 0 + result []] + (if styles + (let [[node-style text] (first styles) + from acc + to (+ acc (count text)) + taking? (or taking? (and (<= from start) (< start to))) + text (subs text (max 0 (- start acc)) (- end acc)) + result (cond-> result + (and taking? (d/not-empty? text)) + (conj (assoc node-style :text text))) + continue? (or (> from end) (>= end to))] + (recur (when continue? (rest styles)) taking? to result)) + result)))) + + (defn content->text "Given a root node of a text content extracts the texts with its associated styles" [content] diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index d1e55e027..bceaeacf3 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -205,6 +205,102 @@ ;; --- TEXT EDITION IMPL +(defn count-node-chars + ([node] + (count-node-chars node false)) + ([node last?] + (case (:type node) + ("root" "paragraph-set") + (apply + (concat (map count-node-chars (drop-last (:children node))) + (map #(count-node-chars % true) (take-last 1 (:children node))))) + + "paragraph" + (+ (apply + (map count-node-chars (:children node))) (if last? 0 1)) + + (count (:text node))))) + + +(defn decorate-range-info + "Adds information about ranges inside the metadata of the text nodes" + [content] + (->> (with-meta content {:start 0 :end (count-node-chars content)}) + (txt/transform-nodes + (fn [node] + (d/update-when + node + :children + (fn [children] + (let [start (-> node meta (:start 0))] + (->> children + (reduce (fn [[result start] node] + (let [end (+ start (count-node-chars node))] + [(-> result + (conj (with-meta node {:start start :end end}))) + end])) + [[] start]) + (first))))))))) + +(defn split-content-at + [content position] + (->> content + (txt/transform-nodes + (fn [node] + (and (txt/is-paragraph-node? node) + (< (-> node meta :start) position (-> node meta :end)))) + (fn [node] + (letfn + [(process-node [child] + (let [start (-> child meta :start) + end (-> child meta :end)] + (if (< start position end) + [(-> child + (vary-meta assoc :end position) + (update :text subs 0 (- position start))) + (-> child + (vary-meta assoc :start position) + (update :text subs (- position start)))] + [child])))] + (-> node + (d/update-when :children #(into [] (mapcat process-node) %)))))))) + +(defn update-content-range + [content start end attrs] + (->> content + (txt/transform-nodes + (fn [node] + (and (txt/is-text-node? node) + (and (>= (-> node meta :start) start) + (<= (-> node meta :end) end)))) + #(d/patch-object % attrs)))) + +(defn- update-text-range-attrs + [shape start end attrs] + (let [new-content (-> (:content shape) + (decorate-range-info) + (split-content-at start) + (split-content-at end) + (update-content-range start end attrs))] + (assoc shape :content new-content))) + +(defn update-text-range + [id start end attrs] + (ptk/reify ::update-text-range + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + shape (get objects id) + + update-fn + (fn [shape] + (cond-> shape + (cfh/text-shape? shape) + (update-text-range-attrs start end attrs))) + + shape-ids (cond (cfh/text-shape? shape) [id] + (cfh/group-shape? shape) (cfh/get-children-ids objects id))] + + (rx/of (dwsh/update-shapes shape-ids update-fn)))))) + (defn- update-text-content [shape pred-fn update-fn attrs] (let [update-attrs-fn #(update-fn % attrs) @@ -278,7 +374,6 @@ (cfh/group-shape? shape) (cfh/get-children-ids objects id))] (rx/of (dwsh/update-shapes shape-ids #(update-text-content % update-node? d/txt-merge attrs)))))))) - (defn migrate-node [node] (let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])] diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs index 6f6f87ed3..380ff68c3 100644 --- a/frontend/src/app/plugins/library.cljs +++ b/frontend/src/app/plugins/library.cljs @@ -233,9 +233,16 @@ (st/emit! (dwt/apply-typography #{shape-id} typography $file)))) (applyToTextRange - [_ _shape _from _to] - ;; TODO - ) + [self range] + (let [shape-id (obj/get range "$id") + start (obj/get range "start") + end (obj/get range "end") + typography (u/proxy->library-typography self) + attrs (-> typography + (assoc :typography-ref-file $file) + (assoc :typography-ref-id (:id typography)) + (dissoc :id :name))] + (st/emit! (dwt/update-text-range shape-id start end attrs)))) ;; PLUGIN DATA (getPluginData diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 81ad46bbb..0d239dbe2 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -36,6 +36,139 @@ [app.util.text-editor :as ted] [cuerdas.core :as str])) + +(deftype TextRange [$plugin $file $page $id start end] + Object + (applyTypography [_ typography] + (let [typography (u/proxy->library-typography typography) + attrs (-> typography + (assoc :typography-ref-file $file) + (assoc :typography-ref-id (:id typography)) + (dissoc :id :name))] + (st/emit! (dwt/update-text-range $id start end attrs))))) + +(defn mixed-value + [values] + (let [s (set values)] + (if (= (count s) 1) (first s) "mixed"))) + +;; TODO Validate inputs +(defn text-range + [plugin-id file-id page-id id start end] + (-> (TextRange. plugin-id file-id page-id id start end) + (crc/add-properties! + {:name "$plugin" :enumerable false :get (constantly plugin-id)} + {:name "$id" :enumerable false :get (constantly id)} + {:name "$file" :enumerable false :get (constantly file-id)} + {:name "$page" :enumerable false :get (constantly page-id)} + + {:name "shape" + :get #(-> % u/proxy->shape)} + + {:name "characters" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :text) (str/join "")))} + + {:name "fontId" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-id) mixed-value)) + + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:font-id value})))} + + {:name "fontFamily" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-family) mixed-value)) + + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:font-family value})))} + + {:name "fontVariantId" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-variant-id) mixed-value)) + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:font-variant-id value})))} + + {:name "fontSize" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-size) mixed-value)) + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:font-size value})))} + + {:name "fontWeight" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-weight) mixed-value)) + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:font-weight value})))} + + {:name "fontStyle" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :font-style) mixed-value)) + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:font-style value})))} + + {:name "lineHeight" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :line-height) mixed-value)) + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:line-height value})))} + + {:name "letterSpacing" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :letter-spacing) mixed-value)) + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:letter-spacing value})))} + + {:name "textTransform" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :text-transform) mixed-value)) + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:text-transform value})))} + + {:name "textDecoration" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :text-decoration) mixed-value)) + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:text-decoration value})))} + + {:name "direction" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :direction) mixed-value)) + :set + (fn [_ value] + (st/emit! (dwt/update-text-range id start end {:direction value})))} + + {:name "fills" + :get #(let [range-data + (-> % u/proxy->shape :content (txt/content-range->text+styles start end))] + (->> range-data (map :fills) mixed-value u/array-to-js)) + :set + (fn [_ value] + (let [value (mapv #(u/from-js %) value)] + (st/emit! (dwt/update-text-range id start end {:fills value}))))}))) + (declare shape-proxy) (defn parse-command @@ -214,11 +347,19 @@ ;; Text shapes (getRange - [_ _from _to] + [_ start end] (let [shape (u/locate-shape $file $page $id)] (if (cfh/text-shape? shape) - nil ;; TODO - (u/display-not-valid :makeMask (:type shape)))))) + (text-range $plugin $file $page $id start end) + (u/display-not-valid :makeMask (:type shape))))) + + (applyTypography + [_ typography] + (let [shape (u/locate-shape $file $page $id)] + (if (cfh/text-shape? shape) + (let [typography (u/proxy->library-typography typography)] + (st/emit! (dwt/apply-typography #{$id} typography $file))) + (u/display-not-valid :applyTypography (:type shape)))))) (crc/define-properties! ShapeProxy @@ -490,14 +631,18 @@ :get #(-> % u/proxy->shape :flip-y)} ;; Strokes and fills + ;; TODO: Validate fills input {:name "fills" :get #(if (cfh/text-shape? data) (-> % u/proxy->shape text-props :fills u/array-to-js) (-> % u/proxy->shape :fills u/array-to-js)) :set (fn [self value] - (let [id (obj/get self "$id") + (let [shape (u/proxy->shape self) + id (:id shape) value (mapv #(u/from-js %) value)] - (st/emit! (dwsh/update-shapes [id] #(assoc % :fills value)))))} + (if (cfh/text-shape? shape) + (st/emit! (dwt/update-attrs id {:fills value})) + (st/emit! (dwsh/update-shapes [id] #(assoc % :fills value))))))} {:name "strokes" :get #(-> % u/proxy->shape :strokes u/array-to-js) @@ -634,7 +779,7 @@ :set (fn [self value] (let [id (obj/get self "$id")] - (st/emit! (dwt/update-attrs id {:font-id value}))))} + (st/emit! (dwt/update-attrs id {:font-size value}))))} {:name "fontWeight" :get #(-> % u/proxy->shape text-props :font-weight) diff --git a/frontend/src/app/plugins/utils.cljs b/frontend/src/app/plugins/utils.cljs index c057aa0b6..87fb3abb3 100644 --- a/frontend/src/app/plugins/utils.cljs +++ b/frontend/src/app/plugins/utils.cljs @@ -177,9 +177,11 @@ (defn array-to-js [value] - (.freeze - js/Object - (apply array (->> value (map to-js))))) + (if (coll? value) + (.freeze + js/Object + (apply array (->> value (map to-js)))) + value)) (defn result-p "Creates a pair of atom+promise. The promise will be resolved when the atom gets a value.