From 9d827d4b3017e502108994f64dcc56d8769c9075 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 May 2020 18:58:36 +0200 Subject: [PATCH] :recycle: Refactor shapes structure. --- frontend/src/uxbox/main/exports.cljs | 87 ++--- frontend/src/uxbox/main/ui/shapes.cljs | 20 -- frontend/src/uxbox/main/ui/shapes/attrs.cljs | 14 +- frontend/src/uxbox/main/ui/shapes/circle.cljs | 64 +--- .../uxbox/main/ui/shapes/custom_stroke.cljs | 87 ++--- frontend/src/uxbox/main/ui/shapes/frame.cljs | 119 +------ frontend/src/uxbox/main/ui/shapes/group.cljs | 67 +--- frontend/src/uxbox/main/ui/shapes/icon.cljs | 70 +--- frontend/src/uxbox/main/ui/shapes/image.cljs | 65 +--- frontend/src/uxbox/main/ui/shapes/path.cljs | 73 +--- frontend/src/uxbox/main/ui/shapes/rect.cljs | 64 +--- frontend/src/uxbox/main/ui/shapes/shape.cljs | 58 +--- frontend/src/uxbox/main/ui/shapes/text.cljs | 256 +------------- frontend/src/uxbox/main/ui/viewer.cljs | 8 +- .../uxbox/main/ui/viewer/frame_viewer.cljs | 104 ------ frontend/src/uxbox/main/ui/viewer/shapes.cljs | 149 +++++++++ .../src/uxbox/main/ui/workspace/drawarea.cljs | 2 +- .../src/uxbox/main/ui/workspace/shapes.cljs | 77 +++++ .../shapes/bbox.cljs} | 2 +- .../ui/{ => workspace}/shapes/common.cljs | 58 ++-- .../uxbox/main/ui/workspace/shapes/frame.cljs | 109 ++++++ .../uxbox/main/ui/workspace/shapes/group.cljs | 74 +++++ .../uxbox/main/ui/workspace/shapes/path.cljs | 51 +++ .../uxbox/main/ui/workspace/shapes/text.cljs | 311 ++++++++++++++++++ .../src/uxbox/main/ui/workspace/viewport.cljs | 2 +- frontend/src/uxbox/worker/thumbnails.cljs | 34 ++ 26 files changed, 983 insertions(+), 1042 deletions(-) delete mode 100644 frontend/src/uxbox/main/ui/shapes.cljs delete mode 100644 frontend/src/uxbox/main/ui/viewer/frame_viewer.cljs create mode 100644 frontend/src/uxbox/main/ui/viewer/shapes.cljs create mode 100644 frontend/src/uxbox/main/ui/workspace/shapes.cljs rename frontend/src/uxbox/main/ui/{shapes/bounding_box.cljs => workspace/shapes/bbox.cljs} (98%) rename frontend/src/uxbox/main/ui/{ => workspace}/shapes/common.cljs (59%) create mode 100644 frontend/src/uxbox/main/ui/workspace/shapes/frame.cljs create mode 100644 frontend/src/uxbox/main/ui/workspace/shapes/group.cljs create mode 100644 frontend/src/uxbox/main/ui/workspace/shapes/path.cljs create mode 100644 frontend/src/uxbox/main/ui/workspace/shapes/text.cljs create mode 100644 frontend/src/uxbox/worker/thumbnails.cljs diff --git a/frontend/src/uxbox/main/exports.cljs b/frontend/src/uxbox/main/exports.cljs index a0dc5a6f10..85b3f67041 100644 --- a/frontend/src/uxbox/main/exports.cljs +++ b/frontend/src/uxbox/main/exports.cljs @@ -24,6 +24,7 @@ [uxbox.main.ui.shapes.group :as group])) (def ^:private background-color "#E8E9EA") ;; $color-canvas + (mf/defc background [] [:rect @@ -41,40 +42,34 @@ {:width (if (mth/nan? width) 100 width) :height (if (mth/nan? height) 100 height)})) -(declare shape-wrapper) +(declare shape-wrapper-factory) -(defn frame-wrapper +(defn frame-wrapper-factory [objects] - (mf/fnc frame-wrapper - [{:keys [shape] :as props}] - (let [childs (mapv #(get objects %) - (:shapes shape)) - shape-wrapper (mf/use-memo (mf/deps objects) - #(shape-wrapper objects)) - frame-shape (mf/use-memo (mf/deps objects) - #(frame/frame-shape shape-wrapper)) - shape (geom/transform-shape shape)] - [:& frame-shape {:shape shape :childs childs}]))) + (let [shape-wrapper (shape-wrapper-factory objects) + frame-shape (frame/frame-shape shape-wrapper)] + (mf/fnc frame-wrapper + [{:keys [shape] :as props}] + (let [childs (mapv #(get objects %) (:shapes shape)) + shape (geom/transform-shape shape)] + [:& frame-shape {:shape shape :childs childs}])))) -(defn group-wrapper +(defn group-wrapper-factory [objects] - (mf/fnc group-wrapper - [{:keys [shape frame] :as props}] - (let [children (mapv #(get objects %) - (:shapes shape)) - shape-wrapper (mf/use-memo (mf/deps objects) - #(shape-wrapper objects)) - group-shape (mf/use-memo (mf/deps objects) - #(group/group-shape shape-wrapper))] - [:& group-shape {:frame frame - :shape shape - :children children}]))) + (let [shape-wrapper (shape-wrapper-factory objects) + group-shape (group/group-shape shape-wrapper)] + (mf/fnc group-wrapper + [{:keys [shape frame] :as props}] + (let [children (mapv #(get objects %) (:shapes shape))] + [:& group-shape {:frame frame + :shape shape + :children children}])))) -(defn shape-wrapper +(defn shape-wrapper-factory [objects] (mf/fnc shape-wrapper [{:keys [frame shape] :as props}] - (let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper objects))] + (let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects))] (when (and shape (not (:hidden shape))) (let [shape (geom/transform-shape frame shape) opts #js {:shape shape}] @@ -93,13 +88,24 @@ {::mf/wrap [mf/memo]} [{:keys [data] :as props}] (let [objects (:objects data) + dim (calculate-dimensions data) root (get objects uuid/zero) shapes (->> (:shapes root) (map #(get objects %))) - dim (calculate-dimensions data) - frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper objects)) - shape-wrapper (mf/use-memo (mf/deps objects) #(shape-wrapper objects))] - [:svg {:view-box (str "0 0 " (:width dim 0) " " (:height dim 0)) + + vbox (str "0 0 " (:width dim 0) " " (:height dim 0)) + + frame-wrapper + (mf/use-memo + (mf/deps objects) + #(frame-wrapper-factory objects)) + + shape-wrapper + (mf/use-memo + (mf/deps objects) + #(shape-wrapper-factory objects))] + + [:svg {:view-box vbox :version "1.1" :xmlnsXlink "http://www.w3.org/1999/xlink" :xmlns "http://www.w3.org/2000/svg"} @@ -119,19 +125,19 @@ (gmt/translate-matrix)) frame-id (:id frame) + modifier-ids (concat [frame-id] (cp/get-children frame-id objects)) - - update-fn (fn [state shape-id] - (-> state - (assoc-in [shape-id :modifiers :displacement] modifier))) + update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) objects (reduce update-fn objects modifier-ids) - frame (assoc-in frame [:modifiers :displacement] modifier ) + frame (assoc-in frame [:modifiers :displacement] modifier) - width (* (:width frame) zoom) + width (* (:width frame) zoom) height (* (:height frame) zoom) - vbox (str "0 0 " (:width frame 0) " " (:height frame 0)) - frame-wrapper (mf/use-memo (mf/deps objects) - #(frame-wrapper objects))] + vbox (str "0 0 " (:width frame 0) + " " (:height frame 0)) + wrapper (mf/use-memo + (mf/deps objects) + #(frame-wrapper-factory objects))] [:svg {:view-box vbox :width width @@ -139,6 +145,5 @@ :version "1.1" :xmlnsXlink "http://www.w3.org/1999/xlink" :xmlns "http://www.w3.org/2000/svg"} - [:& frame-wrapper {:shape frame - :view-box vbox}]])) + [:& wrapper {:shape frame :view-box vbox}]])) diff --git a/frontend/src/uxbox/main/ui/shapes.cljs b/frontend/src/uxbox/main/ui/shapes.cljs deleted file mode 100644 index 5e3e51ebe8..0000000000 --- a/frontend/src/uxbox/main/ui/shapes.cljs +++ /dev/null @@ -1,20 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2016-2020 Andrey Antukh - -(ns uxbox.main.ui.shapes - (:require - [lentes.core :as l] - [rumext.alpha :as mf] - [uxbox.main.refs :as refs] - [uxbox.main.store :as st] - [uxbox.main.ui.shapes.frame :as frame] - [uxbox.main.ui.shapes.shape :as shape])) - -(def shape-wrapper shape/shape-wrapper) -(def frame-wrapper shape/frame-wrapper) diff --git a/frontend/src/uxbox/main/ui/shapes/attrs.cljs b/frontend/src/uxbox/main/ui/shapes/attrs.cljs index dc276c0f2e..62b5f4bc99 100644 --- a/frontend/src/uxbox/main/ui/shapes/attrs.cljs +++ b/frontend/src/uxbox/main/ui/shapes/attrs.cljs @@ -8,9 +8,7 @@ ;; Copyright (c) 2016-2020 UXBOX Labs SL (ns uxbox.main.ui.shapes.attrs - (:require - [cuerdas.core :as str] - [uxbox.util.interop :as itr])) + (:require [uxbox.util.object :as obj])) (defn- stroke-type->dasharray [style] @@ -28,9 +26,9 @@ :rx (:rx shape nil) :ry (:ry shape nil)}] (when (not= stroke-style :none) - (itr/obj-assign! attrs - #js {:stroke (:stroke-color shape nil) - :strokeWidth (:stroke-width shape nil) - :strokeOpacity (:stroke-opacity shape nil) - :strokeDasharray (stroke-type->dasharray stroke-style)})) + (obj/merge! attrs + #js {:stroke (:stroke-color shape nil) + :strokeWidth (:stroke-width shape nil) + :strokeOpacity (:stroke-opacity shape nil) + :strokeDasharray (stroke-type->dasharray stroke-style)})) attrs)) diff --git a/frontend/src/uxbox/main/ui/shapes/circle.cljs b/frontend/src/uxbox/main/ui/shapes/circle.cljs index a183d5147a..b9144498e9 100644 --- a/frontend/src/uxbox/main/ui/shapes/circle.cljs +++ b/frontend/src/uxbox/main/ui/shapes/circle.cljs @@ -2,65 +2,18 @@ ;; 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) 2016-2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL (ns uxbox.main.ui.shapes.circle (:require [rumext.alpha :as mf] - [uxbox.util.geom.shapes :as geom] - [uxbox.main.refs :as refs] [uxbox.main.ui.shapes.attrs :as attrs] - [uxbox.main.ui.shapes.common :as common] - [uxbox.util.interop :as itr] - [uxbox.util.geom.matrix :as gmt] - [uxbox.util.geom.point :as gpt] - [uxbox.main.ui.shapes.bounding-box :refer [bounding-box]] - [uxbox.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]])) - -;; --- Circle Wrapper for workspace - -(declare circle-shape) - -(mf/defc circle-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - selected (mf/deref refs/selected-shapes) - selected? (contains? selected (:id shape)) - on-mouse-down #(common/on-mouse-down % shape) - on-context-menu #(common/on-context-menu % shape)] - [:g.shape {:class (when selected? "selected") - :on-mouse-down on-mouse-down - :on-context-menu on-context-menu} - [:& circle-shape {:shape shape}]])) - -;; --- Circle Wrapper for viewer - -(mf/defc circle-viewer-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - {:keys [x y width height]} (geom/selection-rect-shape shape) - show-interactions? (mf/deref refs/show-interactions?) - on-mouse-down (mf/use-callback - (mf/deps shape) - #(common/on-mouse-down-viewer % shape))] - - [:g.shape {:on-mouse-down on-mouse-down - :cursor (when (:interactions shape) "pointer")} - [:* - [:& circle-shape {:shape shape}] - (when (and (:interactions shape) show-interactions?) - [:> "rect" #js {:x (- x 1) - :y (- y 1) - :width (+ width 2) - :height (+ height 2) - :fill "#31EFB8" - :stroke "#31EFB8" - :strokeWidth 1 - :fillOpacity 0.2}])]])) - -;; --- Circle Shape + [uxbox.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] + [uxbox.util.geom.shapes :as geom] + [uxbox.util.object :as obj])) (mf/defc circle-shape {::mf/wrap-props false} @@ -75,13 +28,14 @@ ry (/ height 2) props (-> (attrs/extract-style-attrs shape) - (itr/obj-assign! + (obj/merge! #js {:cx cx :cy cy :rx rx :ry ry :transform transform :id (str "shape-" id)}))] + [:& shape-custom-stroke {:shape shape :base-props props :elem-name "ellipse"}])) diff --git a/frontend/src/uxbox/main/ui/shapes/custom_stroke.cljs b/frontend/src/uxbox/main/ui/shapes/custom_stroke.cljs index f1f91282bd..3b8755b921 100644 --- a/frontend/src/uxbox/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/uxbox/main/ui/shapes/custom_stroke.cljs @@ -8,11 +8,12 @@ (:require [rumext.alpha :as mf] [uxbox.util.geom.shapes :as geom] - [uxbox.util.interop :as itr])) + [uxbox.util.object :as obj])) -; The SVG standard does not implement yet the 'stroke-alignment' attribute, to define the position -; of the stroke relative to the stroke axis (inner, center, outer). Here we implement a patch -; to be able to draw the stroke in the three cases. See discussion at: +; The SVG standard does not implement yet the 'stroke-alignment' +; attribute, to define the position of the stroke relative to the +; stroke axis (inner, center, outer). Here we implement a patch to be +; able to draw the stroke in the three cases. See discussion at: ; https://stackoverflow.com/questions/7241393/can-you-control-how-an-svgs-stroke-width-is-drawn (mf/defc shape-custom-stroke {::mf/wrap-props false} @@ -25,66 +26,68 @@ stroke-position (:stroke-alignment shape :center)] (cond - ; Center alignment (or no stroke): the default in SVG + ;; Center alignment (or no stroke): the default in SVG (or (= stroke-style :none) (= stroke-position :center)) [:> elem-name base-props] - ; Inner alignment: display the shape with double width stroke, and clip the result - ; with the original shape without stroke. + ;; Inner alignment: display the shape with double width stroke, + ;; and clip the result with the original shape without stroke. (= stroke-position :inner) (let [clip-id (str "clip-" id) - clip-props (-> (itr/obj-assign! #js {} base-props) - (itr/obj-assign! #js {:stroke nil - :strokeWidth nil - :strokeOpacity nil - :strokeDasharray nil - :fill "white" - :fillOpacity 1})) + clip-props (-> (obj/merge! #js {} base-props) + (obj/merge! #js {:stroke nil + :strokeWidth nil + :strokeOpacity nil + :strokeDasharray nil + :fill "white" + :fillOpacity 1})) stroke-width (.-strokeWidth base-props) - shape-props (-> (itr/obj-assign! #js {} base-props) - (itr/obj-assign! #js {:strokeWidth (* stroke-width 2) - :clipPath (str "url('#" clip-id "')")}))] + shape-props (-> (obj/merge! #js {} base-props) + (obj/merge! #js {:strokeWidth (* stroke-width 2) + :clipPath (str "url('#" clip-id "')")}))] [:* [:> "clipPath" #js {:id clip-id} [:> elem-name clip-props]] [:> elem-name shape-props]]) - ; Outer alingmnent: display the shape in two layers. One without stroke (only fill), - ; and another one only with stroke at double width (transparent fill) and passed - ; through a mask that shows the whole shape, but hides the original shape without stroke + ;; Outer alingmnent: display the shape in two layers. One + ;; without stroke (only fill), and another one only with stroke + ;; at double width (transparent fill) and passed through a mask + ;; that shows the whole shape, but hides the original shape + ;; without stroke + (= stroke-position :outer) (let [mask-id (str "mask-" id) - - stroke-width (.-strokeWidth base-props) - mask-props1 (-> (itr/obj-assign! #js {} base-props) - (itr/obj-assign! #js {:stroke "white" + stroke-width (.-strokeWidth ^js base-props) + mask-props1 (-> (obj/merge! #js {} base-props) + (obj/merge! #js {:stroke "white" :strokeWidth (* stroke-width 2) :strokeOpacity 1 :strokeDasharray nil :fill "white" :fillOpacity 1})) - mask-props2 (-> (itr/obj-assign! #js {} base-props) - (itr/obj-assign! #js {:stroke nil - :strokeWidth nil - :strokeOpacity nil - :strokeDasharray nil - :fill "black" - :fillOpacity 1})) + mask-props2 (-> (obj/merge! #js {} base-props) + (obj/merge! #js {:stroke nil + :strokeWidth nil + :strokeOpacity nil + :strokeDasharray nil + :fill "black" + :fillOpacity 1})) - shape-props1 (-> (itr/obj-assign! #js {} base-props) - (itr/obj-assign! #js {:stroke nil - :strokeWidth nil - :strokeOpacity nil - :strokeDasharray nil})) - shape-props2 (-> (itr/obj-assign! #js {} base-props) - (itr/obj-assign! #js {:strokeWidth (* stroke-width 2) - :fill "none" - :fillOpacity 0 - :mask (str "url('#" mask-id "')")}))] + shape-props1 (-> (obj/merge! #js {} base-props) + (obj/merge! #js {:stroke nil + :strokeWidth nil + :strokeOpacity nil + :strokeDasharray nil})) + shape-props2 (-> (obj/merge! #js {} base-props) + (obj/merge! #js {:strokeWidth (* stroke-width 2) + :fill "none" + :fillOpacity 0 + :mask (str "url('#" mask-id "')")}))] [:* - [:> "mask" #js {:id mask-id} + [:mask {:id mask-id} [:> elem-name mask-props1] [:> elem-name mask-props2]] [:> elem-name shape-props1] diff --git a/frontend/src/uxbox/main/ui/shapes/frame.cljs b/frontend/src/uxbox/main/ui/shapes/frame.cljs index ba69a57da9..2ec407dd16 100644 --- a/frontend/src/uxbox/main/ui/shapes/frame.cljs +++ b/frontend/src/uxbox/main/ui/shapes/frame.cljs @@ -9,127 +9,14 @@ (ns uxbox.main.ui.shapes.frame (:require - [lentes.core :as l] [rumext.alpha :as mf] [uxbox.common.data :as d] - [uxbox.main.constants :as c] - [uxbox.main.data.workspace :as dw] - [uxbox.util.geom.shapes :as geom] - [uxbox.main.refs :as refs] - [uxbox.main.store :as st] [uxbox.main.ui.shapes.attrs :as attrs] - [uxbox.main.ui.shapes.common :as common] - [uxbox.util.dom :as dom] - [uxbox.util.timers :as ts] - [uxbox.util.interop :as itr] - [uxbox.util.geom.matrix :as gmt] - [uxbox.util.geom.point :as gpt])) - -(declare frame-wrapper) + [uxbox.util.geom.shapes :as geom] + [uxbox.util.object :as obj])) (def frame-default-props {:fill-color "#ffffff"}) -(declare frame-shape) -(declare translate-to-frame) - -;; ---- Frame Wrapper for workspace - -(defn frame-wrapper-memo-equals? - [np op] - (let [n-shape (aget np "shape") - o-shape (aget op "shape") - n-objs (aget np "objects") - o-objs (aget op "objects") - - ids (:shapes n-shape)] - (and (identical? n-shape o-shape) - (loop [id (first ids) - ids (rest ids)] - (if (nil? id) - true - (if (identical? (get n-objs id) - (get o-objs id)) - (recur (first ids) (rest ids)) - false)))))) - -(defn frame-wrapper - [shape-wrapper] - (let [frame-shape (frame-shape shape-wrapper)] - (mf/fnc frame-wrapper - {::mf/wrap [#(mf/memo' % frame-wrapper-memo-equals?) - #(mf/deferred % ts/schedule-on-idle)] - ::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - objects (unchecked-get props "objects") - - selected-iref (mf/use-memo (mf/deps (:id shape)) - #(refs/make-selected (:id shape))) - selected? (mf/deref selected-iref) - zoom (mf/deref refs/selected-zoom) - - - on-mouse-down (mf/use-callback (mf/deps shape) - #(common/on-mouse-down % shape)) - on-context-menu (mf/use-callback (mf/deps shape) - #(common/on-context-menu % shape)) - - - {:keys [x y width height]} shape - - inv-zoom (/ 1 zoom) - childs (mapv #(get objects %) (:shapes shape)) - ds-modifier (get-in shape [:modifiers :displacement]) - - label-pos (cond-> (gpt/point x (- y 10)) - (gmt/matrix? ds-modifier) (gpt/transform ds-modifier)) - - on-double-click - (mf/use-callback - (mf/deps (:id shape)) - (fn [event] - (dom/prevent-default event) - (st/emit! dw/deselect-all - (dw/select-shape (:id shape)))))] - - (when-not (:hidden shape) - [:g {:class (when selected? "selected") - :on-context-menu on-context-menu - :on-double-click on-double-click - :on-mouse-down on-mouse-down} - [:text {:x 0 - :y 0 - :width width - :height 20 - :class "workspace-frame-label" - ;; Ensure that the label has always the same font - ;; size, regardless of zoom - ;; https://css-tricks.com/transforms-on-svg-elements/ - :transform (str - "scale(" inv-zoom ", " inv-zoom ") " - "translate(" (* zoom (:x label-pos)) ", " - (* zoom (:y label-pos)) ")") - ;; User may also select the frame with single click in the label - :on-click on-double-click} - (:name shape)] - [:& frame-shape - {:shape (geom/transform-shape shape) - :childs childs}]]))))) - -;; ;; --- Frame Wrapper for viewer -;; -;; (mf/defc frame-viewer-wrapper -;; {::mf/wrap-props false} -;; [props] -;; (let [shape (unchecked-get props "shape") -;; on-mouse-down (mf/use-callback -;; (mf/deps shape) -;; #(common/on-mouse-down-viewer % shape))] -;; [:g.shape {:on-mouse-down on-mouse-down} -;; [:& rect-shape {:shape shape}]])) - -;; ---- Frame shape - (defn frame-shape [shape-wrapper] (mf/fnc frame-shape @@ -141,7 +28,7 @@ props (-> (merge frame-default-props shape) (attrs/extract-style-attrs) - (itr/obj-assign! + (obj/merge! #js {:x 0 :y 0 :id (str "shape-" id) diff --git a/frontend/src/uxbox/main/ui/shapes/group.cljs b/frontend/src/uxbox/main/ui/shapes/group.cljs index 28feee6e50..590a141e79 100644 --- a/frontend/src/uxbox/main/ui/shapes/group.cljs +++ b/frontend/src/uxbox/main/ui/shapes/group.cljs @@ -9,73 +9,10 @@ (ns uxbox.main.ui.shapes.group (:require - [cuerdas.core :as str] [rumext.alpha :as mf] - [uxbox.util.geom.shapes :as geom] - [uxbox.main.refs :as refs] - [uxbox.util.dom :as dom] - [uxbox.util.interop :as itr] - [uxbox.util.debug :refer [debug?]] - [uxbox.main.ui.shapes.common :as common] [uxbox.main.ui.shapes.attrs :as attrs] - [uxbox.main.data.workspace :as dw] - [uxbox.main.store :as st] - [uxbox.main.streams :as ms] - [uxbox.main.ui.shapes.bounding-box :refer [bounding-box]])) - -(defn- equals? - [np op] - (let [n-shape (unchecked-get np "shape") - o-shape (unchecked-get op "shape") - n-frame (unchecked-get np "frame") - o-frame (unchecked-get op "frame")] - (and (= n-frame o-frame) - (= n-shape o-shape)))) - -(declare translate-to-frame) -(declare group-shape) - -(defn group-wrapper - [shape-wrapper] - (let [group-shape (group-shape shape-wrapper)] - (mf/fnc group-wrapper - {::mf/wrap [#(mf/memo' % equals?)] - ::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - frame (unchecked-get props "frame") - on-mouse-down (mf/use-callback (mf/deps shape) - #(common/on-mouse-down % shape)) - on-context-menu (mf/use-callback (mf/deps shape) - #(common/on-context-menu % shape)) - - children-ref (mf/use-memo (mf/deps shape) - #(refs/objects-by-id (:shapes shape))) - children (mf/deref children-ref) - - - is-child-selected-ref (mf/use-memo (mf/deps (:id shape)) - #(refs/is-child-selected? (:id shape))) - is-child-selected? (mf/deref is-child-selected-ref) - - on-double-click - (mf/use-callback - (mf/deps (:id shape)) - (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (dw/select-inside-group (:id shape) @ms/mouse-position))))] - - [:g.shape - {:on-mouse-down on-mouse-down - :on-context-menu on-context-menu - :on-double-click on-double-click} - - [:& group-shape - {:frame frame - :shape shape - :children children - :is-child-selected? is-child-selected?}]])))) + [uxbox.util.debug :refer [debug?]] + [uxbox.util.geom.shapes :as geom])) (defn group-shape [shape-wrapper] diff --git a/frontend/src/uxbox/main/ui/shapes/icon.cljs b/frontend/src/uxbox/main/ui/shapes/icon.cljs index ee540744e2..a2fb31b0be 100644 --- a/frontend/src/uxbox/main/ui/shapes/icon.cljs +++ b/frontend/src/uxbox/main/ui/shapes/icon.cljs @@ -2,93 +2,41 @@ ;; 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) 2016 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL (ns uxbox.main.ui.shapes.icon (:require [rumext.alpha :as mf] - [cuerdas.core :as str] [uxbox.util.geom.shapes :as geom] - [uxbox.main.refs :as refs] [uxbox.main.ui.shapes.attrs :as attrs] - [uxbox.main.ui.shapes.common :as common] - [uxbox.util.interop :as itr])) - -;; --- Icon Wrapper for workspace - -(declare icon-shape) - -(mf/defc icon-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - frame (unchecked-get props "frame") - selected (mf/deref refs/selected-shapes) - selected? (contains? selected (:id shape)) - on-mouse-down #(common/on-mouse-down % shape)] - [:g.shape {:class (when selected? "selected") - :on-mouse-down on-mouse-down} - [:& icon-shape {:shape (geom/transform-shape frame shape)}]])) - -;; --- Icon Wrapper for viewer - -(mf/defc icon-viewer-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - frame (unchecked-get props "frame") - {:keys [x y width height]} (geom/selection-rect-shape shape) - show-interactions? (mf/deref refs/show-interactions?) - on-mouse-down (mf/use-callback - (mf/deps shape) - #(common/on-mouse-down-viewer % shape))] - [:g.shape {:on-mouse-down on-mouse-down - :cursor (when (:interactions shape) "pointer")} - [:* - [:& icon-shape {:shape (geom/transform-shape frame shape)}] - (when (and (:interactions shape) show-interactions?) - [:> "rect" #js {:x (- x 1) - :y (- y 1) - :width (+ width 2) - :height (+ height 2) - :fill "#31EFB8" - :stroke "#31EFB8" - :strokeWidth 1 - :fillOpacity 0.2}])]])) - -;; --- Icon Shape + [uxbox.util.object :as obj])) (mf/defc icon-shape {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") {:keys [id x y width height metadata rotation content]} shape - transform (when (and rotation (pos? rotation)) - (str/format "rotate(%s %s %s)" - rotation - (+ x (/ width 2)) - (+ y (/ height 2)))) - view-box (apply str (interpose " " (:view-box metadata))) + transform (geom/transform-matrix shape) + vbox (apply str (interpose " " (:view-box metadata))) props (-> (attrs/extract-style-attrs shape) - (itr/obj-assign! + (obj/merge! #js {:x x :y y :transform transform :id (str "shape-" id) :width width :height height - :viewBox view-box + :viewBox vbox :preserveAspectRatio "none" :dangerouslySetInnerHTML #js {:__html content}}))] - - [:g {:transform transform} [:> "svg" props]])) -;; --- Icon SVG - (mf/defc icon-svg [{:keys [shape] :as props}] (let [{:keys [content id metadata]} shape diff --git a/frontend/src/uxbox/main/ui/shapes/image.cljs b/frontend/src/uxbox/main/ui/shapes/image.cljs index 91faa4ddd2..de39928ec9 100644 --- a/frontend/src/uxbox/main/ui/shapes/image.cljs +++ b/frontend/src/uxbox/main/ui/shapes/image.cljs @@ -2,70 +2,17 @@ ;; 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) 2016-2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL (ns uxbox.main.ui.shapes.image (:require [rumext.alpha :as mf] - [cuerdas.core :as str] - [uxbox.main.data.images :as udi] [uxbox.util.geom.shapes :as geom] - [uxbox.main.refs :as refs] - [uxbox.main.store :as st] [uxbox.main.ui.shapes.attrs :as attrs] - [uxbox.main.ui.shapes.common :as common] - [uxbox.util.interop :as itr] - [uxbox.util.geom.matrix :as gmt])) - -;; --- Image Wrapper for workspace - -(declare image-shape) - -(mf/defc image-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - selected (mf/deref refs/selected-shapes) - selected? (contains? selected (:id shape)) - on-mouse-down (mf/use-callback - (mf/deps shape) - #(common/on-mouse-down % shape)) - - on-context-menu (mf/use-callback - (mf/deps shape) - #(common/on-context-menu % shape))] - - [:g.shape {:class (when selected? "selected") - :on-mouse-down on-mouse-down - :on-context-menu on-context-menu} - [:& image-shape {:shape shape}]])) - -;; --- Image Wrapper for viewer - -(mf/defc image-viewer-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - {:keys [x y width height]} (geom/selection-rect-shape shape) - show-interactions? (mf/deref refs/show-interactions?) - on-mouse-down (mf/use-callback - (mf/deps shape) - #(common/on-mouse-down-viewer % shape))] - [:g.shape {:on-mouse-down on-mouse-down - :cursor (when (:interactions shape) "pointer")} - [:* - [:& image-shape {:shape shape}] - (when (and (:interactions shape) show-interactions?) - [:> "rect" #js {:x (- x 1) - :y (- y 1) - :width (+ width 2) - :height (+ height 2) - :fill "#31EFB8" - :stroke "#31EFB8" - :strokeWidth 1 - :fillOpacity 0.2}])]])) - -;; --- Image Shape + [uxbox.util.object :as obj])) (mf/defc image-shape {::mf/wrap-props false} @@ -79,7 +26,7 @@ (:uri metadata)) props (-> (attrs/extract-style-attrs shape) - (itr/obj-assign! + (obj/merge! #js {:x x :y y :transform transform diff --git a/frontend/src/uxbox/main/ui/shapes/path.cljs b/frontend/src/uxbox/main/ui/shapes/path.cljs index 68d5eeffb9..889866b60c 100644 --- a/frontend/src/uxbox/main/ui/shapes/path.cljs +++ b/frontend/src/uxbox/main/ui/shapes/path.cljs @@ -2,74 +2,19 @@ ;; 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) 2016-2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL (ns uxbox.main.ui.shapes.path (:require - [cuerdas.core :as str :include-macros true] + [cuerdas.core :as str] [rumext.alpha :as mf] - [uxbox.main.data.workspace :as dw] - [uxbox.util.geom.shapes :as geom] - [uxbox.main.refs :as refs] - [uxbox.main.store :as st] [uxbox.main.ui.shapes.attrs :as attrs] - [uxbox.main.ui.shapes.common :as common] - [uxbox.util.interop :as itr] - [uxbox.util.geom.matrix :as gmt] - [uxbox.main.ui.shapes.bounding-box :refer [bounding-box]] - [uxbox.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]])) - -;; --- Path Wrapper for workspace - -(declare path-shape) - -(mf/defc path-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - selected (mf/deref refs/selected-shapes) - selected? (contains? selected (:id shape)) - on-mouse-down (mf/use-callback - (mf/deps shape) - #(common/on-mouse-down % shape)) - on-context-menu (mf/use-callback - (mf/deps shape) - #(common/on-context-menu % shape)) - on-double-click (mf/use-callback - (mf/deps shape) - (fn [event] - (when selected? - (st/emit! (dw/start-edition-mode (:id shape))))))] - - [:g.shape {:on-double-click on-double-click - :on-mouse-down on-mouse-down - :on-context-menu on-context-menu} - [:& path-shape {:shape shape :background? true}]])) - -;; --- Path Wrapper for viewer - -(mf/defc path-viewer-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - {:keys [x y width height]} (geom/selection-rect-shape shape) - show-interactions? (mf/deref refs/show-interactions?) - on-mouse-down (mf/use-callback - (mf/deps shape) - #(common/on-mouse-down-viewer % shape))] - [:g.shape {:on-mouse-down on-mouse-down - :cursor (when (:interactions shape) "pointer")} - [:* - [:& path-shape {:shape shape}] - (when (and (:interactions shape) show-interactions?) - [:> "rect" #js {:x (- x 1) - :y (- y 1) - :width (+ width 2) - :height (+ height 2) - :fill "#31EFB8" - :stroke "#31EFB8" - :strokeWidth 1 - :fillOpacity 0.2}])]])) + [uxbox.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] + [uxbox.util.geom.shapes :as geom] + [uxbox.util.object :as obj])) ;; --- Path Shape @@ -103,7 +48,7 @@ transform (geom/transform-matrix shape) pdata (render-path shape) props (-> (attrs/extract-style-attrs shape) - (itr/obj-assign! + (obj/merge! #js {:transform transform :id (str "shape-" id) :d pdata}))] diff --git a/frontend/src/uxbox/main/ui/shapes/rect.cljs b/frontend/src/uxbox/main/ui/shapes/rect.cljs index 9b9f76ce00..0d5b71c9de 100644 --- a/frontend/src/uxbox/main/ui/shapes/rect.cljs +++ b/frontend/src/uxbox/main/ui/shapes/rect.cljs @@ -2,66 +2,18 @@ ;; 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) 2016 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL (ns uxbox.main.ui.shapes.rect (:require [rumext.alpha :as mf] - [cuerdas.core :as str] - [uxbox.util.geom.matrix :as gmt] - [uxbox.util.geom.point :as gpt] - [uxbox.util.geom.shapes :as geom] - [uxbox.main.refs :as refs] [uxbox.main.ui.shapes.attrs :as attrs] - [uxbox.main.ui.shapes.common :as common] - [uxbox.util.interop :as itr] - [uxbox.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]])) - -(declare rect-shape) - -;; --- Rect Wrapper for workspace - -(mf/defc rect-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - on-mouse-down (mf/use-callback - (mf/deps shape) - #(common/on-mouse-down % shape)) - on-context-menu (mf/use-callback - (mf/deps shape) - #(common/on-context-menu % shape))] - [:g.shape {:on-mouse-down on-mouse-down - :on-context-menu on-context-menu} - [:& rect-shape {:shape shape}]])) - -;; --- Rect Wrapper for viewer - -(mf/defc rect-viewer-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - {:keys [x y width height]} (geom/selection-rect-shape shape) - show-interactions? (mf/deref refs/show-interactions?) - on-mouse-down (mf/use-callback - (mf/deps shape) - #(common/on-mouse-down-viewer % shape))] - - [:g.shape {:on-mouse-down on-mouse-down - :cursor (when (:interactions shape) "pointer")} - [:* - [:& rect-shape {:shape shape}] - (when (and (:interactions shape) show-interactions?) - [:> "rect" #js {:x (- x 1) - :y (- y 1) - :width (+ width 2) - :height (+ height 2) - :fill "#31EFB8" - :stroke "#31EFB8" - :strokeWidth 1 - :fillOpacity 0.2}])]])) - -;; --- Rect Shape + [uxbox.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] + [uxbox.util.geom.shapes :as geom] + [uxbox.util.object :as obj])) (mf/defc rect-shape {::mf/wrap-props false} @@ -70,7 +22,7 @@ {:keys [id x y width height]} shape transform (geom/transform-matrix shape) props (-> (attrs/extract-style-attrs shape) - (itr/obj-assign! + (obj/merge! #js {:x x :y y :transform transform diff --git a/frontend/src/uxbox/main/ui/shapes/shape.cljs b/frontend/src/uxbox/main/ui/shapes/shape.cljs index 31f50ad06e..620e279fa4 100644 --- a/frontend/src/uxbox/main/ui/shapes/shape.cljs +++ b/frontend/src/uxbox/main/ui/shapes/shape.cljs @@ -7,61 +7,5 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns uxbox.main.ui.shapes.shape - (:require - [rumext.alpha :as mf] - [uxbox.main.ui.shapes.circle :as circle] - [uxbox.main.ui.shapes.common :as common] - [uxbox.main.ui.shapes.icon :as icon] - [uxbox.main.ui.shapes.image :as image] - [uxbox.main.ui.shapes.path :as path] - [uxbox.main.ui.shapes.rect :as rect] - [uxbox.main.ui.shapes.text :as text] - [uxbox.main.ui.shapes.group :as group] - [uxbox.main.ui.shapes.frame :as frame] - [uxbox.main.ui.shapes.bounding-box :refer [bounding-box]] - [uxbox.util.geom.shapes :as gsh] - [uxbox.main.refs :as refs])) +(ns uxbox.main.ui.shapes.shape) -(defn- shape-wrapper-memo-equals? - [np op] - (let [n-shape (unchecked-get np "shape") - o-shape (unchecked-get op "shape") - n-frame (unchecked-get np "frame") - o-frame (unchecked-get op "frame")] - ;; (prn "shape-wrapper-memo-equals?" (identical? n-frame o-frame)) - (if (= (:type n-shape) :group) - false - (and (identical? n-shape o-shape) - (identical? n-frame o-frame))))) - -(declare group-wrapper) -(declare frame-wrapper) - -(mf/defc shape-wrapper - {::mf/wrap [#(mf/memo' % shape-wrapper-memo-equals?)] - ::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - frame (unchecked-get props "frame") - opts #js {:shape (->> shape (gsh/transform-shape frame)) - :frame frame}] - (when (and shape (not (:hidden shape))) - [:* - (case (:type shape) - :group [:> group-wrapper opts] - :curve [:> path/path-wrapper opts] - :text [:> text/text-wrapper opts] - :icon [:> icon/icon-wrapper opts] - :rect [:> rect/rect-wrapper opts] - :path [:> path/path-wrapper opts] - :image [:> image/image-wrapper opts] - :circle [:> circle/circle-wrapper opts] - - ;; Only used when drawing a new frame. - :frame [:> frame-wrapper {:shape shape}] - nil) - [:& bounding-box {:shape shape :frame frame}]]))) - -(def group-wrapper (group/group-wrapper shape-wrapper)) -(def frame-wrapper (frame/frame-wrapper shape-wrapper)) diff --git a/frontend/src/uxbox/main/ui/shapes/text.cljs b/frontend/src/uxbox/main/ui/shapes/text.cljs index a97e5cd061..00c2248569 100644 --- a/frontend/src/uxbox/main/ui/shapes/text.cljs +++ b/frontend/src/uxbox/main/ui/shapes/text.cljs @@ -6,98 +6,12 @@ (ns uxbox.main.ui.shapes.text (:require - [cuerdas.core :as str] - [goog.events :as events] - [goog.object :as gobj] - [lentes.core :as l] [rumext.alpha :as mf] [uxbox.common.data :as d] - [uxbox.main.data.workspace :as dw] - [uxbox.main.data.workspace.texts :as dwt] - [uxbox.main.refs :as refs] - [uxbox.main.store :as st] - [uxbox.main.ui.shapes.common :as common] - [uxbox.main.ui.keyboard :as kbd] [uxbox.main.fonts :as fonts] - [uxbox.util.color :as color] - [uxbox.util.dom :as dom] [uxbox.util.geom.shapes :as geom] [uxbox.util.object :as obj] - [uxbox.util.geom.matrix :as gmt] - ["slate" :as slate] - ["slate-react" :as rslate]) - (:import goog.events.EventType - goog.events.KeyCodes)) - -;; --- Events - -(defn handle-mouse-down - [event {:keys [id group] :as shape}] - (if (and (not (:blocked shape)) - (or @refs/selected-drawing-tool - @refs/selected-edition)) - (dom/stop-propagation event) - (common/on-mouse-down event shape))) - -;; --- Text Wrapper for workspace - -(declare text-shape-html) -(declare text-shape-edit) -(declare text-shape) - -(mf/defc text-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - {:keys [id x1 y1 content group]} shape - selected (mf/deref refs/selected-shapes) - edition (mf/deref refs/selected-edition) - edition? (= edition id) - selected? (and (contains? selected id) - (= (count selected) 1)) - - on-mouse-down #(handle-mouse-down % shape) - on-context-menu #(common/on-context-menu % shape) - - on-double-click - (fn [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (when selected? - (st/emit! (dw/start-edition-mode (:id shape)))))] - - [:g.shape {:on-double-click on-double-click - :on-mouse-down on-mouse-down - :on-context-menu on-context-menu} - (if edition? - [:& text-shape-edit {:shape shape}] - [:& text-shape {:shape shape - :selected? selected?}])])) - -;; --- Text Wrapper for viewer - -(mf/defc text-viewer-wrapper - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - {:keys [x y width height]} (geom/selection-rect-shape shape) - show-interactions? (mf/deref refs/show-interactions?) - on-mouse-down (mf/use-callback - (mf/deps shape) - #(common/on-mouse-down-viewer % shape))] - [:g.shape {:on-mouse-down on-mouse-down - :cursor (when (:interactions shape) "pointer")} - [:* - [:& text-shape {:shape shape}] - (when (and (:interactions shape) show-interactions?) - [:> "rect" #js {:x (- x 1) - :y (- y 1) - :width (+ width 2) - :height (+ height 2) - :fill "#31EFB8" - :stroke "#31EFB8" - :strokeWidth 1 - :fillOpacity 0.2}])]])) + [uxbox.util.geom.matrix :as gmt])) ;; --- Text Editor Rendering @@ -169,174 +83,6 @@ base)) - -(mf/defc editor-root-node - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [props] - (let [attrs (obj/get props "attributes") - childs (obj/get props "children") - data (obj/get props "element") - type (obj/get data "type") - style (generate-root-styles data) - attrs (obj/set! attrs "style" style) - attrs (obj/set! attrs "className" type)] - [:> :div attrs childs])) - -(mf/defc editor-paragraph-set-node - {::mf/wrap-props false} - [props] - (let [attrs (obj/get props "attributes") - childs (obj/get props "children") - data (obj/get props "element") - type (obj/get data "type") - style #js {:display "inline-block" - :width "100%"} - attrs (obj/set! attrs "style" style) - attrs (obj/set! attrs "className" type)] - [:> :div attrs childs])) - -(mf/defc editor-paragraph-node - {::mf/wrap-props false} - [props] - (let [attrs (obj/get props "attributes") - childs (obj/get props "children") - data (obj/get props "element") - style (generate-paragraph-styles data) - attrs (obj/set! attrs "style" style)] - [:> :p attrs childs])) - -(mf/defc editor-text-node - {::mf/wrap-props false} - [props] - (let [attrs (obj/get props "attributes") - childs (obj/get props "children") - data (obj/get props "leaf") - style (generate-text-styles data) - attrs (obj/set! attrs "style" style)] - [:> :span attrs childs])) - -(defn- render-element - [props] - (mf/html - (let [element (obj/get props "element")] - (case (obj/get element "type") - "root" [:> editor-root-node props] - "paragraph-set" [:> editor-paragraph-set-node props] - "paragraph" [:> editor-paragraph-node props] - nil)))) - -(defn- render-text - [props] - (mf/html - [:> editor-text-node props])) - -;; --- Text Shape Edit - -(defn- initial-text - [text] - (clj->js - [{:type "root" - :children [{:type "paragraph-set" - :children [{:type "paragraph" - :children [{:text (or text "")}]}]}]}])) -(defn- parse-content - [content] - (cond - (string? content) (initial-text content) - (map? content) (clj->js [content]) - :else (initial-text ""))) - -(mf/defc text-shape-edit - {::mf/wrap [mf/memo]} - [{:keys [shape] :as props}] - (let [{:keys [id x y width height content]} shape - - state (mf/use-state #(parse-content content)) - editor (mf/use-memo #(dwt/create-editor)) - self-ref (mf/use-ref) - selecting-ref (mf/use-ref) - - on-close - (fn [] - (st/emit! dw/clear-edition-mode)) - - on-click - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (let [sidebar (dom/get-element "settings-bar") - cpicker (dom/get-element-by-class "colorpicker-tooltip") - self (mf/ref-val self-ref) - target (dom/get-target event) - selecting? (mf/ref-val selecting-ref)] - (when-not (or (.contains sidebar target) - (.contains self target) - (and cpicker (.contains cpicker target))) - (if selecting? - (mf/set-ref-val! selecting-ref false) - (on-close))))) - - on-mouse-down - (fn [event] - (mf/set-ref-val! selecting-ref true)) - - on-mouse-up - (fn [event] - (mf/set-ref-val! selecting-ref false)) - - on-keyup - (fn [event] - (when (= (.-keyCode event) 27) ; ESC - (on-close))) - - on-mount - (fn [] - (let [lkey1 (events/listen js/document EventType.CLICK on-click) - lkey2 (events/listen js/document EventType.KEYUP on-keyup)] - (st/emit! (dwt/assign-editor id editor)) - #(do - (st/emit! (dwt/assign-editor id nil)) - (events/unlistenByKey lkey1) - (events/unlistenByKey lkey2)))) - - on-focus - (fn [event] - (dwt/editor-select-all! editor)) - - on-change - (mf/use-callback - (fn [val] - (let [content (js->clj val :keywordize-keys true) - content (first content)] - (st/emit! (dw/update-shape id {:content content})) - (reset! state val))))] - - (mf/use-effect on-mount) - - [:foreignObject {:transform (geom/transform-matrix shape) - :x x :y y :width width :height height :ref self-ref} - [:> rslate/Slate {:editor editor - :value @state - :on-change on-change} - [:> rslate/Editable - {:auto-focus "true" - :spell-check "false" - :on-focus on-focus - :class "rich-text" - :render-element render-element - :render-leaf render-text - :on-mouse-up on-mouse-up - :on-mouse-down on-mouse-down - :on-blur (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - ;; WARN: monky patch - (obj/set! slate/Transforms "deselect" (constantly nil))) - :placeholder "Type some text here..."}]]])) - -;; --- Text Shape Wrapper - (defn- render-text-node ([node] (render-text-node 0 node)) ([index {:keys [type text children] :as node}] diff --git a/frontend/src/uxbox/main/ui/viewer.cljs b/frontend/src/uxbox/main/ui/viewer.cljs index 3de656e7a9..0779849056 100644 --- a/frontend/src/uxbox/main/ui/viewer.cljs +++ b/frontend/src/uxbox/main/ui/viewer.cljs @@ -25,7 +25,7 @@ [uxbox.main.ui.messages :refer [messages]] [uxbox.main.ui.viewer.header :refer [header]] [uxbox.main.ui.viewer.thumbnails :refer [thumbnails-panel]] - [uxbox.main.ui.viewer.frame-viewer :refer [frame-viewer-svg]] + [uxbox.main.ui.viewer.shapes :refer [frame-svg]] [uxbox.util.data :refer [classnames]] [uxbox.util.dom :as dom] [uxbox.util.i18n :as i18n :refer [t tr]]) @@ -48,9 +48,9 @@ [:span (t locale "viewer.frame-not-found")]] :else - [:& frame-viewer-svg {:frame frame - :zoom zoom - :objects objects}])])) + [:& frame-svg {:frame frame + :zoom zoom + :objects objects}])])) (mf/defc viewer-content [{:keys [data local index] :as props}] diff --git a/frontend/src/uxbox/main/ui/viewer/frame_viewer.cljs b/frontend/src/uxbox/main/ui/viewer/frame_viewer.cljs deleted file mode 100644 index 851fcc09c3..0000000000 --- a/frontend/src/uxbox/main/ui/viewer/frame_viewer.cljs +++ /dev/null @@ -1,104 +0,0 @@ -;; 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) 2016 Andrey Antukh - -(ns uxbox.main.ui.viewer.frame-viewer - "The main container for a frame in viewer mode" - (:require - [rumext.alpha :as mf] - [uxbox.common.uuid :as uuid] - [uxbox.util.math :as mth] - [uxbox.util.geom.shapes :as geom] - [uxbox.util.geom.point :as gpt] - [uxbox.util.geom.matrix :as gmt] - [uxbox.common.pages :as cp] - [uxbox.main.ui.shapes.frame :as frame] - [uxbox.main.ui.shapes.circle :as circle] - [uxbox.main.ui.shapes.icon :as icon] - [uxbox.main.ui.shapes.image :as image] - [uxbox.main.ui.shapes.path :as path] - [uxbox.main.ui.shapes.rect :as rect] - [uxbox.main.ui.shapes.text :as text] - [uxbox.main.ui.shapes.group :as group])) - -(declare shape-wrapper) - -(defn frame-wrapper - [objects] - (mf/fnc frame-wrapper - [{:keys [shape] :as props}] - (let [childs (mapv #(get objects %) - (:shapes shape)) - shape-wrapper (mf/use-memo (mf/deps objects) - #(shape-wrapper objects)) - frame-shape (mf/use-memo (mf/deps objects) - #(frame/frame-shape shape-wrapper)) - shape (geom/transform-shape shape)] - [:& frame-shape {:shape shape :childs childs}]))) - -(defn group-wrapper - [objects] - (mf/fnc group-wrapper - [{:keys [shape frame] :as props}] - (let [children (mapv #(get objects %) - (:shapes shape)) - shape-wrapper (mf/use-memo (mf/deps objects) - #(shape-wrapper objects)) - group-shape (mf/use-memo (mf/deps objects) - #(group/group-shape shape-wrapper))] - [:& group-shape {:frame frame - :shape shape - :children children}]))) - -(defn shape-wrapper - [objects] - (mf/fnc shape-wrapper - [{:keys [frame shape] :as props}] - (let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper objects))] - (when (and shape (not (:hidden shape))) - (let [shape (geom/transform-shape frame shape) - opts #js {:shape shape}] - (case (:type shape) - :curve [:> path/path-viewer-wrapper opts] - :text [:> text/text-viewer-wrapper opts] - :icon [:> icon/icon-viewer-wrapper opts] - :rect [:> rect/rect-viewer-wrapper opts] - :path [:> path/path-viewer-wrapper opts] - :image [:> image/image-viewer-wrapper opts] - :circle [:> circle/circle-viewer-wrapper opts] - :group [:> group-wrapper {:shape shape :frame frame}] - nil)))))) - -(mf/defc frame-viewer-svg - {::mf/wrap [mf/memo]} - [{:keys [objects frame zoom] :or {zoom 1} :as props}] - (let [modifier (-> (gpt/point (:x frame) (:y frame)) - (gpt/negate) - (gmt/translate-matrix)) - - frame-id (:id frame) - modifier-ids (concat [frame-id] (cp/get-children frame-id objects)) - - update-fn (fn [state shape-id] - (-> state - (assoc-in [shape-id :modifiers :displacement] modifier))) - objects (reduce update-fn objects modifier-ids) - frame (assoc-in frame [:modifiers :displacement] modifier ) - - width (* (:width frame) zoom) - height (* (:height frame) zoom) - vbox (str "0 0 " (:width frame 0) " " (:height frame 0)) - frame-wrapper (mf/use-memo (mf/deps objects) - #(frame-wrapper objects))] - - [:svg {:view-box vbox - :width width - :height height - :version "1.1" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg"} - [:& frame-wrapper {:shape frame - :view-box vbox}]])) - diff --git a/frontend/src/uxbox/main/ui/viewer/shapes.cljs b/frontend/src/uxbox/main/ui/viewer/shapes.cljs new file mode 100644 index 0000000000..64383d2cb5 --- /dev/null +++ b/frontend/src/uxbox/main/ui/viewer/shapes.cljs @@ -0,0 +1,149 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns uxbox.main.ui.viewer.shapes + "The main container for a frame in viewer mode" + (:require + [rumext.alpha :as mf] + [uxbox.common.pages :as cp] + [uxbox.main.data.viewer :as dv] + [uxbox.main.refs :as refs] + [uxbox.main.store :as st] + [uxbox.main.ui.shapes.circle :as circle] + [uxbox.main.ui.shapes.frame :as frame] + [uxbox.main.ui.shapes.group :as group] + [uxbox.main.ui.shapes.icon :as icon] + [uxbox.main.ui.shapes.image :as image] + [uxbox.main.ui.shapes.path :as path] + [uxbox.main.ui.shapes.rect :as rect] + [uxbox.main.ui.shapes.text :as text] + [uxbox.util.geom.matrix :as gmt] + [uxbox.util.geom.point :as gpt] + [uxbox.util.geom.shapes :as geom])) + +;; TODO: reivisit show interactions ref +;; TODO: revisit refs/frames + + +;; --- Interaction actions (in viewer mode) + +(defn on-mouse-down + [event {:keys [interactions] :as shape}] + (let [interaction (first (filter #(= (:action-type % :click)) interactions))] + (case (:action-type interaction) + :navigate + (let [frame-id (:destination interaction)] + (st/emit! (dv/go-to-frame frame-id))) + nil))) + +(declare shape-wrapper-factory) + +(defn frame-wrapper-factory + [objects] + (let [shape-wrapper (shape-wrapper-factory objects) + frame-shape (frame/frame-shape shape-wrapper)] + (mf/fnc frame-wrapper + [{:keys [shape] :as props}] + (let [childs (mapv #(get objects %) (:shapes shape)) + shape (geom/transform-shape shape)] + [:& frame-shape {:shape shape :childs childs}])))) + +(defn group-wrapper-factory + [objects] + (let [shape-wrapper (shape-wrapper-factory objects) + group-shape (group/group-shape shape-wrapper)] + (mf/fnc group-wrapper + [{:keys [shape frame] :as props}] + (let [children (mapv #(get objects %) (:shapes shape))] + [:& group-shape {:frame frame + :shape shape + :children children}])))) + +(defn generic-wrapper-factory + [component] + (mf/fnc generic-wrapper + {::mf/wrap-props false} + [props] + (let [{:keys [x y width height] + :as shape} (->> (unchecked-get props "shape") + (geom/selection-rect-shape)) + show-interactions? (unchecked-get props "show-interactions?") + on-mouse-down (mf/use-callback + (mf/deps shape) + #(on-mouse-down % shape))] + + [:g.shape {:on-mouse-down on-mouse-down + :cursor (when (:interactions shape) "pointer")} + [:& component {:shape shape}] + (when (and (:interactions shape) show-interactions?) + [:rect {:x (- x 1) + :y (- y 1) + :width (+ width 2) + :height (+ height 2) + :fill "#31EFB8" + :stroke "#31EFB8" + :stroke-width 1 + :fill-opacity 0.2}])]))) + +(def rect-wrapper (generic-wrapper-factory rect/rect-shape)) +(def icon-wrapper (generic-wrapper-factory icon/icon-shape)) +(def image-wrapper (generic-wrapper-factory image/image-shape)) +(def path-wrapper (generic-wrapper-factory path/path-shape)) +(def text-wrapper (generic-wrapper-factory text/text-shape)) +(def circle-wrapper (generic-wrapper-factory circle/circle-shape)) + +(defn shape-wrapper-factory + [objects] + (mf/fnc shape-wrapper + [{:keys [frame shape] :as props}] + (let [group-wrapper (mf/use-memo (mf/deps objects) + #(group-wrapper-factory objects))] + (when (and shape (not (:hidden shape))) + (let [shape (geom/transform-shape frame shape) + opts #js {:shape shape}] + (case (:type shape) + :curve [:> path-wrapper opts] + :text [:> text-wrapper opts] + :icon [:> icon-wrapper opts] + :rect [:> rect-wrapper opts] + :path [:> path-wrapper opts] + :image [:> image-wrapper opts] + :circle [:> circle-wrapper opts] + :group [:> group-wrapper {:shape shape :frame frame}] + nil)))))) + +(mf/defc frame-svg + {::mf/wrap [mf/memo]} + [{:keys [objects frame zoom] :or {zoom 1} :as props}] + (let [modifier (-> (gpt/point (:x frame) (:y frame)) + (gpt/negate) + (gmt/translate-matrix)) + + frame-id (:id frame) + modifier-ids (concat [frame-id] (cp/get-children frame-id objects)) + update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) + objects (reduce update-fn objects modifier-ids) + frame (assoc-in frame [:modifiers :displacement] modifier) + + width (* (:width frame) zoom) + height (* (:height frame) zoom) + vbox (str "0 0 " (:width frame 0) + " " (:height frame 0)) + wrapper (mf/use-memo + (mf/deps objects) + #(frame-wrapper-factory objects))] + + [:svg {:view-box vbox + :width width + :height height + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg"} + [:& wrapper {:shape frame :view-box vbox}]])) + diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index d0e6c025a6..ab64024243 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -16,7 +16,7 @@ [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.streams :as ms] - [uxbox.main.ui.shapes :as shapes] + [uxbox.main.ui.workspace.shapes :as shapes] [uxbox.util.math :as mth] [uxbox.util.dom :as dom] [uxbox.util.data :refer [seek]] diff --git a/frontend/src/uxbox/main/ui/workspace/shapes.cljs b/frontend/src/uxbox/main/ui/workspace/shapes.cljs new file mode 100644 index 0000000000..37e50afe4d --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/shapes.cljs @@ -0,0 +1,77 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns uxbox.main.ui.workspace.shapes + "A workspace specific shapes wrappers." + (:require + [rumext.alpha :as mf] + [uxbox.main.ui.shapes.rect :as rect] + [uxbox.main.ui.shapes.circle :as circle] + [uxbox.main.ui.shapes.icon :as icon] + [uxbox.main.ui.shapes.image :as image] + + ;; Shapes that has some peculiarities are defined in its own + ;; namespace under uxbox.ui.workspace.shapes.* prefix, all the + ;; others are defined using a generic wrapper implemented in + ;; common. + [uxbox.main.ui.workspace.shapes.bbox :as bbox] + [uxbox.main.ui.workspace.shapes.common :as common] + [uxbox.main.ui.workspace.shapes.frame :as frame] + [uxbox.main.ui.workspace.shapes.group :as group] + [uxbox.main.ui.workspace.shapes.path :as path] + [uxbox.main.ui.workspace.shapes.text :as text] + [uxbox.util.geom.shapes :as geom])) + +(declare group-wrapper) +(declare frame-wrapper) + +(def circle-wrapper (common/generic-wrapper-factory circle/circle-shape)) +(def icon-wrapper (common/generic-wrapper-factory icon/icon-shape)) +(def image-wrapper (common/generic-wrapper-factory image/image-shape)) +(def rect-wrapper (common/generic-wrapper-factory rect/rect-shape)) + +(defn- shape-wrapper-memo-equals? + [np op] + (let [n-shape (unchecked-get np "shape") + o-shape (unchecked-get op "shape") + n-frame (unchecked-get np "frame") + o-frame (unchecked-get op "frame")] + ;; (prn "shape-wrapper-memo-equals?" (identical? n-frame o-frame)) + (if (= (:type n-shape) :group) + false + (and (identical? n-shape o-shape) + (identical? n-frame o-frame))))) + +(mf/defc shape-wrapper + {::mf/wrap [#(mf/memo' % shape-wrapper-memo-equals?)] + ::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + frame (unchecked-get props "frame") + opts #js {:shape (->> shape (geom/transform-shape frame)) + :frame frame}] + (when (and shape (not (:hidden shape))) + [:* + (case (:type shape) + :curve [:> path/path-wrapper opts] + :path [:> path/path-wrapper opts] + :text [:> text/text-wrapper opts] + :group [:> group-wrapper opts] + :icon [:> icon-wrapper opts] + :rect [:> rect-wrapper opts] + :image [:> image-wrapper opts] + :circle [:> circle-wrapper opts] + + ;; Only used when drawing a new frame. + :frame [:> frame-wrapper {:shape shape}] + nil) + [:& bbox/bounding-box {:shape shape :frame frame}]]))) + +(def group-wrapper (group/group-wrapper-factory shape-wrapper)) +(def frame-wrapper (frame/frame-wrapper-factory shape-wrapper)) diff --git a/frontend/src/uxbox/main/ui/shapes/bounding_box.cljs b/frontend/src/uxbox/main/ui/workspace/shapes/bbox.cljs similarity index 98% rename from frontend/src/uxbox/main/ui/shapes/bounding_box.cljs rename to frontend/src/uxbox/main/ui/workspace/shapes/bbox.cljs index e136c6191f..2f61119d12 100644 --- a/frontend/src/uxbox/main/ui/shapes/bounding_box.cljs +++ b/frontend/src/uxbox/main/ui/workspace/shapes/bbox.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns uxbox.main.ui.shapes.bounding-box +(ns uxbox.main.ui.workspace.shapes.bbox (:require [cuerdas.core :as str] [rumext.alpha :as mf] diff --git a/frontend/src/uxbox/main/ui/shapes/common.cljs b/frontend/src/uxbox/main/ui/workspace/shapes/common.cljs similarity index 59% rename from frontend/src/uxbox/main/ui/shapes/common.cljs rename to frontend/src/uxbox/main/ui/workspace/shapes/common.cljs index 37ee6da8ad..b702ada78f 100644 --- a/frontend/src/uxbox/main/ui/shapes/common.cljs +++ b/frontend/src/uxbox/main/ui/workspace/shapes/common.cljs @@ -2,32 +2,24 @@ ;; 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) 2016-2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL -;; TODO: we need to consider moving this under uxbox.ui.workspace -;; namespace because this is logic only related to workspace -;; manipulation. Staying here causes a lot of confusion and finding -;; this code is also very difficult. - -(ns uxbox.main.ui.shapes.common +(ns uxbox.main.ui.workspace.shapes.common (:require - [potok.core :as ptk] - [beicon.core :as rx] - [uxbox.common.data :as d] - [uxbox.common.spec :as us] + [rumext.alpha :as mf] [uxbox.main.data.workspace :as dw] - [uxbox.main.data.viewer :as dv] [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.ui.keyboard :as kbd] - [uxbox.main.streams :as uws] - [uxbox.util.geom.shapes :as geom] + [uxbox.util.dom :as dom] [uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.point :as gpt] - [uxbox.util.dom :as dom])) + [uxbox.util.geom.shapes :as geom])) -;; --- Shape Movement (by mouse) -(defn on-mouse-down +(defn- on-mouse-down [event {:keys [id type] :as shape}] (let [selected @refs/selected-shapes selected? (contains? selected id) @@ -66,25 +58,27 @@ (dom/stop-propagation event) (st/emit! (dw/start-move-selected))))))) - -;; --- Workspace context menu (defn on-context-menu [event shape] (dom/prevent-default event) (dom/stop-propagation event) (let [position (dom/get-client-position event)] - (st/emit! (dw/show-shape-context-menu {:position position - :shape shape})))) + (st/emit! (dw/show-shape-context-menu {:position position :shape shape})))) + +(defn generic-wrapper-factory + [component] + (mf/fnc generic-wrapper + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + on-mouse-down (mf/use-callback + (mf/deps shape) + #(on-mouse-down % shape)) + on-context-menu (mf/use-callback + (mf/deps shape) + #(on-context-menu % shape))] + [:g.shape {:on-mouse-down on-mouse-down + :on-context-menu on-context-menu} + [:& component {:shape shape}]]))) -;; --- Interaction actions (in viewer mode) - -(defn on-mouse-down-viewer - [event {:keys [interactions] :as shape}] - (let [interaction (first (filter #(= (:action-type % :click)) interactions))] - (case (:action-type interaction) - :navigate - (let [frame-id (:destination interaction)] - (st/emit! (dv/go-to-frame frame-id))) - nil))) - diff --git a/frontend/src/uxbox/main/ui/workspace/shapes/frame.cljs b/frontend/src/uxbox/main/ui/workspace/shapes/frame.cljs new file mode 100644 index 0000000000..44aa4e0d94 --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/shapes/frame.cljs @@ -0,0 +1,109 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns uxbox.main.ui.workspace.shapes.frame + (:require + [rumext.alpha :as mf] + [uxbox.common.data :as d] + [uxbox.main.constants :as c] + [uxbox.main.data.workspace :as dw] + [uxbox.main.refs :as refs] + [uxbox.main.store :as st] + [uxbox.main.ui.workspace.shapes.common :as common] + [uxbox.main.ui.shapes.frame :as frame] + [uxbox.util.geom.matrix :as gmt] + [uxbox.util.geom.point :as gpt] + [uxbox.util.geom.shapes :as geom] + [uxbox.util.dom :as dom] + [uxbox.main.streams :as ms] + [uxbox.util.timers :as ts])) + +(defn- frame-wrapper-factory-equals? + [np op] + (let [n-shape (aget np "shape") + o-shape (aget op "shape") + n-objs (aget np "objects") + o-objs (aget op "objects") + + ids (:shapes n-shape)] + (and (identical? n-shape o-shape) + (loop [id (first ids) + ids (rest ids)] + (if (nil? id) + true + (if (identical? (get n-objs id) + (get o-objs id)) + (recur (first ids) (rest ids)) + false)))))) + +(defn frame-wrapper-factory + [shape-wrapper] + (let [frame-shape (frame/frame-shape shape-wrapper)] + (mf/fnc frame-wrapper + {::mf/wrap [#(mf/memo' % frame-wrapper-factory-equals?) + #(mf/deferred % ts/schedule-on-idle)] + ::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + objects (unchecked-get props "objects") + + selected-iref (mf/use-memo (mf/deps (:id shape)) + #(refs/make-selected (:id shape))) + selected? (mf/deref selected-iref) + zoom (mf/deref refs/selected-zoom) + + + on-mouse-down (mf/use-callback (mf/deps shape) + #(common/on-mouse-down % shape)) + on-context-menu (mf/use-callback (mf/deps shape) + #(common/on-context-menu % shape)) + + + {:keys [x y width height]} shape + + inv-zoom (/ 1 zoom) + childs (mapv #(get objects %) (:shapes shape)) + ds-modifier (get-in shape [:modifiers :displacement]) + + label-pos (cond-> (gpt/point x (- y 10)) + (gmt/matrix? ds-modifier) (gpt/transform ds-modifier)) + + on-double-click + (mf/use-callback + (mf/deps (:id shape)) + (fn [event] + (dom/prevent-default event) + (st/emit! dw/deselect-all + (dw/select-shape (:id shape)))))] + + (when-not (:hidden shape) + [:g {:class (when selected? "selected") + :on-context-menu on-context-menu + :on-double-click on-double-click + :on-mouse-down on-mouse-down} + [:text {:x 0 + :y 0 + :width width + :height 20 + :class "workspace-frame-label" + ;; Ensure that the label has always the same font + ;; size, regardless of zoom + ;; https://css-tricks.com/transforms-on-svg-elements/ + :transform (str + "scale(" inv-zoom ", " inv-zoom ") " + "translate(" (* zoom (:x label-pos)) ", " + (* zoom (:y label-pos)) ")") + ;; User may also select the frame with single click in the label + :on-click on-double-click} + (:name shape)] + [:& frame-shape + {:shape (geom/transform-shape shape) + :childs childs}]]))))) + + diff --git a/frontend/src/uxbox/main/ui/workspace/shapes/group.cljs b/frontend/src/uxbox/main/ui/workspace/shapes/group.cljs new file mode 100644 index 0000000000..be9779208d --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/shapes/group.cljs @@ -0,0 +1,74 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns uxbox.main.ui.workspace.shapes.group + (:require + [rumext.alpha :as mf] + [uxbox.common.data :as d] + [uxbox.main.constants :as c] + [uxbox.main.data.workspace :as dw] + [uxbox.main.refs :as refs] + [uxbox.main.store :as st] + [uxbox.main.ui.workspace.shapes.common :as common] + [uxbox.main.ui.shapes.group :as group] + [uxbox.util.dom :as dom] + [uxbox.main.streams :as ms] + [uxbox.util.timers :as ts])) + +(defn- group-wrapper-factory-equals? + [np op] + (let [n-shape (unchecked-get np "shape") + o-shape (unchecked-get op "shape") + n-frame (unchecked-get np "frame") + o-frame (unchecked-get op "frame")] + (and (= n-frame o-frame) + (= n-shape o-shape)))) + +(defn group-wrapper-factory + [shape-wrapper] + (let [group-shape (group/group-shape shape-wrapper)] + (mf/fnc group-wrapper + {::mf/wrap [#(mf/memo' % group-wrapper-factory-equals?)] + ::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + frame (unchecked-get props "frame") + on-mouse-down (mf/use-callback (mf/deps shape) + #(common/on-mouse-down % shape)) + on-context-menu (mf/use-callback (mf/deps shape) + #(common/on-context-menu % shape)) + + children-ref (mf/use-memo (mf/deps shape) + #(refs/objects-by-id (:shapes shape))) + children (mf/deref children-ref) + + + is-child-selected-ref (mf/use-memo (mf/deps (:id shape)) + #(refs/is-child-selected? (:id shape))) + is-child-selected? (mf/deref is-child-selected-ref) + + on-double-click + (mf/use-callback + (mf/deps (:id shape)) + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! (dw/select-inside-group (:id shape) @ms/mouse-position))))] + + [:g.shape + {:on-mouse-down on-mouse-down + :on-context-menu on-context-menu + :on-double-click on-double-click} + + [:& group-shape + {:frame frame + :shape shape + :children children + :is-child-selected? is-child-selected?}]])))) + diff --git a/frontend/src/uxbox/main/ui/workspace/shapes/path.cljs b/frontend/src/uxbox/main/ui/workspace/shapes/path.cljs new file mode 100644 index 0000000000..878b2534dc --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/shapes/path.cljs @@ -0,0 +1,51 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns uxbox.main.ui.workspace.shapes.path + (:require + [rumext.alpha :as mf] + [uxbox.common.data :as d] + [uxbox.main.constants :as c] + [uxbox.main.data.workspace :as dw] + [uxbox.main.refs :as refs] + [uxbox.main.store :as st] + [uxbox.main.ui.keyboard :as kbd] + [uxbox.main.ui.shapes.path :as path] + [uxbox.main.ui.workspace.shapes.common :as common] + [uxbox.util.dom :as dom] + [uxbox.util.geom.matrix :as gmt] + [uxbox.util.geom.point :as gpt] + [uxbox.util.geom.shapes :as geom] + [uxbox.util.interop :as itr] + [uxbox.main.streams :as ms] + [uxbox.util.timers :as ts])) + +(mf/defc path-wrapper + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + selected (mf/deref refs/selected-shapes) + selected? (contains? selected (:id shape)) + on-mouse-down (mf/use-callback + (mf/deps shape) + #(common/on-mouse-down % shape)) + on-context-menu (mf/use-callback + (mf/deps shape) + #(common/on-context-menu % shape)) + on-double-click (mf/use-callback + (mf/deps shape) + (fn [event] + (when selected? + (st/emit! (dw/start-edition-mode (:id shape))))))] + + [:g.shape {:on-double-click on-double-click + :on-mouse-down on-mouse-down + :on-context-menu on-context-menu} + [:& path/path-shape {:shape shape :background? true}]])) + diff --git a/frontend/src/uxbox/main/ui/workspace/shapes/text.cljs b/frontend/src/uxbox/main/ui/workspace/shapes/text.cljs new file mode 100644 index 0000000000..fdb04b344e --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/shapes/text.cljs @@ -0,0 +1,311 @@ +;; 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) 2016-2019 Andrey Antukh + +(ns uxbox.main.ui.workspace.shapes.text + (:require + [cuerdas.core :as str] + [goog.events :as events] + [goog.object :as gobj] + [lentes.core :as l] + [rumext.alpha :as mf] + [uxbox.common.data :as d] + [uxbox.main.data.workspace :as dw] + [uxbox.main.data.workspace.texts :as dwt] + [uxbox.main.refs :as refs] + [uxbox.main.store :as st] + [uxbox.main.ui.workspace.shapes.common :as common] + [uxbox.main.ui.shapes.text :as text] + [uxbox.main.ui.keyboard :as kbd] + [uxbox.main.fonts :as fonts] + [uxbox.util.color :as color] + [uxbox.util.dom :as dom] + [uxbox.util.geom.shapes :as geom] + [uxbox.util.object :as obj] + [uxbox.util.geom.matrix :as gmt] + ["slate" :as slate] + ["slate-react" :as rslate]) + (:import + goog.events.EventType + goog.events.KeyCodes)) + +;; --- Events + +(defn handle-mouse-down + [event {:keys [id group] :as shape}] + (if (and (not (:blocked shape)) + (or @refs/selected-drawing-tool + @refs/selected-edition)) + (dom/stop-propagation event) + (common/on-mouse-down event shape))) + +;; --- Text Wrapper for workspace + +(declare text-shape-edit) +(declare text-shape) + +(mf/defc text-wrapper + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + {:keys [id x1 y1 content group]} shape + selected (mf/deref refs/selected-shapes) + edition (mf/deref refs/selected-edition) + edition? (= edition id) + selected? (and (contains? selected id) + (= (count selected) 1)) + + on-mouse-down #(handle-mouse-down % shape) + on-context-menu #(common/on-context-menu % shape) + + on-double-click + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (when selected? + (st/emit! (dw/start-edition-mode (:id shape)))))] + + [:g.shape {:on-double-click on-double-click + :on-mouse-down on-mouse-down + :on-context-menu on-context-menu} + (if edition? + [:& text-shape-edit {:shape shape}] + [:& text/text-shape {:shape shape + :selected? selected?}])])) + +;; --- Text Editor Rendering + +(defn- generate-root-styles + [data] + (let [valign (obj/get data "vertical-align") + base #js {:height "100%" + :width "100%" + :display "flex"}] + (cond-> base + (= valign "top") (obj/set! "alignItems" "flex-start") + (= valign "center") (obj/set! "alignItems" "center") + (= valign "bottom") (obj/set! "alignItems" "flex-end")))) + +(defn- generate-paragraph-styles + [data] + (let [base #js {:fontSize "14px" + :margin "inherit" + :lineHeight "1.2"} + lh (obj/get data "line-height") + ta (obj/get data "text-align")] + (cond-> base + ta (obj/set! "textAlign" ta) + lh (obj/set! "lineHeight" lh)))) + +(defn- generate-text-styles + [data] + (let [letter-spacing (obj/get data "letter-spacing") + text-decoration (obj/get data "text-decoration") + text-transform (obj/get data "text-transform") + + font-id (obj/get data "font-id") + font-variant-id (obj/get data "font-variant-id") + + font-family (obj/get data "font-family") + font-size (obj/get data "font-size") + fill (obj/get data "fill") + opacity (obj/get data "opacity") + fontsdb (deref fonts/fontsdb) + + base #js {:textDecoration text-decoration + :color fill + :opacity opacity + :textTransform text-transform}] + + (when (and (string? letter-spacing) + (pos? (alength letter-spacing))) + (obj/set! base "letterSpacing" (str letter-spacing "px"))) + + (when (and (string? font-size) + (pos? (alength font-size))) + (obj/set! base "fontSize" (str font-size "px"))) + + (when (and (string? font-id) + (pos? (alength font-id))) + (let [font (get fontsdb font-id)] + (fonts/ensure-loaded! font-id) + (let [font-family (or (:family font) + (obj/get data "fontFamily")) + font-variant (d/seek #(= font-variant-id (:id %)) + (:variants font)) + font-style (or (:style font-variant) + (obj/get data "fontStyle")) + font-weight (or (:weight font-variant) + (obj/get data "fontWeight"))] + (obj/set! base "fontFamily" font-family) + (obj/set! base "fontStyle" font-style) + (obj/set! base "fontWeight" font-weight)))) + + base)) + +(mf/defc editor-root-node + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [props] + (let [attrs (obj/get props "attributes") + childs (obj/get props "children") + data (obj/get props "element") + type (obj/get data "type") + style (generate-root-styles data) + attrs (obj/set! attrs "style" style) + attrs (obj/set! attrs "className" type)] + [:> :div attrs childs])) + +(mf/defc editor-paragraph-set-node + {::mf/wrap-props false} + [props] + (let [attrs (obj/get props "attributes") + childs (obj/get props "children") + data (obj/get props "element") + type (obj/get data "type") + style #js {:display "inline-block" + :width "100%"} + attrs (obj/set! attrs "style" style) + attrs (obj/set! attrs "className" type)] + [:> :div attrs childs])) + +(mf/defc editor-paragraph-node + {::mf/wrap-props false} + [props] + (let [attrs (obj/get props "attributes") + childs (obj/get props "children") + data (obj/get props "element") + style (generate-paragraph-styles data) + attrs (obj/set! attrs "style" style)] + [:> :p attrs childs])) + +(mf/defc editor-text-node + {::mf/wrap-props false} + [props] + (let [attrs (obj/get props "attributes") + childs (obj/get props "children") + data (obj/get props "leaf") + style (generate-text-styles data) + attrs (obj/set! attrs "style" style)] + [:> :span attrs childs])) + +(defn- render-element + [props] + (mf/html + (let [element (obj/get props "element")] + (case (obj/get element "type") + "root" [:> editor-root-node props] + "paragraph-set" [:> editor-paragraph-set-node props] + "paragraph" [:> editor-paragraph-node props] + nil)))) + +(defn- render-text + [props] + (mf/html + [:> editor-text-node props])) + +;; --- Text Shape Edit + +(defn- initial-text + [text] + (clj->js + [{:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" + :children [{:text (or text "")}]}]}]}])) +(defn- parse-content + [content] + (cond + (string? content) (initial-text content) + (map? content) (clj->js [content]) + :else (initial-text ""))) + +(mf/defc text-shape-edit + {::mf/wrap [mf/memo]} + [{:keys [shape] :as props}] + (let [{:keys [id x y width height content]} shape + + state (mf/use-state #(parse-content content)) + editor (mf/use-memo #(dwt/create-editor)) + self-ref (mf/use-ref) + selecting-ref (mf/use-ref) + + on-close + (fn [] + (st/emit! dw/clear-edition-mode)) + + on-click + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (let [sidebar (dom/get-element "settings-bar") + cpicker (dom/get-element-by-class "colorpicker-tooltip") + self (mf/ref-val self-ref) + target (dom/get-target event) + selecting? (mf/ref-val selecting-ref)] + (when-not (or (.contains sidebar target) + (.contains self target) + (and cpicker (.contains cpicker target))) + (if selecting? + (mf/set-ref-val! selecting-ref false) + (on-close))))) + + on-mouse-down + (fn [event] + (mf/set-ref-val! selecting-ref true)) + + on-mouse-up + (fn [event] + (mf/set-ref-val! selecting-ref false)) + + on-keyup + (fn [event] + (when (= (.-keyCode event) 27) ; ESC + (on-close))) + + on-mount + (fn [] + (let [lkey1 (events/listen js/document EventType.CLICK on-click) + lkey2 (events/listen js/document EventType.KEYUP on-keyup)] + (st/emit! (dwt/assign-editor id editor)) + #(do + (st/emit! (dwt/assign-editor id nil)) + (events/unlistenByKey lkey1) + (events/unlistenByKey lkey2)))) + + on-focus + (fn [event] + (dwt/editor-select-all! editor)) + + on-change + (mf/use-callback + (fn [val] + (let [content (js->clj val :keywordize-keys true) + content (first content)] + (st/emit! (dw/update-shape id {:content content})) + (reset! state val))))] + + (mf/use-effect on-mount) + + [:foreignObject {:transform (geom/transform-matrix shape) + :x x :y y :width width :height height :ref self-ref} + [:> rslate/Slate {:editor editor + :value @state + :on-change on-change} + [:> rslate/Editable + {:auto-focus "true" + :spell-check "false" + :on-focus on-focus + :class "rich-text" + :render-element render-element + :render-leaf render-text + :on-mouse-up on-mouse-up + :on-mouse-down on-mouse-down + :on-blur (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + ;; WARN: monky patch + (obj/set! slate/Transforms "deselect" (constantly nil))) + :placeholder "Type some text here..."}]]])) diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index 5e5fc597fa..6fdf281598 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -23,7 +23,7 @@ [uxbox.main.streams :as ms] [uxbox.main.ui.keyboard :as kbd] [uxbox.main.ui.hooks :as hooks] - [uxbox.main.ui.shapes :refer [shape-wrapper frame-wrapper]] + [uxbox.main.ui.workspace.shapes :refer [shape-wrapper frame-wrapper]] [uxbox.main.ui.workspace.drawarea :refer [draw-area start-drawing]] [uxbox.main.ui.workspace.grid :refer [grid]] [uxbox.main.ui.workspace.ruler :refer [ruler]] diff --git a/frontend/src/uxbox/worker/thumbnails.cljs b/frontend/src/uxbox/worker/thumbnails.cljs new file mode 100644 index 0000000000..9603813cdf --- /dev/null +++ b/frontend/src/uxbox/worker/thumbnails.cljs @@ -0,0 +1,34 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns uxbox.worker.thumbnails + (:require + [rumext.alpha :as mf] + [cljs.spec.alpha :as s] + [uxbox.common.exceptions :as ex] + [uxbox.common.spec :as us] + [uxbox.main.exports :as exports] + [uxbox.worker.impl :as impl] + ["react-dom/server" :as rds])) + +(mf/defc foobar + [{:keys [name]}] + [:span name]) + +(defmethod impl/handler :echo + [message] + {:result (rds/renderToString (mf/element foobar {:name "foobar"}))}) + +(defmethod impl/handler :thumbnails/generate + [{:keys [data] :as message}] + (let [elem (mf/element exports/page-svg #js {:data data + :width "290" + :height "150"})] + (rds/renderToStaticMarkup elem))) +