diff --git a/CHANGES.md b/CHANGES.md index 1727ae2c3..e82b1dc2f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,17 @@ ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) +## 1.13.4-beta + +### :bug: Bugs fixed + +- Fix undo when drawing curves [Taiga #3523](https://tree.taiga.io/project/penpot/issue/3523) +- Fix issue with text edition and certain fonts (WorkSans, Raleway, ...) and foreign objects [Taiga #3521](https://tree.taiga.io/project/penpot/issue/3521) +- Fix thumbnail generation when concurrent edition [Taiga #3522](https://tree.taiga.io/project/penpot/issue/3522) +- Fix environment imporot for exporter in Docker +- Fix auto scroll layers in Firefox [Taiga #3531](https://tree.taiga.io/project/penpot/issue/3531) +- Fix base background not visible for imported SVG + ## 1.13.3-beta ### :bug: Bugs fixed diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index 6ec344559..e5455aab6 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -23,7 +23,41 @@ [expound.alpha :as expound] [fipp.edn :refer [pprint]])) +;; ==== Utility functions + +(defn reset-file-data + "Hardcode replace of the data of one file." + [system id data] + (db/with-atomic [conn (:app.db/pool system)] + (db/update! conn :file + {:data data} + {:id id}))) + +(defn get-file + "Get the migrated data of one file." + [system id] + (-> (:app.db/pool system) + (db/get-by-id :file id) + (update :data app.util.blob/decode) + (update :data pmg/migrate-data))) + +(defn duplicate-file + "This is a raw version of duplication of file just only for forensic analysis." + [system file-id email] + (db/with-atomic [conn (:app.db/pool system)] + (when-let [profile (some->> (prof/retrieve-profile-data-by-email conn (str/lower email)) + (prof/populate-additional-data conn))] + (when-let [file (db/exec-one! conn (sql/select :file {:id file-id}))] + (let [params (assoc file + :id (uuid/next) + :project-id (:default-project-id profile))] + (db/insert! conn :file params) + (:id file)))))) + (defn update-file + "Apply a function to the data of one file. Optionally save the changes or not. + + The function receives the decoded and migrated file data." ([system id f] (update-file system id f false)) ([system id f save?] (db/with-atomic [conn (:app.db/pool system)] @@ -40,85 +74,115 @@ {:id (:id file)})) (update file :data blob/decode))))) -(defn reset-file-data - [system id data] - (db/with-atomic [conn (:app.db/pool system)] - (db/update! conn :file - {:data data} - {:id id}))) +(defn analyze-files + "Apply a function to all files in the database, reading them in batches. Do not change data. + + The function receives an object with some properties of the file and the decoded data, and + an empty atom where it may accumulate statistics, if desired." + [system {:keys [sleep chunk-size max-chunks on-file] + :or {sleep 1000 chunk-size 10 max-chunks ##Inf}}] + (let [stats (atom {})] + (letfn [(retrieve-chunk [conn cursor] + (let [sql (str "select id, name, modified_at, data from file " + " where modified_at < ? and deleted_at is null " + " order by modified_at desc limit ?")] + (->> (db/exec! conn [sql cursor chunk-size]) + (map #(update % :data blob/decode))))) -(defn get-file - [system id] - (-> (:app.db/pool system) - (db/get-by-id :file id) - (update :data app.util.blob/decode) - (update :data pmg/migrate-data))) + (process-chunk [chunk] + (loop [files chunk] + (when-let [file (first files)] + (on-file file stats) + (recur (rest files)))))] -(defn duplicate-file - "This is a raw version of duplication of file just only for forensic analysis" - [system file-id email] - (db/with-atomic [conn (:app.db/pool system)] - (when-let [profile (some->> (prof/retrieve-profile-data-by-email conn (str/lower email)) - (prof/populate-additional-data conn))] - (when-let [file (db/exec-one! conn (sql/select :file {:id file-id}))] - (let [params (assoc file - :id (uuid/next) - :project-id (:default-project-id profile))] - (db/insert! conn :file params) - (:id file)))))) + (db/with-atomic [conn (:app.db/pool system)] + (loop [cursor (dt/now) + chunks 0] + (when (< chunks max-chunks) + (let [chunk (retrieve-chunk conn cursor)] + (when-not (empty? chunk) + (let [cursor (-> chunk last :modified-at)] + (process-chunk chunk) + (Thread/sleep (inst-ms (dt/duration sleep))) + (recur cursor (inc chunks))))))) + @stats)))) -(defn repair-orphaned-components - "We have detected some cases of component instances that are not nested, but - however they have not the :component-root? attribute (so the system considers - them nested). This script fixes this adding them the attribute. +(defn update-pages + "Apply a function to all pages of one file. The function receives a page and returns an updated page." + [data f] + (update data :pages-index d/update-vals f)) - Use it with the update-file function above." - [data] - (let [update-page - (fn [page] - (prn "================= Page:" (:name page)) - (letfn [(is-nested? [object] - (and (some? (:component-id object)) - (nil? (:component-root? object)))) +(defn update-shapes + "Apply a function to all shapes of one page The function receives a shape and returns an updated shape" + [page f] + (update page :objects d/update-vals f)) - (is-instance? [object] - (some? (:shape-ref object))) - (get-parent [object] - (get (:objects page) (:parent-id object))) +;; ==== Specific fixes - (update-object [object] - (if (and (is-nested? object) - (not (is-instance? (get-parent object)))) - (do - (prn "Orphan:" (:name object)) - (assoc object :component-root? true)) - object))] +(defn repair-orphaned-shapes + "There are some shapes whose parent has been deleted. This + function detects them and puts them as children of the root node." + ([file _] ; to be called from analyze-files to search for files with the problem + (repair-orphaned-shapes (:data file))) - (update page :objects d/update-vals update-object)))] + ([data] + (let [is-orphan? (fn [shape objects] + (and (some? (:parent-id shape)) + (nil? (get objects (:parent-id shape))))) - (update data :pages-index d/update-vals update-page))) + update-page (fn [page] + (let [objects (:objects page) + orphans (set (filter #(is-orphan? % objects) (vals objects)))] + (if (seq orphans) + (do + (prn (:id data) "file has" (count orphans) "broken shapes") + (-> page + (update-shapes (fn [shape] + (if (orphans shape) + (assoc shape :parent-id uuid/zero) + shape))) + (update-in [:objects uuid/zero :shapes] + (fn [shapes] (into shapes (map :id orphans)))))) + page)))] -(defn repair-idless-components - "There are some files that contains components with no :id attribute. - This function detects them and repairs it. + (update-pages data update-page)))) - Use it with the update-file function above." - [data] - (letfn [(update-component [id component] - (if (nil? (:id component)) - (do - (prn (:id data) "Broken component" (:name component) id) - (assoc component :id id)) - component))] - (update data :components #(d/mapm update-component %)))) +;; DO NOT DELETE already used scripts, could be taken as templates for easyly writing new ones +;; ------------------------------------------------------------------------------------------- -(defn analyze-idless-components - "Scan all files to check if there are any one with idless components. - (Does not save the changes, only used to detect affected files)." - [file _] - (repair-idless-components (:data file))) +;; (defn repair-orphaned-components +;; "We have detected some cases of component instances that are not nested, but +;; however they have not the :component-root? attribute (so the system considers +;; them nested). This script fixes this adding them the attribute. +;; +;; Use it with the update-file function above." +;; [data] +;; (let [update-page +;; (fn [page] +;; (prn "================= Page:" (:name page)) +;; (letfn [(is-nested? [object] +;; (and (some? (:component-id object)) +;; (nil? (:component-root? object)))) +;; +;; (is-instance? [object] +;; (some? (:shape-ref object))) +;; +;; (get-parent [object] +;; (get (:objects page) (:parent-id object))) +;; +;; (update-object [object] +;; (if (and (is-nested? object) +;; (not (is-instance? (get-parent object)))) +;; (do +;; (prn "Orphan:" (:name object)) +;; (assoc object :component-root? true)) +;; object))] +;; +;; (update page :objects d/update-vals update-object)))] +;; +;; (update data :pages-index d/update-vals update-page))) ;; (defn check-image-shapes ;; [{:keys [data] :as file} stats] @@ -138,32 +202,3 @@ ;; (when @affected? ;; (swap! stats update :affected-files (fnil inc 0))))) -(defn analyze-files - [system {:keys [sleep chunk-size max-chunks on-file] - :or {sleep 1000 chunk-size 10 max-chunks ##Inf}}] - (let [stats (atom {})] - (letfn [(retrieve-chunk [conn cursor] - (let [sql (str "select id, name, modified_at, data from file " - " where modified_at < ? and deleted_at is null " - " order by modified_at desc limit ?")] - (->> (db/exec! conn [sql cursor chunk-size]) - (map #(update % :data blob/decode))))) - - (process-chunk [chunk] - (loop [items chunk] - (when-let [item (first items)] - (on-file item stats) - (recur (rest items)))))] - - (db/with-atomic [conn (:app.db/pool system)] - (loop [cursor (dt/now) - chunks 0] - (when (< chunks max-chunks) - (let [chunk (retrieve-chunk conn cursor)] - (when-not (empty? chunk) - (let [cursor (-> chunk last :modified-at)] - (process-chunk chunk) - (Thread/sleep (inst-ms (dt/duration sleep))) - (recur cursor (inc chunks))))))) - @stats)))) - diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index 75ab687e0..e0f162c39 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -44,6 +44,8 @@ services: penpot-exporter: image: "penpotapp/exporter:latest" + env_file: + - config.env environment: # Don't touch it; this uses internal docker network to # communicate with the frontend. diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index bdbccb122..47b835c60 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -26,7 +26,7 @@ :http-server-port 6061 :http-server-host "localhost" :redis-uri "redis://redis/0" - :exporter-domain-whitelist #{"localhost:3449"}}) + :domain-white-list #{"localhost:3449"}}) (s/def ::http-server-port ::us/integer) (s/def ::http-server-host ::us/string) @@ -45,7 +45,7 @@ ::http-server-host ::browser-pool-max ::browser-pool-min - ::domain-whitelist])) + ::domain-white-list])) (defn- read-env [prefix] diff --git a/frontend/resources/polyfills/scrollIntoViewIfNeeded.js b/frontend/resources/polyfills/scrollIntoViewIfNeeded.js index a341b3e40..16763f504 100644 --- a/frontend/resources/polyfills/scrollIntoViewIfNeeded.js +++ b/frontend/resources/polyfills/scrollIntoViewIfNeeded.js @@ -3,8 +3,9 @@ Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) { centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; - var parent = this.parentNode, - parentComputedStyle = window.getComputedStyle(parent, null), + var parent = this.parentNode; + if (parent) { + var parentComputedStyle = window.getComputedStyle(parent, null), parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')), parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')), overTop = this.offsetTop - parent.offsetTop < parent.scrollTop, @@ -13,14 +14,15 @@ overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth), alignWithTop = overTop && !overBottom; - if ((overTop || overBottom) && centerIfNeeded) { - parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2; - } - if ((overLeft || overRight) && centerIfNeeded) { - parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2; - } - if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) { - this.scrollIntoView(alignWithTop); + if ((overTop || overBottom) && centerIfNeeded) { + parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2; + } + if ((overLeft || overRight) && centerIfNeeded) { + parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2; + } + if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) { + this.scrollIntoView(alignWithTop); + } } }; } diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index f396d8547..bdc2b6110 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -124,7 +124,7 @@ (let [edition (get-in state [:workspace-local :edition]) drawing (get state :workspace-drawing)] ;; Editors handle their own undo's - (when-not (or (some? edition) (and (not-empty drawing) (nil? (:object drawing)))) + (when (and (nil? edition) (nil? (:object drawing))) (let [undo (:workspace-undo state) items (:items undo) index (or (:index undo) (dec (count items)))] @@ -420,19 +420,26 @@ (reverse) (into (d/ordered-set))) - empty-parents-xform - (comp - (map (fn [id] (get objects id))) - (map (fn [{:keys [shapes type] :as obj}] - (when (and (= :group type) - (zero? (count (remove #(contains? ids %) shapes)))) - obj))) - (take-while some?) - (map :id)) + find-all-empty-parents (fn recursive-find-empty-parents [empty-parents] + (let [all-ids (into empty-parents ids) + empty-parents-xform + (comp + (map (fn [id] (get objects id))) + (map (fn [{:keys [shapes type] :as obj}] + (when (and (= :group type) + (zero? (count (remove #(contains? all-ids %) shapes)))) + obj))) + (take-while some?) + (map :id)) + calculated-empty-parents (into #{} empty-parents-xform all-parents)] + + (if (= empty-parents calculated-empty-parents) + empty-parents + (recursive-find-empty-parents calculated-empty-parents)))) empty-parents ;; Any parent whose children are all deleted, must be deleted too. - (into (d/ordered-set) empty-parents-xform all-parents) + (into (d/ordered-set) (find-all-empty-parents #{})) changes (-> (pcb/empty-changes it page-id) (pcb/with-page page) @@ -448,13 +455,13 @@ (pcb/update-shapes (map :id interacting-shapes) (fn [shape] (update shape :interactions - (fn [interactions] - (when interactions - (d/removev #(and (csi/has-destination %) - (contains? ids (:destination %))) - interactions)))))) + (fn [interactions] + (when interactions + (d/removev #(and (csi/has-destination %) + (contains? ids (:destination %))) + interactions)))))) (cond-> - (seq starting-flows) + (seq starting-flows) (pcb/update-page-option :flows (fn [flows] (reduce #(csp/remove-flow %1 (:id %2)) flows diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index e13c80f73..c87ed1299 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -189,10 +189,14 @@ (s/def ::file-change-event (s/keys :req-un [::type ::profile-id ::file-id ::session-id ::revn ::changes])) + (defn handle-file-change [{:keys [file-id changes] :as msg}] (us/assert ::file-change-event msg) (ptk/reify ::handle-file-change + IDeref + (-deref [_] {:changes changes}) + ptk/WatchEvent (watch [_ _ _] (let [position-data-operation? diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs index 9b4986922..e0f60d196 100644 --- a/frontend/src/app/main/data/workspace/state_helpers.cljs +++ b/frontend/src/app/main/data/workspace/state_helpers.cljs @@ -18,6 +18,10 @@ ([state page-id] (get-in state [:workspace-data :pages-index page-id]))) +(defn lookup-data-objects + [data page-id] + (dm/get-in data [:pages-index page-id :objects])) + (defn lookup-page-objects ([state] (lookup-page-objects state (:current-page-id state))) diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 40d9d14ee..0424e7390 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -355,7 +355,7 @@ (assoc :svg-attrs (dissoc attrs :x :y :width :height :href :xlink:href)))))) (defn parse-svg-element [frame-id svg-data element-data unames] - (let [{:keys [tag attrs]} element-data + (let [{:keys [tag attrs hidden]} element-data attrs (usvg/format-styles attrs) element-data (cond-> element-data (map? element-data) (assoc :attrs attrs)) name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag))) @@ -402,6 +402,9 @@ (setup-fill) (setup-stroke)) + shape (cond-> shape + hidden (assoc :hidden true)) + children (cond->> (:content element-data) (or (= tag :g) (= tag :svg)) (mapv #(usvg/inherit-attributes attrs %)))] @@ -471,6 +474,7 @@ :height (str (:height root-shape)) :fill "none" :id "base-background"} + :hidden true :content []} svg-data (-> svg-data diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 7766494a1..60e6a039c 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -10,6 +10,7 @@ [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.state-helpers :as wsh] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] @@ -31,7 +32,9 @@ [object-id] (rx/create (fn [subs] - (let [node (dom/query (dm/fmt "canvas.thumbnail-canvas[data-object-id='%'" object-id))] + ;; We look in the DOM a canvas that 1) matches the id and 2) that it's not empty + ;; will be empty on first rendering before drawing the thumbnail and we don't want to store that + (let [node (dom/query (dm/fmt "canvas.thumbnail-canvas[data-object-id='%']:not([data-empty])" object-id))] (if (some? node) (-> node (.toBlob (fn [blob] @@ -43,6 +46,14 @@ (do (rx/push! subs nil) (rx/end! subs))))))) +(defn clear-thumbnail + [page-id frame-id] + (ptk/reify ::clear-thumbnail + ptk/UpdateEvent + (update [_ state] + (let [object-id (dm/str page-id frame-id)] + (assoc-in state [:workspace-file :thumbnails object-id] nil))))) + (defn update-thumbnail "Updates the thumbnail information for the given frame `id`" [page-id frame-id] @@ -71,50 +82,39 @@ (defn- extract-frame-changes "Process a changes set in a commit to extract the frames that are changing" - [[event [old-objects new-objects]]] + [[event [old-data new-data]]] (let [changes (-> event deref :changes) extract-ids - (fn [{type :type :as change}] + (fn [{:keys [page-id type] :as change}] (case type - :add-obj [(:id change)] - :mod-obj [(:id change)] - :del-obj [(:id change)] - :reg-objects (:shapes change) - :mov-objects (:shapes change) + :add-obj [[page-id (:id change)]] + :mod-obj [[page-id (:id change)]] + :del-obj [[page-id (:id change)]] + :mov-objects (->> (:shapes change) (map #(vector page-id %))) [])) get-frame-id - (fn [id] - (let [shape (or (get new-objects id) - (get old-objects id))] - (or (and (cph/frame-shape? shape) id) (:frame-id shape)))) + (fn [[page-id id]] + (let [old-objects (wsh/lookup-data-objects old-data page-id) + new-objects (wsh/lookup-data-objects new-data page-id) - ;; Extracts the frames and then removes nils and the root frame - xform (comp (mapcat extract-ids) - (map get-frame-id) - (remove nil?) - (filter #(not= uuid/zero %)) - (filter #(contains? new-objects %)))] + new-shape (get new-objects id) + old-shape (get old-objects id) - (into #{} xform changes))) + old-frame-id (if (cph/frame-shape? old-shape) id (:frame-id old-shape)) + new-frame-id (if (cph/frame-shape? new-shape) id (:frame-id new-shape))] -(defn thumbnail-change? - "Checks if a event is only updating thumbnails to ignore in the thumbnail generation process" - [event] - (let [changes (-> event deref :changes) + (cond-> #{} + (and old-frame-id (not= uuid/zero old-frame-id)) + (conj [page-id old-frame-id]) - is-thumbnail-op? - (fn [{type :type attr :attr}] - (and (= type :set) - (= attr :thumbnail))) - - is-thumbnail-change? - (fn [change] - (and (= (:type change) :mod-obj) - (->> change :operations (every? is-thumbnail-op?))))] - - (->> changes (every? is-thumbnail-change?)))) + (and new-frame-id (not= uuid/zero new-frame-id)) + (conj [page-id new-frame-id]))))] + (into #{} + (comp (mapcat extract-ids) + (mapcat get-frame-id)) + changes))) (defn watch-state-changes "Watch the state for changes inside frames. If a change is detected will force a rendering @@ -123,32 +123,39 @@ (ptk/reify ::watch-state-changes ptk/WatchEvent (watch [_ _ stream] - (let [stopper (->> stream - (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) - (= ::watch-state-changes (ptk/type %))))) + (let [stopper + (->> stream + (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) + (= ::watch-state-changes (ptk/type %))))) - objects-stream (->> (rx/concat - (rx/of nil) - (rx/from-atom refs/workspace-page-objects {:emit-current-value? true})) - ;; We need to keep the old-objects so we can check the frame for the - ;; deleted objects - (rx/buffer 2 1)) + workspace-data-str + (->> (rx/concat + (rx/of nil) + (rx/from-atom refs/workspace-data {:emit-current-value? true})) + ;; We need to keep the old-objects so we can check the frame for the + ;; deleted objects + (rx/buffer 2 1)) - frame-changes (->> stream - (rx/filter dch/commit-changes?) + change-str + (->> stream + (rx/filter #(or (dch/commit-changes? %) + (= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change))) + (rx/observe-on :async)) - ;; Async so we wait for additional side-effects of commit-changes - (rx/observe-on :async) - (rx/filter (complement thumbnail-change?)) - (rx/with-latest-from objects-stream) - (rx/map extract-frame-changes) - (rx/share))] + frame-changes-str + (->> change-str + (rx/with-latest-from workspace-data-str) + (rx/flat-map extract-frame-changes) + (rx/share))] - (->> frame-changes - (rx/flat-map - (fn [ids] - (->> (rx/from ids) - (rx/map #(ptk/data-event ::force-render %))))) + (->> (rx/merge + (->> frame-changes-str + (rx/filter (fn [[page-id _]] (not= page-id (:current-page-id @st/state)))) + (rx/map (fn [[page-id frame-id]] (clear-thumbnail page-id frame-id)))) + + (->> frame-changes-str + (rx/filter (fn [[page-id _]] (= page-id (:current-page-id @st/state)))) + (rx/map (fn [[_ frame-id]] (ptk/data-event ::force-render frame-id))))) (rx/take-until stopper)))))) (defn duplicate-thumbnail diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 99a7e11dc..4e28186b7 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -324,7 +324,8 @@ props (obj/merge! #js {} props #js {:childs childs :objects objects})] - [:> group-wrapper props])))) + (when (not-empty childs) + [:> group-wrapper props]))))) (defn bool-container-factory [objects] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index 70adcdfb6..ae25c9177 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -82,6 +82,7 @@ frame? (= :frame type) group? (= :group type) + text? (= :text type) mask? (and group? masked-group?)] (cond @@ -103,6 +104,10 @@ (dom/query-all shape-defs ".svg-def") (dom/query-all shape-defs ".svg-mask-wrapper"))) + text? + [shape-node + (dom/query shape-node ".text-container")] + :else [shape-node]))) @@ -185,6 +190,15 @@ (dom/class? node "frame-children") (set-transform-att! node "transform" (gmt/inverse transform)) + (dom/class? node "text-container") + (let [modifiers (dissoc modifiers :displacement :rotation)] + (when (not (gsh/empty-modifiers? modifiers)) + (let [mtx (-> shape + (assoc :modifiers modifiers) + (gsh/transform-shape) + (gsh/transform-matrix {:no-flip true}))] + (override-transform-att! node "transform" mtx)))) + (or (= (dom/get-tag-name node) "mask") (= (dom/get-tag-name node) "filter")) (transform-region! node modifiers) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs index 1e763a462..c61ca1d84 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -32,6 +32,7 @@ (.clearRect canvas-context 0 0 canvas-width canvas-height) (.drawImage canvas-context img-node 0 0 canvas-width canvas-height) + (.removeAttribute canvas-node "data-empty") true)) (catch :default err (.error js/console err) @@ -75,6 +76,8 @@ thumbnail-data-ref (mf/use-memo (mf/deps page-id id) #(refs/thumbnail-frame-data page-id id)) thumbnail-data (mf/deref thumbnail-data-ref) + prev-thumbnail-data (hooks/use-previous thumbnail-data) + render-frame? (mf/use-state (not thumbnail-data)) on-image-load @@ -141,6 +144,12 @@ (.observe observer node #js {:childList true :attributes true :attributeOldValue true :characterData true :subtree true}) (reset! observer-ref observer)))))] + (mf/use-effect + (mf/deps thumbnail-data) + (fn [] + (when (and (some? prev-thumbnail-data) (nil? thumbnail-data)) + (rx/push! updates-str :update)))) + (mf/use-effect (mf/deps @render-frame? thumbnail-data) (fn [] @@ -198,8 +207,10 @@ [:foreignObject {:x x :y y :width width :height height} [:canvas.thumbnail-canvas - {:ref frame-canvas-ref + {:key (dm/str "thumbnail-canvas-" (:id shape)) + :ref frame-canvas-ref :data-object-id (dm/str page-id (:id shape)) + :data-empty true :width fixed-width :height fixed-height ;; DEBUG diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index ff2fd0bfc..372cbda74 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -7,9 +7,10 @@ (ns app.main.ui.workspace.shapes.text.editor (:require ["draft-js" :as draft] - [app.common.geom.matrix :as gmt] + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.text :as gsht] [app.common.text :as txt] [app.main.data.workspace :as dw] [app.main.data.workspace.texts :as dwt] @@ -255,30 +256,37 @@ (-> (gpt/subtract pt box) (gpt/multiply zoom))))) -(mf/defc text-editor-viewport +(mf/defc text-editor-svg {::mf/wrap-props false} [props] (let [shape (obj/get props "shape") - viewport-ref (obj/get props "viewport-ref") - zoom (obj/get props "zoom") - position - (-> (gpt/point (-> shape :selrect :x) - (-> shape :selrect :y)) - (translate-point-from-viewport (mf/ref-val viewport-ref) zoom)) + clip-id + (dm/str "text-edition-clip" (:id shape)) - top-left-corner (gpt/point (/ (:width shape) 2) (/ (:height shape) 2)) + text-modifier-ref + (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) - transform - (-> (gmt/matrix) - (gmt/scale (gpt/point zoom)) - (gmt/multiply (gsh/transform-matrix shape nil top-left-corner)))] + text-modifier + (mf/deref text-modifier-ref) - [:div {:style {:position "absolute" - :left (str (:x position) "px") - :top (str (:y position) "px") - :pointer-events "all" - :transform (str transform) - :transform-origin "left top"}} + bounding-box + (gsht/position-data-bounding-box text-modifier)] - [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]])) + [:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id) + :transform (dm/str (gsh/transform-matrix shape))} + [:defs + [:clipPath {:id clip-id} + [:rect {:x (min (:x bounding-box) (:x shape)) + :y (min (:y bounding-box) (:y shape)) + :width (max (:width bounding-box) (:width shape)) + :height (max (:height bounding-box) (:height shape)) + :fill "red"}]]] + + [:foreignObject {:x (:x shape) :y (:y shape) :width "100%" :height "100%" + :externalResourcesRequired true} + [:div {:style {:position "absolute" + :left 0 + :top 0 + :pointer-events "all"}} + [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index cd673a628..88ee7e26b 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -187,10 +187,7 @@ [:div.viewport [:div.viewport-overlays {:ref overlays-ref} - (when show-text-editor? - [:& editor/text-editor-viewport {:shape editing-shape - :viewport-ref viewport-ref - :zoom zoom}]) + (when show-comments? [:& comments/comments-layer {:vbox vbox :vport vport @@ -275,6 +272,9 @@ :on-pointer-up on-pointer-up} [:g {:style {:pointer-events (if disable-events? "none" "auto")}} + (when show-text-editor? + [:& editor/text-editor-svg {:shape editing-shape}]) + (when show-outlines? [:& outline/shape-outlines {:objects base-objects