mirror of
https://github.com/penpot/penpot.git
synced 2025-03-13 16:21:57 -05:00
commit
0eb2336bc6
24 changed files with 648 additions and 327 deletions
12
CHANGES.md
12
CHANGES.md
|
@ -32,6 +32,14 @@
|
|||
- After team onboarding importing a file will import into the team drafts [Taiga #2408](https://tree.taiga.io/project/penpot/issue/2408)
|
||||
- Fix problem exporting shapes from handoff mode [Taiga #2386](https://tree.taiga.io/project/penpot/issue/2386)
|
||||
- Fix lock/hide elements in context menu when multiples shapes selected [Taiga #2340](https://tree.taiga.io/project/penpot/issue/2340)
|
||||
- Fix problem with booleans [Taiga #2356](https://tree.taiga.io/project/penpot/issue/2356)
|
||||
- Fix line-height/letter-spacing inputs behaviour [Taiga #2331](https://tree.taiga.io/project/penpot/issue/2331)
|
||||
- Fix dotted style in strokes [Taiga #2312](https://tree.taiga.io/project/penpot/issue/2312)
|
||||
- Fix problem when resizing texts inside groups [Taiga #2310](https://tree.taiga.io/project/penpot/issue/2310)
|
||||
- Fix problem with multiple exports [Taiga #2468](https://tree.taiga.io/project/penpot/issue/2468)
|
||||
- Allow import to continue from recoverable failures [#1412](https://github.com/penpot/penpot/issues/1412)
|
||||
- Improved behaviour on text options when not text is selected [Taiga #2390](https://tree.taiga.io/project/penpot/issue/2390)
|
||||
- Fix decimal numbers in export viewbox [Taiga #2290](https://tree.taiga.io/project/penpot/issue/2290)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
@ -75,8 +83,6 @@
|
|||
### :arrow_up: Deps updates
|
||||
|
||||
- Update log4j2 dependency.
|
||||
>>>>>>> main
|
||||
|
||||
|
||||
# 1.10.2-beta
|
||||
|
||||
|
@ -134,6 +140,8 @@
|
|||
- Add placeholder to create shareable link
|
||||
- Fix project files count not refreshing correctly after import [Taiga #2216](https://tree.taiga.io/project/penpot/issue/2216)
|
||||
- Remove button after import process finish [Taiga #2215](https://tree.taiga.io/project/penpot/issue/2215)
|
||||
- Fix problem with styles in the viewer [Taiga #2467](https://tree.taiga.io/project/penpot/issue/2467)
|
||||
- Fix default state in viewer [Taiga #2465](https://tree.taiga.io/project/penpot/issue/2465)
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages.changes :as ch]
|
||||
[app.common.pages.init :as init]
|
||||
[app.common.pages.spec :as spec]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]))
|
||||
|
@ -21,15 +20,14 @@
|
|||
(def conjv (fnil conj []))
|
||||
(def conjs (fnil conj #{}))
|
||||
|
||||
;; This flag controls if we should execute spec validation after every commit
|
||||
(def verify-on-commit? true)
|
||||
|
||||
(defn- commit-change
|
||||
([file change]
|
||||
(commit-change file change nil))
|
||||
|
||||
([file change {:keys [add-container?]
|
||||
:or {add-container? false}}]
|
||||
([file change {:keys [add-container?
|
||||
fail-on-spec?]
|
||||
:or {add-container? false
|
||||
fail-on-spec? false}}]
|
||||
(let [component-id (:current-component-id file)
|
||||
change (cond-> change
|
||||
(and add-container? (some? component-id))
|
||||
|
@ -39,11 +37,20 @@
|
|||
(assoc :page-id (:current-page-id file)
|
||||
:frame-id (:current-frame-id file)))]
|
||||
|
||||
(when verify-on-commit?
|
||||
(us/assert ::spec/change change))
|
||||
(-> file
|
||||
(update :changes conjv change)
|
||||
(update :data ch/process-changes [change] verify-on-commit?)))))
|
||||
(when fail-on-spec?
|
||||
(us/verify :app.common.pages.spec/change change))
|
||||
|
||||
(let [valid? (us/valid? :app.common.pages.spec/change change)]
|
||||
#?(:cljs
|
||||
(when-not valid? (.warn js/console "Invalid shape" (clj->js change))))
|
||||
|
||||
(cond-> file
|
||||
valid?
|
||||
(-> (update :changes conjv change)
|
||||
(update :data ch/process-changes [change] false))
|
||||
|
||||
(not valid?)
|
||||
(update :errors conjv change))))))
|
||||
|
||||
(defn- lookup-objects
|
||||
([file]
|
||||
|
@ -56,15 +63,16 @@
|
|||
(get shape-id)))
|
||||
|
||||
(defn- commit-shape [file obj]
|
||||
(let [parent-id (-> file :parent-stack peek)]
|
||||
(-> file
|
||||
(commit-change
|
||||
{:type :add-obj
|
||||
:id (:id obj)
|
||||
:obj obj
|
||||
:parent-id parent-id}
|
||||
(let [parent-id (-> file :parent-stack peek)
|
||||
change {:type :add-obj
|
||||
:id (:id obj)
|
||||
:obj obj
|
||||
:parent-id parent-id}
|
||||
|
||||
{:add-container? true}))))
|
||||
fail-on-spec? (or (= :group (:type obj))
|
||||
(= :frame (:type obj)))]
|
||||
|
||||
(commit-change file change {:add-container? true :fail-on-spec? fail-on-spec?})))
|
||||
|
||||
(defn setup-rect-selrect [obj]
|
||||
(let [rect (select-keys obj [:x :y :width :height])
|
||||
|
@ -421,35 +429,36 @@
|
|||
(defn add-interaction
|
||||
[file from-id interaction-src]
|
||||
|
||||
(assert (some? (lookup-shape file from-id)) (str "Cannot locate shape with id " from-id))
|
||||
(let [shape (lookup-shape file from-id)]
|
||||
(if (nil? shape)
|
||||
file
|
||||
(let [{:keys [event-type action-type]} (read-classifier interaction-src)
|
||||
{:keys [delay]} (read-event-opts interaction-src)
|
||||
{:keys [destination overlay-pos-type overlay-position url
|
||||
close-click-outside background-overlay preserve-scroll]}
|
||||
(read-action-opts interaction-src)
|
||||
|
||||
(let [{:keys [event-type action-type]} (read-classifier interaction-src)
|
||||
{:keys [delay]} (read-event-opts interaction-src)
|
||||
{:keys [destination overlay-pos-type overlay-position url
|
||||
close-click-outside background-overlay preserve-scroll]}
|
||||
(read-action-opts interaction-src)
|
||||
interactions (-> shape
|
||||
:interactions
|
||||
(conjv
|
||||
(d/without-nils {:event-type event-type
|
||||
:action-type action-type
|
||||
:delay delay
|
||||
:destination destination
|
||||
:overlay-pos-type overlay-pos-type
|
||||
:overlay-position overlay-position
|
||||
:url url
|
||||
:close-click-outside close-click-outside
|
||||
:background-overlay background-overlay
|
||||
:preserve-scroll preserve-scroll})))]
|
||||
(commit-change
|
||||
file
|
||||
{:type :mod-obj
|
||||
:page-id (:current-page-id file)
|
||||
:id from-id
|
||||
|
||||
interactions (-> (lookup-shape file from-id)
|
||||
:interactions
|
||||
(conjv
|
||||
(d/without-nils {:event-type event-type
|
||||
:action-type action-type
|
||||
:delay delay
|
||||
:destination destination
|
||||
:overlay-pos-type overlay-pos-type
|
||||
:overlay-position overlay-position
|
||||
:url url
|
||||
:close-click-outside close-click-outside
|
||||
:background-overlay background-overlay
|
||||
:preserve-scroll preserve-scroll})))]
|
||||
(commit-change
|
||||
file
|
||||
{:type :mod-obj
|
||||
:page-id (:current-page-id file)
|
||||
:id from-id
|
||||
|
||||
:operations
|
||||
[{:type :set :attr :interactions :val interactions}]})))
|
||||
:operations
|
||||
[{:type :set :attr :interactions :val interactions}]})))))
|
||||
|
||||
(defn generate-changes
|
||||
[file]
|
||||
|
|
|
@ -451,9 +451,6 @@
|
|||
rt-modif (:rotation modifiers)]
|
||||
|
||||
(cond-> (gmt/matrix)
|
||||
(some? displacement)
|
||||
(gmt/multiply displacement)
|
||||
|
||||
(some? resize-1)
|
||||
(-> (gmt/translate origin-1)
|
||||
(gmt/multiply resize-transform)
|
||||
|
@ -468,6 +465,9 @@
|
|||
(gmt/multiply resize-transform-inverse)
|
||||
(gmt/translate (gpt/negate origin-2)))
|
||||
|
||||
(some? displacement)
|
||||
(gmt/multiply displacement)
|
||||
|
||||
(some? rt-modif)
|
||||
(-> (gmt/translate center)
|
||||
(gmt/multiply (gmt/rotate-matrix rt-modif))
|
||||
|
|
|
@ -212,6 +212,27 @@
|
|||
(d/seek overlap-single?)
|
||||
(some?))))
|
||||
|
||||
(defn fix-move-to
|
||||
[content]
|
||||
;; Remove the field `:prev` and makes the necessaries `move-to`
|
||||
;; then clean the subpaths
|
||||
|
||||
(loop [current (first content)
|
||||
content (rest content)
|
||||
prev nil
|
||||
result []]
|
||||
|
||||
(if (nil? current)
|
||||
result
|
||||
|
||||
(let [result (if (not= (:prev current) prev)
|
||||
(conj result (upc/make-move-to (:prev current)))
|
||||
result)]
|
||||
(recur (first content)
|
||||
(rest content)
|
||||
(gsp/command->point current)
|
||||
(conj result (dissoc current :prev)))))))
|
||||
|
||||
(defn create-union [content-a content-a-split content-b content-b-split sr-a sr-b]
|
||||
;; Pick all segments in content-a that are not inside content-b
|
||||
;; Pick all segments in content-b that are not inside content-a
|
||||
|
@ -225,7 +246,7 @@
|
|||
|
||||
content-geom (gsp/content->geom-data content)
|
||||
|
||||
content-sr (gsp/content->selrect content)
|
||||
content-sr (gsp/content->selrect (fix-move-to content))
|
||||
|
||||
;; Overlapping segments should be added when they are part of the border
|
||||
border-content
|
||||
|
@ -265,36 +286,22 @@
|
|||
;; Pick all segments
|
||||
(d/concat-vec content-a content-b))
|
||||
|
||||
(defn fix-move-to
|
||||
[content]
|
||||
;; Remove the field `:prev` and makes the necessaries `move-to`
|
||||
;; then clean the subpaths
|
||||
|
||||
(loop [current (first content)
|
||||
content (rest content)
|
||||
prev nil
|
||||
result []]
|
||||
|
||||
(if (nil? current)
|
||||
result
|
||||
|
||||
(let [result (if (not= (:prev current) prev)
|
||||
(conj result (upc/make-move-to (:prev current)))
|
||||
result)]
|
||||
(recur (first content)
|
||||
(rest content)
|
||||
(gsp/command->point current)
|
||||
(conj result (dissoc current :prev)))))))
|
||||
|
||||
(defn content-bool-pair
|
||||
[bool-type content-a content-b]
|
||||
|
||||
(let [content-a (-> content-a (close-paths) (add-previous))
|
||||
(let [;; We need to reverse the second path when making a difference/intersection/exclude
|
||||
;; and both shapes are in the same direction
|
||||
should-reverse? (and (not= :union bool-type)
|
||||
(= (ups/clockwise? content-b)
|
||||
(ups/clockwise? content-a)))
|
||||
|
||||
content-a (-> content-a
|
||||
(close-paths)
|
||||
(add-previous))
|
||||
|
||||
content-b (-> content-b
|
||||
(close-paths)
|
||||
(cond-> (ups/clockwise? content-b)
|
||||
(ups/reverse-content))
|
||||
(cond-> should-reverse? (ups/reverse-content))
|
||||
(add-previous))
|
||||
|
||||
sr-a (gsp/content->selrect content-a)
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
(def max-safe-int (int 1e6))
|
||||
(def min-safe-int (int -1e6))
|
||||
|
||||
(def valid? s/valid?)
|
||||
|
||||
;; --- Conformers
|
||||
|
||||
(defn uuid-conformer
|
||||
|
@ -216,8 +218,11 @@
|
|||
:code :spec-validation
|
||||
:hint hint
|
||||
:ctx ctx
|
||||
:value val
|
||||
::s/problems (::s/problems data)))))
|
||||
|
||||
|
||||
|
||||
(defmacro assert
|
||||
"Development only assertion macro."
|
||||
[spec x]
|
||||
|
|
|
@ -80,9 +80,11 @@
|
|||
(p/then (fn [results]
|
||||
(reduce #(zip/add! %1 (:filename %2) (:content %2)) (zip/create) results)))
|
||||
(p/then (fn [fzip]
|
||||
(.generateAsync ^js fzip #js {:type "uint8array"})))
|
||||
(p/then (fn [data]
|
||||
{:status 200
|
||||
:headers {"content-type" "application/zip"}
|
||||
:body (.generateNodeStream ^js fzip)})))))
|
||||
:body data})))))
|
||||
|
||||
(defn- perform-export
|
||||
[params]
|
||||
|
|
|
@ -322,6 +322,10 @@
|
|||
fill: $color-success;
|
||||
}
|
||||
|
||||
.icon-msg-warning {
|
||||
fill: $color-warning;
|
||||
}
|
||||
|
||||
.icon-close {
|
||||
transform: rotate(45deg);
|
||||
fill: $color-danger;
|
||||
|
@ -392,6 +396,13 @@
|
|||
fill: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: $color-warning-lighter;
|
||||
.icon {
|
||||
background: $color-warning;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
.empty-state {
|
||||
justify-content: center;
|
||||
|
|
|
@ -29,11 +29,13 @@
|
|||
[app.main.ui.shapes.text :as text]
|
||||
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||
[app.util.object :as obj]
|
||||
[app.util.strings :as ust]
|
||||
[app.util.timers :as ts]
|
||||
[cuerdas.core :as str]
|
||||
[debug :refer [debug?]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def ^:const viewbox-decimal-precision 3)
|
||||
(def ^:private default-color clr/canvas)
|
||||
|
||||
(mf/defc background
|
||||
|
@ -139,8 +141,13 @@
|
|||
;; Don't wrap svg elements inside a <g> otherwise some can break
|
||||
[:> svg-raw-wrapper {:shape shape :frame frame}]))))))
|
||||
|
||||
(defn get-viewbox [{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
|
||||
(str/fmt "%s %s %s %s" x y width height))
|
||||
(defn format-viewbox
|
||||
"Format a viewbox given a rectangle"
|
||||
[{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
|
||||
(str/join
|
||||
" "
|
||||
(->> [x y width height]
|
||||
(map #(ust/format-precision % viewbox-decimal-precision)))))
|
||||
|
||||
(mf/defc page-svg
|
||||
{::mf/wrap [mf/memo]}
|
||||
|
@ -160,7 +167,7 @@
|
|||
vport (when (and (some? width) (some? height))
|
||||
{:width width :height height})
|
||||
dim (calculate-dimensions data vport)
|
||||
vbox (get-viewbox dim)
|
||||
vbox (format-viewbox dim)
|
||||
background-color (get-in data [:options :background] default-color)
|
||||
frame-wrapper
|
||||
(mf/use-memo
|
||||
|
@ -221,15 +228,15 @@
|
|||
|
||||
width (* (:width frame) zoom)
|
||||
height (* (:height frame) zoom)
|
||||
vbox (str "0 0 " (:width frame 0)
|
||||
" " (:height frame 0))
|
||||
vbox (format-viewbox {:width (:width frame 0) :height (:height frame 0)})
|
||||
|
||||
wrapper (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(frame-wrapper-factory objects))]
|
||||
|
||||
[:svg {:view-box vbox
|
||||
:width width
|
||||
:height height
|
||||
:width (ust/format-precision width viewbox-decimal-precision)
|
||||
:height (ust/format-precision height viewbox-decimal-precision)
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
|
@ -255,18 +262,20 @@
|
|||
|
||||
group (get objects group-id)
|
||||
|
||||
|
||||
width (* (:width group) zoom)
|
||||
height (* (:height group) zoom)
|
||||
vbox (str "0 0 " (:width group 0)
|
||||
" " (:height group 0))
|
||||
vbox (format-viewbox {:width (:width group 0)
|
||||
:height (:height group 0)})
|
||||
|
||||
group-wrapper
|
||||
(mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(group-wrapper-factory objects))]
|
||||
|
||||
[:svg {:view-box vbox
|
||||
:width width
|
||||
:height height
|
||||
:width (ust/format-precision width viewbox-decimal-precision)
|
||||
:height (ust/format-precision height viewbox-decimal-precision)
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
|
@ -281,7 +290,7 @@
|
|||
root (get objects id)
|
||||
|
||||
{:keys [width height]} (:selrect root)
|
||||
vbox (str "0 0 " width " " height)
|
||||
vbox (format-viewbox {:width width :height height})
|
||||
|
||||
modifier (-> (gpt/point (:x root) (:y root))
|
||||
(gpt/negate)
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.simple-math :as sm]
|
||||
[app.util.strings :as ust]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn num? [val]
|
||||
|
@ -24,13 +25,16 @@
|
|||
{::mf/wrap-props false
|
||||
::mf/forward-ref true}
|
||||
[props external-ref]
|
||||
(let [value-str (obj/get props "value")
|
||||
min-val-str (obj/get props "min")
|
||||
max-val-str (obj/get props "max")
|
||||
wrap-value? (obj/get props "data-wrap")
|
||||
on-change (obj/get props "onChange")
|
||||
title (obj/get props "title")
|
||||
default-val (obj/get props "default" 0)
|
||||
(let [value-str (obj/get props "value")
|
||||
min-val-str (obj/get props "min")
|
||||
max-val-str (obj/get props "max")
|
||||
step-val-str (obj/get props "step")
|
||||
wrap-value? (obj/get props "data-wrap")
|
||||
on-change (obj/get props "onChange")
|
||||
on-blur (obj/get props "onBlur")
|
||||
title (obj/get props "title")
|
||||
default-val (obj/get props "default" 0)
|
||||
precision (obj/get props "precision")
|
||||
|
||||
;; We need a ref pointing to the input dom element, but the user
|
||||
;; of this component may provide one (that is forwarded here).
|
||||
|
@ -56,6 +60,15 @@
|
|||
(string? max-val-str)
|
||||
(d/parse-integer max-val-str))
|
||||
|
||||
step-val (cond
|
||||
(number? step-val-str)
|
||||
step-val-str
|
||||
|
||||
(string? step-val-str)
|
||||
(d/parse-integer step-val-str)
|
||||
|
||||
:else 1)
|
||||
|
||||
parse-value
|
||||
(mf/use-callback
|
||||
(mf/deps ref min-val max-val value)
|
||||
|
@ -65,7 +78,10 @@
|
|||
(sm/expr-eval value))]
|
||||
(when (num? new-value)
|
||||
(-> new-value
|
||||
(math/round)
|
||||
(cond-> (number? precision)
|
||||
(math/precision precision))
|
||||
(cond-> (nil? precision)
|
||||
(math/round))
|
||||
(cljs.core/max us/min-safe-int)
|
||||
(cljs.core/min us/max-safe-int)
|
||||
(cond->
|
||||
|
@ -80,7 +96,9 @@
|
|||
(mf/deps ref)
|
||||
(fn [new-value]
|
||||
(let [input-node (mf/ref-val ref)]
|
||||
(dom/set-value! input-node (str new-value)))))
|
||||
(dom/set-value! input-node (if (some? precision)
|
||||
(ust/format-precision new-value precision)
|
||||
(str new-value))))))
|
||||
|
||||
apply-value
|
||||
(mf/use-callback
|
||||
|
@ -97,18 +115,18 @@
|
|||
(let [current-value (parse-value)]
|
||||
(when current-value
|
||||
(let [increment (if (kbd/shift? event)
|
||||
(if up? 10 -10)
|
||||
(if up? 1 -1))
|
||||
(if up? (* step-val 10) (* step-val -10))
|
||||
(if up? step-val (- step-val)))
|
||||
|
||||
new-value (+ current-value increment)
|
||||
new-value (cond
|
||||
(and wrap-value? (num? max-val) (num? min-val)
|
||||
(> new-value max-val) up?)
|
||||
(-> new-value (- max-val) (+ min-val) (- 1))
|
||||
(-> new-value (- max-val) (+ min-val) (- step-val))
|
||||
|
||||
(and wrap-value? (num? min-val) (num? max-val)
|
||||
(< new-value min-val) down?)
|
||||
(-> new-value (- min-val) (+ max-val) (+ 1))
|
||||
(-> new-value (- min-val) (+ max-val) (+ step-val))
|
||||
|
||||
(and (num? min-val) (< new-value min-val))
|
||||
min-val
|
||||
|
@ -144,12 +162,13 @@
|
|||
|
||||
handle-blur
|
||||
(mf/use-callback
|
||||
(mf/deps parse-value apply-value update-input)
|
||||
(mf/deps parse-value apply-value update-input on-blur)
|
||||
(fn [_]
|
||||
(let [new-value (or (parse-value) default-val)]
|
||||
(if new-value
|
||||
(apply-value new-value)
|
||||
(update-input new-value)))))
|
||||
(update-input new-value)))
|
||||
(when on-blur (on-blur))))
|
||||
|
||||
props (-> props
|
||||
(obj/without ["value" "onChange"])
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
(filter #(= :ready (:status %)))
|
||||
(mapv #(assoc % :status :importing))))
|
||||
|
||||
(defn update-status [files file-id status progress]
|
||||
(defn update-status [files file-id status progress errors]
|
||||
(->> files
|
||||
(mapv (fn [file]
|
||||
(cond-> file
|
||||
|
@ -106,7 +106,10 @@
|
|||
(assoc :status status)
|
||||
|
||||
(and (= file-id (:file-id file)) (= status :import-progress))
|
||||
(assoc :progress progress))))))
|
||||
(assoc :progress progress)
|
||||
|
||||
(= file-id (:file-id file))
|
||||
(assoc :errors errors))))))
|
||||
|
||||
(defn parse-progress-message
|
||||
[message]
|
||||
|
@ -139,9 +142,10 @@
|
|||
|
||||
(let [loading? (or (= :analyzing (:status file))
|
||||
(= :importing (:status file)))
|
||||
load-success? (= :import-success (:status file))
|
||||
analyze-error? (= :analyze-error (:status file))
|
||||
import-finish? (= :import-finish (:status file))
|
||||
import-error? (= :import-error (:status file))
|
||||
import-warn? (d/not-empty? (:errors file))
|
||||
ready? (= :ready (:status file))
|
||||
is-shared? (:shared file)
|
||||
progress (:progress file)
|
||||
|
@ -177,7 +181,8 @@
|
|||
[:div.file-entry
|
||||
{:class (dom/classnames
|
||||
:loading loading?
|
||||
:success load-success?
|
||||
:success (and import-finish? (not import-warn?) (not import-error?))
|
||||
:warning (and import-finish? import-warn? (not import-error?))
|
||||
:error (or import-error? analyze-error?)
|
||||
:editable (and ready? (not editing?)))}
|
||||
|
||||
|
@ -185,8 +190,9 @@
|
|||
[:div.file-icon
|
||||
(cond loading? i/loader-pencil
|
||||
ready? i/logo-icon
|
||||
load-success? i/tick
|
||||
import-warn? i/msg-warning
|
||||
import-error? i/close
|
||||
import-finish? i/tick
|
||||
analyze-error? i/close)]
|
||||
|
||||
(if editing?
|
||||
|
@ -212,7 +218,7 @@
|
|||
[:div.error-message
|
||||
(tr "dashboard.import.import-error")]
|
||||
|
||||
(and (not load-success?) (some? progress))
|
||||
(and (not import-finish?) (some? progress))
|
||||
[:div.progress-message (parse-progress-message progress)])
|
||||
|
||||
[:div.linked-libraries
|
||||
|
@ -258,9 +264,8 @@
|
|||
:project-id project-id
|
||||
:files files})
|
||||
(rx/subs
|
||||
(fn [{:keys [file-id status message] :as msg}]
|
||||
(log/debug :msg msg)
|
||||
(swap! state update :files update-status file-id status message))))))
|
||||
(fn [{:keys [file-id status message errors] :as msg}]
|
||||
(swap! state update :files update-status file-id status message errors))))))
|
||||
|
||||
handle-cancel
|
||||
(mf/use-callback
|
||||
|
@ -291,7 +296,8 @@
|
|||
(st/emit! (modal/hide))
|
||||
(when on-finish-import (on-finish-import))))
|
||||
|
||||
success-files (->> @state :files (filter #(= (:status %) :import-success)) count)
|
||||
warning-files (->> @state :files (filter #(and (= (:status %) :import-finish) (d/not-empty? (:errors %)))) count)
|
||||
success-files (->> @state :files (filter #(and (= (:status %) :import-finish) (empty? (:errors %)))) count)
|
||||
pending-analysis? (> (->> @state :files (filter #(= (:status %) :analyzing)) count) 0)
|
||||
pending-import? (> (->> @state :files (filter #(= (:status %) :importing)) count) 0)]
|
||||
|
||||
|
@ -316,11 +322,15 @@
|
|||
{:on-click handle-cancel} i/close]]
|
||||
|
||||
[:div.modal-content
|
||||
(when (and (= :importing (:status @state))
|
||||
(not pending-import?))
|
||||
[:div.feedback-banner
|
||||
[:div.icon i/checkbox-checked]
|
||||
[:div.message (tr "dashboard.import.import-message" success-files)]])
|
||||
(when (and (= :importing (:status @state)) (not pending-import?))
|
||||
(if (> warning-files 0)
|
||||
[:div.feedback-banner.warning
|
||||
[:div.icon i/msg-warning]
|
||||
[:div.message (tr "dashboard.import.import-warning" warning-files success-files)]]
|
||||
|
||||
[:div.feedback-banner
|
||||
[:div.icon i/checkbox-checked]
|
||||
[:div.message (tr "dashboard.import.import-message" success-files)]]))
|
||||
|
||||
(for [file (->> (:files @state) (filterv (comp not :deleted?)))]
|
||||
(let [editing? (and (some? (:file-id file))
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
[width style]
|
||||
(let [values (case style
|
||||
:mixed [5 5 1 5]
|
||||
:dotted [5 5]
|
||||
;; We want 0 so they are circles
|
||||
:dotted [(- width) 5]
|
||||
:dashed [10 10]
|
||||
nil)]
|
||||
|
||||
|
@ -132,9 +133,13 @@
|
|||
;; for inner or outer strokes.
|
||||
(and (spec/stroke-caps-line (:stroke-cap-start shape))
|
||||
(= (:stroke-cap-start shape) (:stroke-cap-end shape))
|
||||
(not (#{:inner :outer} (:stroke-alignment shape))))
|
||||
(not (#{:inner :outer} (:stroke-alignment shape)))
|
||||
(not= :dotted stroke-style))
|
||||
(assoc :strokeLinecap (:stroke-cap-start shape))
|
||||
|
||||
(= :dotted stroke-style)
|
||||
(assoc :strokeLinecap "round")
|
||||
|
||||
;; For other cap types we use markers.
|
||||
(and (or (spec/stroke-caps-marker (:stroke-cap-start shape))
|
||||
(and (spec/stroke-caps-line (:stroke-cap-start shape))
|
||||
|
|
|
@ -14,11 +14,10 @@
|
|||
[cuerdas.core :as str]))
|
||||
|
||||
(defn generate-root-styles
|
||||
[shape node]
|
||||
[_shape node]
|
||||
(let [valign (:vertical-align node "top")
|
||||
width (some-> (:width shape) (+ 1))
|
||||
base #js {:height (or (:height shape) "100%")
|
||||
:width (or width "100%")
|
||||
base #js {:height "100%"
|
||||
:width "100%"
|
||||
:fontFamily "sourcesanspro"}]
|
||||
(cond-> base
|
||||
(= valign "top") (obj/set! "justifyContent" "flex-start")
|
||||
|
|
|
@ -299,7 +299,7 @@
|
|||
[{:keys [file layout project page-id] :as props}]
|
||||
(let [team-id (:team-id project)
|
||||
zoom (mf/deref refs/selected-zoom)
|
||||
params {:page-id page-id :file-id (:id file)}
|
||||
params {:page-id page-id :file-id (:id file) :section "interactions"}
|
||||
|
||||
go-back
|
||||
(mf/use-callback
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
(ns app.main.ui.workspace.shapes.text.editor
|
||||
(:require
|
||||
["draft-js" :as draft]
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.text :as txt]
|
||||
[app.main.data.workspace :as dw]
|
||||
|
@ -72,20 +71,18 @@
|
|||
(def empty-editor-state
|
||||
(ted/create-editor-state nil default-decorator))
|
||||
|
||||
(defn get-content-changes
|
||||
[old-state state]
|
||||
(let [old-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js old-state)))
|
||||
:keywordize-keys false)
|
||||
new-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js state)))
|
||||
(defn get-blocks-to-setup [block-changes]
|
||||
(->> block-changes
|
||||
(filter (fn [[_ v]]
|
||||
(nil? (:old v))))
|
||||
(mapv first)))
|
||||
|
||||
:keywordize-keys false)]
|
||||
(->> old-blocks
|
||||
(d/mapm
|
||||
(fn [bkey bstate]
|
||||
{:old (get bstate "text")
|
||||
:new (get-in new-blocks [bkey "text"])}))
|
||||
(filter #(contains? new-blocks (first %)))
|
||||
(into {}))))
|
||||
(defn get-blocks-to-add-styles
|
||||
[block-changes]
|
||||
(->> block-changes
|
||||
(filter (fn [[_ v]]
|
||||
(and (not= (:old v) (:new v)) (= (:old v) ""))))
|
||||
(mapv first)))
|
||||
|
||||
(mf/defc text-shape-edit-html
|
||||
{::mf/wrap [mf/memo]
|
||||
|
@ -143,25 +140,35 @@
|
|||
(fn [state]
|
||||
(let [old-state (mf/ref-val prev-value)]
|
||||
(if (and (some? state) (some? old-state))
|
||||
(let [block-states (get-content-changes old-state state)
|
||||
|
||||
block-to-add-styles
|
||||
(->> block-states
|
||||
(filter
|
||||
(fn [[_ v]]
|
||||
(and (not= (:old v) (:new v))
|
||||
(= (:old v) ""))))
|
||||
(mapv first))]
|
||||
(ted/apply-block-styles-to-content state block-to-add-styles))
|
||||
(let [block-changes (ted/get-content-changes old-state state)
|
||||
prev-data (ted/get-editor-current-inline-styles old-state)
|
||||
block-to-setup (get-blocks-to-setup block-changes)
|
||||
block-to-add-styles (get-blocks-to-add-styles block-changes)]
|
||||
(-> state
|
||||
(ted/setup-block-styles block-to-setup prev-data)
|
||||
(ted/apply-block-styles-to-content block-to-add-styles)))
|
||||
state))))
|
||||
|
||||
on-change
|
||||
(mf/use-callback
|
||||
(fn [val]
|
||||
(let [val (handle-change val)
|
||||
val (if (true? @blurred)
|
||||
(ted/add-editor-blur-selection val)
|
||||
(ted/remove-editor-blur-selection val))]
|
||||
(let [prev-val (mf/ref-val prev-value)
|
||||
styleOverride (ted/get-style-override prev-val)
|
||||
|
||||
;; If the content and the selection are the same we keep the style override
|
||||
keep-style? (and (some? styleOverride)
|
||||
(ted/content-equals prev-val val)
|
||||
(ted/selection-equals prev-val val))
|
||||
|
||||
val (cond-> (handle-change val)
|
||||
@blurred
|
||||
(ted/add-editor-blur-selection)
|
||||
|
||||
(not @blurred)
|
||||
(ted/remove-editor-blur-selection)
|
||||
|
||||
keep-style?
|
||||
(ted/set-style-override styleOverride))]
|
||||
(st/emit! (dwt/update-editor-state shape val)))))
|
||||
|
||||
on-editor
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
[app.main.fonts :as fonts]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.editable-select :refer [editable-select]]
|
||||
[app.main.ui.components.numeric-input :refer [numeric-input]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.sidebar.options.common :refer [advanced-options]]
|
||||
[app.util.dom :as dom]
|
||||
|
@ -22,6 +23,7 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.strings :as ust]
|
||||
[app.util.timers :as tm]
|
||||
[cuerdas.core :as str]
|
||||
[goog.events :as events]
|
||||
|
@ -30,7 +32,7 @@
|
|||
(defn- attr->string [value]
|
||||
(if (= value :multiple)
|
||||
""
|
||||
(str value)))
|
||||
(ust/format-precision value 2)))
|
||||
|
||||
(defn- get-next-font
|
||||
[{:keys [id] :as current} fonts]
|
||||
|
@ -350,20 +352,19 @@
|
|||
letter-spacing (or letter-spacing "0")
|
||||
|
||||
handle-change
|
||||
(fn [event attr]
|
||||
(let [new-spacing (dom/get-target-val event)]
|
||||
(on-change {attr new-spacing})))]
|
||||
(fn [value attr]
|
||||
(on-change {attr (str value)}))]
|
||||
|
||||
[:div.spacing-options
|
||||
[:div.input-icon
|
||||
[:span.icon-before.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.options.text-options.line-height")}
|
||||
i/line-height]
|
||||
[:input.input-text
|
||||
{:type "number"
|
||||
:step "0.1"
|
||||
:min "-200"
|
||||
:max "200"
|
||||
[:> numeric-input
|
||||
{:min -200
|
||||
:max 200
|
||||
:step 0.1
|
||||
:precision 2
|
||||
:value (attr->string line-height)
|
||||
:placeholder (tr "settings.multiple")
|
||||
:on-change #(handle-change % :line-height)
|
||||
|
@ -373,11 +374,11 @@
|
|||
[:span.icon-before.tooltip.tooltip-bottom
|
||||
{:alt (tr "workspace.options.text-options.letter-spacing")}
|
||||
i/letter-spacing]
|
||||
[:input.input-text
|
||||
{:type "number"
|
||||
:step "0.1"
|
||||
:min "-200"
|
||||
:max "200"
|
||||
[:> numeric-input
|
||||
{:min -200
|
||||
:max 200
|
||||
:step 0.1
|
||||
:precision 2
|
||||
:value (attr->string letter-spacing)
|
||||
:placeholder (tr "settings.multiple")
|
||||
:on-change #(handle-change % :letter-spacing)
|
||||
|
|
|
@ -16,30 +16,54 @@
|
|||
(defn- text-corrected-transform
|
||||
"If we apply a scale directly to the texts it will show deformed so we need to create this
|
||||
correction matrix to \"undo\" the resize but keep the other transformations."
|
||||
[{:keys [points transform transform-inverse]} current-transform modifiers]
|
||||
[{:keys [x y width height points transform transform-inverse] :as shape} current-transform modifiers]
|
||||
|
||||
(let [corner-pt (first points)
|
||||
transform (or transform (gmt/matrix))
|
||||
transform-inverse (or transform-inverse (gmt/matrix))
|
||||
corner-pt (cond-> corner-pt (some? transform-inverse) (gpt/transform transform-inverse))
|
||||
|
||||
current-transform
|
||||
(if (some? (:resize-vector modifiers))
|
||||
(gmt/multiply
|
||||
current-transform
|
||||
transform
|
||||
(gmt/scale-matrix (gpt/inverse (:resize-vector modifiers)) (gpt/transform corner-pt transform-inverse))
|
||||
transform-inverse)
|
||||
current-transform)
|
||||
resize-x? (some? (:resize-vector modifiers))
|
||||
resize-y? (some? (:resize-vector-2 modifiers))
|
||||
|
||||
current-transform
|
||||
(if (some? (:resize-vector-2 modifiers))
|
||||
(gmt/multiply
|
||||
current-transform
|
||||
transform
|
||||
(gmt/scale-matrix (gpt/inverse (:resize-vector-2 modifiers)) (gpt/transform corner-pt transform-inverse))
|
||||
transform-inverse)
|
||||
current-transform)]
|
||||
current-transform))
|
||||
flip-x? (neg? (get-in modifiers [:resize-vector :x]))
|
||||
flip-y? (or (neg? (get-in modifiers [:resize-vector :y]))
|
||||
(neg? (get-in modifiers [:resize-vector-2 :y])))
|
||||
|
||||
result (cond-> (gmt/matrix)
|
||||
(and (some? transform) (or resize-x? resize-y?))
|
||||
(gmt/multiply transform)
|
||||
|
||||
resize-x?
|
||||
(gmt/scale (gpt/inverse (:resize-vector modifiers)) corner-pt)
|
||||
|
||||
resize-y?
|
||||
(gmt/scale (gpt/inverse (:resize-vector-2 modifiers)) corner-pt)
|
||||
|
||||
flip-x?
|
||||
(gmt/scale (gpt/point -1 1) corner-pt)
|
||||
|
||||
flip-y?
|
||||
(gmt/scale (gpt/point 1 -1) corner-pt)
|
||||
|
||||
(and (some? transform) (or resize-x? resize-y?))
|
||||
(gmt/multiply transform-inverse))
|
||||
|
||||
[width height]
|
||||
(if (or resize-x? resize-y?)
|
||||
(let [pc (-> (gpt/point x y)
|
||||
(gpt/transform transform)
|
||||
(gpt/transform current-transform))
|
||||
|
||||
pw (-> (gpt/point (+ x width) y)
|
||||
(gpt/transform transform)
|
||||
(gpt/transform current-transform))
|
||||
|
||||
ph (-> (gpt/point x (+ y height))
|
||||
(gpt/transform transform)
|
||||
(gpt/transform current-transform))]
|
||||
[(gpt/distance pc pw) (gpt/distance pc ph)])
|
||||
[width height])]
|
||||
|
||||
[result width height]))
|
||||
|
||||
(defn get-nodes
|
||||
"Retrieve the DOM nodes to apply the matrix transformation"
|
||||
|
@ -48,6 +72,7 @@
|
|||
|
||||
frame? (= :frame type)
|
||||
group? (= :group type)
|
||||
text? (= :text type)
|
||||
mask? (and group? masked-group?)
|
||||
|
||||
;; When the shape is a frame we maybe need to move its thumbnail
|
||||
|
@ -68,6 +93,11 @@
|
|||
group?
|
||||
[]
|
||||
|
||||
text?
|
||||
[shape-node
|
||||
(dom/query shape-node "foreignObject")
|
||||
(dom/query shape-node ".text-shape")]
|
||||
|
||||
:else
|
||||
[shape-node])))
|
||||
|
||||
|
@ -76,11 +106,23 @@
|
|||
(when-let [nodes (get-nodes shape)]
|
||||
(let [transform (get transforms id)
|
||||
modifiers (get-in modifiers [id :modifiers])
|
||||
transform (case type
|
||||
:text (text-corrected-transform shape transform modifiers)
|
||||
transform)]
|
||||
|
||||
[text-transform text-width text-height]
|
||||
(when (= :text type)
|
||||
(text-corrected-transform shape transform modifiers))]
|
||||
|
||||
(doseq [node nodes]
|
||||
(when (and (some? transform) (some? node))
|
||||
(cond
|
||||
(dom/class? node "text-shape")
|
||||
(when (some? text-transform)
|
||||
(dom/set-attribute node "transform" (str text-transform)))
|
||||
|
||||
(= (dom/get-tag-name node) "foreignObject")
|
||||
(when (and (some? text-width) (some? text-height))
|
||||
(dom/set-attribute node "width" text-width)
|
||||
(dom/set-attribute node "height" text-height))
|
||||
|
||||
(and (some? transform) (some? node))
|
||||
(dom/set-attribute node "transform" (str transform))))))))
|
||||
|
||||
(defn remove-transform [shapes]
|
||||
|
@ -88,7 +130,13 @@
|
|||
(when-let [nodes (get-nodes shape)]
|
||||
(doseq [node nodes]
|
||||
(when (some? node)
|
||||
(dom/remove-attribute node "transform"))))))
|
||||
(cond
|
||||
(= (dom/get-tag-name node) "foreignObject")
|
||||
;; The shape width/height will be automaticaly setup when the modifiers are applied
|
||||
nil
|
||||
|
||||
:else
|
||||
(dom/remove-attribute node "transform")))))))
|
||||
|
||||
(defn format-viewbox [vbox]
|
||||
(str/join " " [(+ (:x vbox 0) (:left-offset vbox 0))
|
||||
|
|
|
@ -17,21 +17,24 @@
|
|||
;; --- Deprecated methods
|
||||
|
||||
(defn event->inner-text
|
||||
[e]
|
||||
(.-innerText (.-target e)))
|
||||
[^js e]
|
||||
(when (some? e)
|
||||
(.-innerText (.-target e))))
|
||||
|
||||
(defn event->value
|
||||
[e]
|
||||
(.-value (.-target e)))
|
||||
[^js e]
|
||||
(when (some? e)
|
||||
(.-value (.-target e))))
|
||||
|
||||
(defn event->target
|
||||
[e]
|
||||
(.-target e))
|
||||
[^js e]
|
||||
(when (some? e)
|
||||
(.-target e)))
|
||||
|
||||
;; --- New methods
|
||||
|
||||
(defn set-html-title
|
||||
[title]
|
||||
[^string title]
|
||||
(set! (.-title globals/document) title))
|
||||
|
||||
(defn set-page-style
|
||||
|
@ -61,98 +64,117 @@
|
|||
(dom/getElement id))
|
||||
|
||||
(defn get-elements-by-tag
|
||||
[node tag]
|
||||
(.getElementsByTagName node tag))
|
||||
[^js node tag]
|
||||
(when (some? node)
|
||||
(.getElementsByTagName node tag)))
|
||||
|
||||
(defn stop-propagation
|
||||
[e]
|
||||
(when e
|
||||
(.stopPropagation e)))
|
||||
[^js event]
|
||||
(when event
|
||||
(.stopPropagation event)))
|
||||
|
||||
(defn prevent-default
|
||||
[e]
|
||||
(when e
|
||||
(.preventDefault e)))
|
||||
[^js event]
|
||||
(when event
|
||||
(.preventDefault event)))
|
||||
|
||||
(defn get-target
|
||||
"Extract the target from event instance."
|
||||
[event]
|
||||
(.-target event))
|
||||
[^js event]
|
||||
(when (some? event)
|
||||
(.-target event)))
|
||||
|
||||
(defn get-current-target
|
||||
"Extract the current target from event instance (different from target
|
||||
when event triggered in a child of the subscribing element)."
|
||||
[event]
|
||||
(.-currentTarget event))
|
||||
[^js event]
|
||||
(when (some? event)
|
||||
(.-currentTarget event)))
|
||||
|
||||
(defn get-parent
|
||||
[dom]
|
||||
(.-parentElement ^js dom))
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(.-parentElement ^js node)))
|
||||
|
||||
(defn get-value
|
||||
"Extract the value from dom node."
|
||||
[node]
|
||||
(.-value node))
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(.-value node)))
|
||||
|
||||
(defn get-attribute
|
||||
"Extract the value of one attribute of a dom node."
|
||||
[node attr-name]
|
||||
(.getAttribute ^js node attr-name))
|
||||
[^js node ^string attr-name]
|
||||
(when (some? node)
|
||||
(.getAttribute ^js node attr-name)))
|
||||
|
||||
(def get-target-val (comp get-value get-target))
|
||||
|
||||
(defn click
|
||||
"Click a node"
|
||||
[node]
|
||||
(.click node))
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(.click node)))
|
||||
|
||||
(defn get-files
|
||||
"Extract the files from dom node."
|
||||
[node]
|
||||
(array-seq (.-files node)))
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(array-seq (.-files node))))
|
||||
|
||||
(defn checked?
|
||||
"Check if the node that represents a radio
|
||||
or checkbox is checked or not."
|
||||
[node]
|
||||
(.-checked node))
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(.-checked node)))
|
||||
|
||||
(defn valid?
|
||||
"Check if the node that is a form input
|
||||
has a valid value, against html5 form validation
|
||||
properties (required, min/max, pattern...)."
|
||||
[node]
|
||||
(.-valid (.-validity node)))
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(when-let [validity (.-validity node)]
|
||||
(.-valid validity))))
|
||||
|
||||
(defn set-validity!
|
||||
"Manually set the validity status of a node that
|
||||
is a form input. If the state is an empty string,
|
||||
the input will be valid. If not, the string will
|
||||
be set as the error message."
|
||||
[node status]
|
||||
(.setCustomValidity node status)
|
||||
(.reportValidity node))
|
||||
[^js node status]
|
||||
(when (some? node)
|
||||
(.setCustomValidity node status)
|
||||
(.reportValidity node)))
|
||||
|
||||
(defn clean-value!
|
||||
[node]
|
||||
(set! (.-value node) ""))
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(set! (.-value node) "")))
|
||||
|
||||
(defn set-value!
|
||||
[node value]
|
||||
(set! (.-value ^js node) value))
|
||||
[^js node value]
|
||||
(when (some? node)
|
||||
(set! (.-value ^js node) value)))
|
||||
|
||||
(defn select-text!
|
||||
[node]
|
||||
(.select ^js node))
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(.select ^js node)))
|
||||
|
||||
(defn ^boolean equals?
|
||||
[node-a node-b]
|
||||
(.isEqualNode ^js node-a node-b))
|
||||
[^js node-a ^js node-b]
|
||||
|
||||
(or (and (nil? node-a) (nil? node-b))
|
||||
(and (some? node-a)
|
||||
(.isEqualNode ^js node-a node-b))))
|
||||
|
||||
(defn get-event-files
|
||||
"Extract the files from event instance."
|
||||
[event]
|
||||
(get-files (get-target event)))
|
||||
[^js event]
|
||||
(when (some? event)
|
||||
(get-files (get-target event))))
|
||||
|
||||
(defn create-element
|
||||
([tag]
|
||||
|
@ -161,50 +183,58 @@
|
|||
(.createElementNS globals/document ns tag)))
|
||||
|
||||
(defn set-html!
|
||||
[el html]
|
||||
(set! (.-innerHTML el) html))
|
||||
[^js el html]
|
||||
(when (some? el)
|
||||
(set! (.-innerHTML el) html)))
|
||||
|
||||
(defn append-child!
|
||||
[el child]
|
||||
(.appendChild ^js el child))
|
||||
[^js el child]
|
||||
(when (some? el)
|
||||
(.appendChild ^js el child)))
|
||||
|
||||
(defn get-first-child
|
||||
[el]
|
||||
(.-firstChild el))
|
||||
[^js el]
|
||||
(when (some? el)
|
||||
(.-firstChild el)))
|
||||
|
||||
(defn get-tag-name
|
||||
[el]
|
||||
(.-tagName el))
|
||||
[^js el]
|
||||
(when (some? el)
|
||||
(.-tagName el)))
|
||||
|
||||
(defn get-outer-html
|
||||
[el]
|
||||
(.-outerHTML el))
|
||||
[^js el]
|
||||
(when (some? el)
|
||||
(.-outerHTML el)))
|
||||
|
||||
(defn get-inner-text
|
||||
[el]
|
||||
(.-innerText el))
|
||||
[^js el]
|
||||
(when (some? el)
|
||||
(.-innerText el)))
|
||||
|
||||
(defn query
|
||||
[el query]
|
||||
[^js el ^string query]
|
||||
(when (some? el)
|
||||
(.querySelector el query)))
|
||||
|
||||
(defn get-client-position
|
||||
[event]
|
||||
[^js event]
|
||||
(let [x (.-clientX event)
|
||||
y (.-clientY event)]
|
||||
(gpt/point x y)))
|
||||
|
||||
(defn get-offset-position
|
||||
[event]
|
||||
(let [x (.-offsetX event)
|
||||
y (.-offsetY event)]
|
||||
(gpt/point x y)))
|
||||
[^js event]
|
||||
(when (some? event)
|
||||
(let [x (.-offsetX event)
|
||||
y (.-offsetY event)]
|
||||
(gpt/point x y))))
|
||||
|
||||
(defn get-client-size
|
||||
[node]
|
||||
{:width (.-clientWidth ^js node)
|
||||
:height (.-clientHeight ^js node)})
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
{:width (.-clientWidth ^js node)
|
||||
:height (.-clientHeight ^js node)}))
|
||||
|
||||
(defn get-bounding-rect
|
||||
[node]
|
||||
|
@ -222,12 +252,12 @@
|
|||
:height (.-innerHeight ^js js/window)})
|
||||
|
||||
(defn focus!
|
||||
[node]
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(.focus node)))
|
||||
|
||||
(defn blur!
|
||||
[node]
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(.blur node)))
|
||||
|
||||
|
@ -245,8 +275,9 @@
|
|||
:hint "seems like the current browser does not support fullscreen api.")))
|
||||
|
||||
(defn ^boolean blob?
|
||||
[v]
|
||||
(instance? js/Blob v))
|
||||
[^js v]
|
||||
(when (some? v)
|
||||
(instance? js/Blob v)))
|
||||
|
||||
(defn create-blob
|
||||
"Create a blob from content."
|
||||
|
@ -265,20 +296,24 @@
|
|||
{:pre [(blob? b)]}
|
||||
(js/URL.createObjectURL b))
|
||||
|
||||
(defn set-property! [node property value]
|
||||
(.setAttribute node property value))
|
||||
(defn set-property! [^js node property value]
|
||||
(when (some? node)
|
||||
(.setAttribute node property value)))
|
||||
|
||||
(defn set-text! [node text]
|
||||
(set! (.-textContent node) text))
|
||||
(defn set-text! [^js node text]
|
||||
(when (some? node)
|
||||
(set! (.-textContent node) text)))
|
||||
|
||||
(defn set-css-property! [node property value]
|
||||
(.setProperty (.-style ^js node) property value))
|
||||
(defn set-css-property! [^js node property value]
|
||||
(when (some? node)
|
||||
(.setProperty (.-style ^js node) property value)))
|
||||
|
||||
(defn capture-pointer [event]
|
||||
(-> event get-target (.setPointerCapture (.-pointerId event))))
|
||||
(defn capture-pointer [^js event]
|
||||
(when (some? event)
|
||||
(-> event get-target (.setPointerCapture (.-pointerId event)))))
|
||||
|
||||
(defn release-pointer [event]
|
||||
(when (.-pointerId event)
|
||||
(defn release-pointer [^js event]
|
||||
(when (and (some? event) (.-pointerId event))
|
||||
(-> event get-target (.releasePointerCapture (.-pointerId event)))))
|
||||
|
||||
(defn get-root []
|
||||
|
@ -295,19 +330,23 @@
|
|||
(partition 2 params))))
|
||||
|
||||
(defn ^boolean class? [node class-name]
|
||||
(let [class-list (.-classList ^js node)]
|
||||
(.contains ^js class-list class-name)))
|
||||
(when (some? node)
|
||||
(let [class-list (.-classList ^js node)]
|
||||
(.contains ^js class-list class-name))))
|
||||
|
||||
(defn add-class! [node class-name]
|
||||
(let [class-list (.-classList ^js node)]
|
||||
(.add ^js class-list class-name)))
|
||||
(defn add-class! [^js node class-name]
|
||||
(when (some? node)
|
||||
(let [class-list (.-classList ^js node)]
|
||||
(.add ^js class-list class-name))))
|
||||
|
||||
(defn remove-class! [node class-name]
|
||||
(let [class-list (.-classList ^js node)]
|
||||
(.remove ^js class-list class-name)))
|
||||
(defn remove-class! [^js node class-name]
|
||||
(when (some? node)
|
||||
(let [class-list (.-classList ^js node)]
|
||||
(.remove ^js class-list class-name))))
|
||||
|
||||
(defn child? [node1 node2]
|
||||
(.contains ^js node2 ^js node1))
|
||||
(defn child? [^js node1 ^js node2]
|
||||
(when (some? node1)
|
||||
(.contains ^js node2 ^js node1)))
|
||||
|
||||
(defn get-user-agent []
|
||||
(.-userAgent globals/navigator))
|
||||
|
@ -315,11 +354,13 @@
|
|||
(defn get-active []
|
||||
(.-activeElement globals/document))
|
||||
|
||||
(defn active? [node]
|
||||
(= (get-active) node))
|
||||
(defn active? [^js node]
|
||||
(when (some? node)
|
||||
(= (get-active) node)))
|
||||
|
||||
(defn get-data [^js node ^string attr]
|
||||
(.getAttribute node (str "data-" attr)))
|
||||
(when (some? node)
|
||||
(.getAttribute node (str "data-" attr))))
|
||||
|
||||
(defn mtype->extension [mtype]
|
||||
;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
|
||||
|
@ -336,42 +377,53 @@
|
|||
nil))
|
||||
|
||||
(defn set-attribute [^js node ^string attr value]
|
||||
(.setAttribute node attr value))
|
||||
(when (some? node)
|
||||
(.setAttribute node attr value)))
|
||||
|
||||
(defn remove-attribute [^js node ^string attr]
|
||||
(.removeAttribute node attr))
|
||||
(when (some? node)
|
||||
(.removeAttribute node attr)))
|
||||
|
||||
(defn get-scroll-pos
|
||||
[element]
|
||||
(.-scrollTop ^js element))
|
||||
[^js element]
|
||||
(when (some? element)
|
||||
(.-scrollTop element)))
|
||||
|
||||
(defn set-scroll-pos!
|
||||
[element scroll]
|
||||
(obj/set! ^js element "scrollTop" scroll))
|
||||
[^js element scroll]
|
||||
(when (some? element)
|
||||
(obj/set! element "scrollTop" scroll)))
|
||||
|
||||
(defn scroll-into-view!
|
||||
([element]
|
||||
(.scrollIntoView ^js element false))
|
||||
([element scroll-top]
|
||||
(.scrollIntoView ^js element scroll-top)))
|
||||
([^js element]
|
||||
(when (some? element)
|
||||
(.scrollIntoView element false)))
|
||||
|
||||
([^js element scroll-top]
|
||||
(when (some? element)
|
||||
(.scrollIntoView element scroll-top))))
|
||||
|
||||
(defn scroll-into-view-if-needed!
|
||||
([element]
|
||||
(.scrollIntoViewIfNeeded ^js element false))
|
||||
([element scroll-top]
|
||||
(.scrollIntoViewIfNeeded ^js element scroll-top)))
|
||||
([^js element]
|
||||
(when (some? element)
|
||||
(.scrollIntoViewIfNeeded ^js element false)))
|
||||
|
||||
([^js element scroll-top]
|
||||
(when (some? element)
|
||||
(.scrollIntoViewIfNeeded ^js element scroll-top))))
|
||||
|
||||
(defn is-in-viewport?
|
||||
[element]
|
||||
(let [rect (.getBoundingClientRect element)
|
||||
height (or (.-innerHeight js/window)
|
||||
(.. js/document -documentElement -clientHeight))
|
||||
width (or (.-innerWidth js/window)
|
||||
(.. js/document -documentElement -clientWidth))]
|
||||
(and (>= (.-top rect) 0)
|
||||
(>= (.-left rect) 0)
|
||||
(<= (.-bottom rect) height)
|
||||
(<= (.-right rect) width))))
|
||||
[^js element]
|
||||
(when (some? element)
|
||||
(let [rect (.getBoundingClientRect element)
|
||||
height (or (.-innerHeight js/window)
|
||||
(.. js/document -documentElement -clientHeight))
|
||||
width (or (.-innerWidth js/window)
|
||||
(.. js/document -documentElement -clientWidth))]
|
||||
(and (>= (.-top rect) 0)
|
||||
(>= (.-left rect) 0)
|
||||
(<= (.-bottom rect) height)
|
||||
(<= (.-right rect) width)))))
|
||||
|
||||
(defn trigger-download-uri
|
||||
[filename mtype uri]
|
||||
|
|
38
frontend/src/app/util/strings.cljs
Normal file
38
frontend/src/app/util/strings.cljs
Normal file
|
@ -0,0 +1,38 @@
|
|||
;; 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/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.util.strings
|
||||
(:require
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
|
||||
(def ^:const trail-zeros-regex-1 #"\.0+$")
|
||||
(def ^:const trail-zeros-regex-2 #"(\.\d*[^0])0+$")
|
||||
|
||||
(defn format-precision
|
||||
"Creates a number with predetermined precision and then removes the trailing 0.
|
||||
Examples:
|
||||
12.0123, 0 => 12
|
||||
12.0123, 1 => 12
|
||||
12.0123, 2 => 12.01"
|
||||
[num precision]
|
||||
|
||||
(try
|
||||
(if (number? num)
|
||||
(let [num-str (.toFixed num precision)
|
||||
|
||||
;; Remove all trailing zeros after the comma 100.00000
|
||||
num-str (str/replace num-str trail-zeros-regex-1 "")
|
||||
|
||||
;; Remove trailing zeros after a decimal number: 0.001|00|
|
||||
num-str (if-let [m (re-find trail-zeros-regex-2 num-str)]
|
||||
(str/replace num-str (first m) (second m))
|
||||
num-str)]
|
||||
num-str)
|
||||
(str num))
|
||||
(catch :default _
|
||||
(str num))))
|
||||
|
|
@ -111,6 +111,16 @@
|
|||
[state]
|
||||
(impl/cursorToEnd state))
|
||||
|
||||
(defn setup-block-styles
|
||||
[state blocks attrs]
|
||||
(if (empty? blocks)
|
||||
state
|
||||
(->> blocks
|
||||
(reduce
|
||||
(fn [state block-key]
|
||||
(impl/updateBlockData state block-key (clj->js attrs)))
|
||||
state))))
|
||||
|
||||
(defn apply-block-styles-to-content
|
||||
[state blocks]
|
||||
(if (empty? blocks)
|
||||
|
@ -130,3 +140,37 @@
|
|||
(defn insert-text [state text attrs]
|
||||
(let [style (txt/attrs-to-styles attrs)]
|
||||
(impl/insertText state text (clj->js attrs) (clj->js style))))
|
||||
|
||||
(defn get-style-override [state]
|
||||
(.getInlineStyleOverride state))
|
||||
|
||||
(defn set-style-override [state inline-style]
|
||||
(impl/setInlineStyleOverride state inline-style))
|
||||
|
||||
(defn content-equals [state other]
|
||||
(.equals (.getCurrentContent state) (.getCurrentContent other)))
|
||||
|
||||
(defn selection-equals [state other]
|
||||
(impl/selectionEquals (.getSelection state) (.getSelection other)))
|
||||
|
||||
(defn get-content-changes
|
||||
[old-state state]
|
||||
(let [old-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js old-state)))
|
||||
:keywordize-keys false)
|
||||
new-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js state)))
|
||||
:keywordize-keys false)]
|
||||
(merge
|
||||
(into {}
|
||||
(comp (filter #(contains? new-blocks (first %)))
|
||||
(map (fn [[bkey bstate]]
|
||||
[bkey
|
||||
{:old (get bstate "text")
|
||||
:new (get-in new-blocks [bkey "text"])}])))
|
||||
old-blocks)
|
||||
(into {}
|
||||
(comp (filter #(not (contains? old-blocks (first %))))
|
||||
(map (fn [[bkey bstate]]
|
||||
[bkey
|
||||
{:old nil
|
||||
:new (get bstate "text")}])))
|
||||
new-blocks))))
|
||||
|
|
|
@ -121,15 +121,33 @@ export function updateCurrentBlockData(state, attrs) {
|
|||
return EditorState.push(state, content, "change-block-data");
|
||||
}
|
||||
|
||||
function addStylesToOverride(styles, other) {
|
||||
let result = styles;
|
||||
|
||||
for (let style of other) {
|
||||
const [p, k, v] = style.split("$$$");
|
||||
const prefix = [p, k, ""].join("$$$");
|
||||
|
||||
const curValue = result.find((it) => it.startsWith(prefix))
|
||||
if (curValue) {
|
||||
result = result.remove(curValue);
|
||||
}
|
||||
result = result.add(style);
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function applyInlineStyle(state, styles) {
|
||||
const userSelection = state.getSelection();
|
||||
let selection = userSelection;
|
||||
let result = state;
|
||||
|
||||
if (selection.isCollapsed()) {
|
||||
selection = getSelectAllSelection(state);
|
||||
const currentOverride = state.getCurrentInlineStyle() || new OrderedSet();
|
||||
const styleOverride = addStylesToOverride(currentOverride, styles)
|
||||
return EditorState.setInlineStyleOverride(state, styleOverride);
|
||||
}
|
||||
|
||||
let result = state;
|
||||
let content = null;
|
||||
|
||||
for (let style of styles) {
|
||||
|
@ -300,6 +318,7 @@ export function getBlockData(state, blockKey) {
|
|||
|
||||
export function updateBlockData(state, blockKey, data) {
|
||||
const userSelection = state.getSelection();
|
||||
const inlineStyleOverride = state.getInlineStyleOverride();
|
||||
const content = state.getCurrentContent();
|
||||
const block = content.getBlockForKey(blockKey);
|
||||
const newBlock = mergeBlockData(block, data);
|
||||
|
@ -312,8 +331,10 @@ export function updateBlockData(state, blockKey, data) {
|
|||
blockData
|
||||
);
|
||||
|
||||
const result = EditorState.push(state, newContent, 'change-block-data');
|
||||
return EditorState.acceptSelection(result, userSelection);
|
||||
let result = EditorState.push(state, newContent, 'change-block-data');
|
||||
result = EditorState.acceptSelection(result, userSelection);
|
||||
result = EditorState.setInlineStyleOverride(result, inlineStyleOverride);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getSelection(state) {
|
||||
|
@ -376,3 +397,15 @@ export function insertText(state, text, attrs, inlineStyles) {
|
|||
const resultSelection = SelectionState.createEmpty(selection.getStartKey());
|
||||
return EditorState.push(state, newContent, 'insert-fragment');
|
||||
}
|
||||
|
||||
export function setInlineStyleOverride(state, inlineStyles) {
|
||||
return EditorState.setInlineStyleOverride(state, inlineStyles);
|
||||
}
|
||||
|
||||
export function selectionEquals(selection, other) {
|
||||
return selection.getAnchorKey() === other.getAnchorKey() &&
|
||||
selection.getAnchorOffset() === other.getAnchorOffset() &&
|
||||
selection.getFocusKey() === other.getFocusKey() &&
|
||||
selection.getFocusOffset() === other.getFocusOffset() &&
|
||||
selection.getIsBackward() === other.getIsBackward();
|
||||
}
|
||||
|
|
|
@ -176,7 +176,9 @@
|
|||
(rx/tap #(reset! revn (:revn %)))
|
||||
(rx/ignore))
|
||||
|
||||
(rp/mutation :persist-temp-file {:id file-id}))))
|
||||
(->> (rp/mutation :persist-temp-file {:id file-id})
|
||||
;; We use merge to keep some information not stored in back-end
|
||||
(rx/map #(merge file %))))))
|
||||
|
||||
(defn upload-media-files
|
||||
"Upload a image to the backend and returns its id"
|
||||
|
@ -457,8 +459,7 @@
|
|||
|
||||
(let [progress-str (rx/subject)
|
||||
context (assoc context :progress progress-str)]
|
||||
(rx/merge
|
||||
progress-str
|
||||
[progress-str
|
||||
(->> (rx/of file)
|
||||
(rx/flat-map (partial process-pages context))
|
||||
(rx/tap #(progress! context :process-colors))
|
||||
|
@ -470,7 +471,7 @@
|
|||
(rx/tap #(progress! context :process-components))
|
||||
(rx/flat-map (partial process-library-components context))
|
||||
(rx/flat-map (partial send-changes context))
|
||||
(rx/tap #(rx/end! progress-str))))))
|
||||
(rx/tap #(rx/end! progress-str)))]))
|
||||
|
||||
(defn create-files
|
||||
[context files]
|
||||
|
@ -482,7 +483,6 @@
|
|||
(rx/flat-map
|
||||
(fn [context]
|
||||
(->> (create-file context)
|
||||
(rx/tap #(.log js/console "create-file" (clj->js %)))
|
||||
(rx/map #(vector % (first (get data (:file-id context)))))))))
|
||||
|
||||
(->> (rx/from files)
|
||||
|
@ -509,20 +509,29 @@
|
|||
(let [context {:project-id project-id
|
||||
:resolve (resolve-factory)}]
|
||||
(->> (create-files context files)
|
||||
(rx/catch #(.error js/console "IMPORT ERROR" %))
|
||||
(rx/catch #(.error js/console "IMPORT ERROR" (clj->js %)))
|
||||
(rx/flat-map
|
||||
(fn [[file data]]
|
||||
(->> (rx/concat
|
||||
(->> (uz/load-from-url (:uri data))
|
||||
(rx/map #(-> context (assoc :zip %) (merge data)))
|
||||
(rx/flat-map #(process-file % file)))
|
||||
(rx/of
|
||||
{:status :import-success
|
||||
:file-id (:file-id data)}))
|
||||
(rx/flat-map
|
||||
(fn [context]
|
||||
;; process file retrieves a stream that will emit progress notifications
|
||||
;; and other that will emit the files once imported
|
||||
(let [[progress-stream file-stream] (process-file context file)]
|
||||
(rx/merge
|
||||
progress-stream
|
||||
(->> file-stream
|
||||
(rx/map
|
||||
(fn [file]
|
||||
{:status :import-finish
|
||||
:errors (:errors file)
|
||||
:file-id (:file-id data)})))))))))
|
||||
|
||||
(rx/catch
|
||||
(fn [err]
|
||||
(.error js/console "ERROR" (:file-id data) err)
|
||||
(.error js/console "ERROR" (str (:file-id data)) (clj->js err) (clj->js (.-data err)))
|
||||
(rx/of {:status :import-error
|
||||
:file-id (:file-id data)
|
||||
:error (.-message err)
|
||||
|
|
|
@ -3322,3 +3322,6 @@ msgstr "Insufficient members to leave team, you probably want to delete it."
|
|||
|
||||
msgid "errors.auth.unable-to-login"
|
||||
msgstr "Looks like you are not authenticated or session expired."
|
||||
|
||||
msgid "dashboard.import.import-warning"
|
||||
msgstr "Some files containted invalid objects that have been removed."
|
||||
|
|
|
@ -3309,3 +3309,6 @@ msgstr "Actualizar"
|
|||
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "Pulsar para cerrar la ruta"
|
||||
|
||||
msgid "dashboard.import.import-warning"
|
||||
msgstr "Algunos ficheros contenían objetos erroneos que no han sido importados."
|
||||
|
|
Loading…
Add table
Reference in a new issue