diff --git a/backend/tests/uxbox/tests/test_common_pages.clj b/backend/tests/uxbox/tests/test_common_pages.clj index 9f8213737c..90c9ecec54 100644 --- a/backend/tests/uxbox/tests/test_common_pages.clj +++ b/backend/tests/uxbox/tests/test_common_pages.clj @@ -14,6 +14,38 @@ [uxbox.common.uuid :as uuid] [uxbox.tests.helpers :as th])) +(t/deftest process-change-set-option + (let [data cp/default-page-data] + (t/testing "Sets option single" + (let [chg {:type :set-option + :option :test + :value "test"} + res (cp/process-changes data [chg])] + (t/is (= "test" (get-in res [:options :test]))))) + + (t/testing "Sets option nested" + (let [chgs [{:type :set-option + :option [:values :test :a] + :value "a"} + {:type :set-option + :option [:values :test :b] + :value "b"}] + res (cp/process-changes data chgs)] + (t/is (= {:a "a" :b "b"} (get-in res [:options :values :test]))))) + + (t/testing "Remove option" + (let [chgs [{:type :set-option + :option [:values :test :a] + :value "a"} + {:type :set-option + :option [:values :test :b] + :value "b"} + {:type :set-option + :option [:values :test] + :value nil}] + res (cp/process-changes data chgs)] + (t/is (= nil (get-in res [:options :values :test]))))))) + (t/deftest process-change-add-obj (let [data cp/default-page-data id-a (uuid/next) diff --git a/common/uxbox/common/data.cljc b/common/uxbox/common/data.cljc index 48ec9b8840..af39f80f26 100644 --- a/common/uxbox/common/data.cljc +++ b/common/uxbox/common/data.cljc @@ -9,7 +9,9 @@ (:refer-clojure :exclude [concat read-string]) (:require [clojure.set :as set] #?(:cljs [cljs.reader :as r] - :clj [clojure.edn :as r]))) + :clj [clojure.edn :as r]) + #?(:cljs [cljs.core :as core] + :clj [clojure.core :as core]))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Structures Manipulation @@ -94,6 +96,12 @@ (persistent! (reduce #(dissoc! %1 %2) (transient data) keys))) +(defn remove-at-index + [v index] + (vec (core/concat + (subvec v 0 index) + (subvec v (inc index))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Parsing / Conversion ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/common/uxbox/common/pages.cljc b/common/uxbox/common/pages.cljc index e02dfbffe1..db454761d3 100644 --- a/common/uxbox/common/pages.cljc +++ b/common/uxbox/common/pages.cljc @@ -253,6 +253,12 @@ (defmulti change-spec-impl :type) +(s/def :set-option/option any? #_(s/or keyword? (s/coll-of keyword?))) +(s/def :set-option/value any?) + +(defmethod change-spec-impl :set-option [_] + (s/keys :req-un [:set-option/option :set-option/value])) + (defmethod change-spec-impl :add-obj [_] (s/keys :req-un [::id ::frame-id ::obj] :opt-un [::session-id ::parent-id])) @@ -313,6 +319,12 @@ (declare insert-at-index) +(defmethod process-change :set-option + [data {:keys [option value]}] + (let [path (if (seqable? option) option [option])] + (-> data + (assoc-in (into [:options] path) value)))) + (defmethod process-change :add-obj [data {:keys [id obj frame-id parent-id index] :as change}] (let [parent-id (or parent-id frame-id) diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index a4ca8a71de..0f5fc05598 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -1,28 +1,28 @@ { "dashboard.grid.delete" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/project.cljs:61", "src/uxbox/main/ui/dashboard/grid.cljs:92" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:102", "src/uxbox/main/ui/dashboard/project.cljs:62" ], "translations" : { "en" : "Delete" } }, "dashboard.grid.edit" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/project.cljs:60", "src/uxbox/main/ui/dashboard/grid.cljs:91" ], "translations" : { "en" : "Edit" + }, + "unused" : true + }, + "dashboard.grid.empty-files" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:124" ], + "translations" : { + "en" : "You still have no files here" } }, "dashboard.grid.rename" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/project.cljs:60", "src/uxbox/main/ui/dashboard/grid.cljs:91" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:101", "src/uxbox/main/ui/dashboard/project.cljs:61" ], "translations" : { "en" : "Rename" } }, - "dashboard.grid.empty-files" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:114" ], - "translations" : { - "en" : "You still have no files here" - } - }, "dashboard.header.draft" : { "used-in" : [ "src/uxbox/main/ui/dashboard/project.cljs:55" ], "translations" : { @@ -63,7 +63,7 @@ } }, "dashboard.header.project" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/project.cljs:68" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/project.cljs:57" ], "translations" : { "en" : "Project %s" } @@ -176,7 +176,7 @@ } }, "ds.button.delete" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:152", "src/uxbox/main/ui/dashboard/library.cljs:220", "src/uxbox/main/ui/dashboard/library.cljs:257", "src/uxbox/main/ui/dashboard/library.cljs:296" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:152", "src/uxbox/main/ui/dashboard/library.cljs:220", "src/uxbox/main/ui/dashboard/library.cljs:259", "src/uxbox/main/ui/dashboard/library.cljs:300" ], "translations" : { "en" : "Delete" } @@ -257,7 +257,7 @@ "unused" : true }, "ds.new-file" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:110", "src/uxbox/main/ui/dashboard/grid.cljs:116" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:120", "src/uxbox/main/ui/dashboard/grid.cljs:126" ], "translations" : { "en" : "+ New File", "fr" : null @@ -299,7 +299,7 @@ } }, "ds.updated-at" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:35" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:45" ], "translations" : { "en" : "Updated: %s", "fr" : "Mis à jour: %s" @@ -341,21 +341,21 @@ } }, "errors.generic" : { - "used-in" : [ "src/uxbox/main/ui.cljs:179" ], + "used-in" : [ "src/uxbox/main/ui.cljs:178" ], "translations" : { "en" : "Something wrong has happened.", "fr" : "Quelque chose c'est mal passé." } }, "errors.network" : { - "used-in" : [ "src/uxbox/main/ui.cljs:173" ], + "used-in" : [ "src/uxbox/main/ui.cljs:172" ], "translations" : { "en" : "Unable to connect to backend server.", "fr" : "Impossible de se connecter au serveur principal." } }, "header.sitemap" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:74" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:67" ], "translations" : { "en" : null, "fr" : null @@ -459,7 +459,7 @@ } }, "profile.recovery.go-to-login" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:65", "src/uxbox/main/ui/profile/recovery.cljs:81" ], + "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:81", "src/uxbox/main/ui/profile/recovery_request.cljs:65" ], "translations" : { "en" : "Go back!", "fr" : "Retour!" @@ -702,73 +702,73 @@ } }, "viewer.header.dont-show-interactions" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:40" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:67" ], "translations" : { "en" : "Don't show interactions" } }, "viewer.header.edit-page" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:137" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:164" ], "translations" : { "en" : "Edit page" } }, "viewer.header.fullscreen" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:148" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:175" ], "translations" : { "en" : "Full Screen" } }, "viewer.header.share.copy-link" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:86" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:113" ], "translations" : { "en" : "Copy link" } }, "viewer.header.share.create-link" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:94" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:121" ], "translations" : { "en" : "Create link" } }, "viewer.header.share.placeholder" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:84" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:111" ], "translations" : { "en" : "Share link will apear here" } }, "viewer.header.share.remove-link" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:92" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:119" ], "translations" : { "en" : "Remove link" } }, "viewer.header.share.subtitle" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:88" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:115" ], "translations" : { "en" : "Anyone with the link will have access" } }, "viewer.header.share.title" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:72", "src/uxbox/main/ui/viewer/header.cljs:74", "src/uxbox/main/ui/viewer/header.cljs:80" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:99", "src/uxbox/main/ui/viewer/header.cljs:101", "src/uxbox/main/ui/viewer/header.cljs:107" ], "translations" : { "en" : "Share link" } }, "viewer.header.show-interactions" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:44" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:71" ], "translations" : { "en" : "Show interactions" } }, "viewer.header.show-interactions-on-click" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:48" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:75" ], "translations" : { "en" : "Show interactions on click" } }, "viewer.header.sitemap" : { - "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:121" ], + "used-in" : [ "src/uxbox/main/ui/viewer/header.cljs:148" ], "translations" : { "en" : "Sitemap" } @@ -821,70 +821,92 @@ "en" : "Align top" } }, + "workspace.header.menu.disable-dynamic-alignment" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:113" ], + "translations" : { + "en" : "Disable dynamic alignment" + } + }, + "workspace.header.menu.disable-snap-grid" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:89" ], + "translations" : { + "en" : "Disable snap to grid" + } + }, + "workspace.header.menu.enable-dynamic-alignment" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:114" ], + "translations" : { + "en" : "Enable dynamic aligment" + } + }, + "workspace.header.menu.enable-snap-grid" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:90" ], + "translations" : { + "en" : "Snap to grid" + } + }, "workspace.header.menu.hide-grid" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:94" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:83" ], "translations" : { "en" : "Hide grid" } }, "workspace.header.menu.hide-layers" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:101" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:95" ], "translations" : { "en" : "Hide layers" } }, "workspace.header.menu.hide-libraries" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:115" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:107" ], "translations" : { "en" : "Hide libraries" } }, "workspace.header.menu.hide-palette" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:108" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:101" ], "translations" : { "en" : "Hide color palette" } }, "workspace.header.menu.hide-rules" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:87" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:77" ], "translations" : { "en" : "Hide rules" } }, "workspace.header.menu.show-grid" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:95" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:84" ], "translations" : { "en" : "Show grid" } }, "workspace.header.menu.show-layers" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:102" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:96" ], "translations" : { "en" : "Show layers" } }, "workspace.header.menu.show-libraries" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:116" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:108" ], "translations" : { "en" : "Show libraries" } }, "workspace.header.menu.show-palette" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:109" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:102" ], "translations" : { "en" : "Show color palette" } }, "workspace.header.menu.show-rules" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:88" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:78" ], "translations" : { "en" : "Show rules" } }, - "workspace.header.menu.disable-dynamic-alignment": "Disable dynamic alignment", - "workspace.header.menu.enable-dynamic-alignment": "Enable dynamic aligment", "workspace.header.viewer" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:153" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:151" ], "translations" : { "en" : "View mode (Ctrl + P)", "fr" : "Mode visualisation (Ctrl + P)" @@ -927,11 +949,11 @@ } }, "workspace.options.color" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:89" ], "translations" : { "en" : "Color", "fr" : "Couleur" - } + }, + "unused" : true }, "workspace.options.design" : { "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options.cljs:76" ], @@ -940,7 +962,7 @@ } }, "workspace.options.fill" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:69", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:446" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:446", "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:71" ], "translations" : { "en" : "Fill", "fr" : "Fond" @@ -1076,10 +1098,124 @@ "unused" : true }, "workspace.options.grid-options" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:76" ], "translations" : { "en" : "Grid settings", "fr" : "Paramètres de la grille" + }, + "unused" : true + }, + "workspace.options.grid.auto" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:40" ], + "translations" : { + "en" : "Auto" + } + }, + "workspace.options.grid.column" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:125" ], + "translations" : { + "en" : "Columns" + } + }, + "workspace.options.grid.params.columns" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:164" ], + "translations" : { + "en" : "Columns" + } + }, + "workspace.options.grid.params.gutter" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:191" ], + "translations" : { + "en" : "Gutter" + } + }, + "workspace.options.grid.params.height" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:184" ], + "translations" : { + "en" : "Height" + } + }, + "workspace.options.grid.params.margin" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:196" ], + "translations" : { + "en" : "Margin" + } + }, + "workspace.options.grid.params.rows" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:156" ], + "translations" : { + "en" : "Rows" + } + }, + "workspace.options.grid.params.set-default" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:208" ], + "translations" : { + "en" : "Set as default" + } + }, + "workspace.options.grid.params.size" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:149" ], + "translations" : { + "en" : "Size" + } + }, + "workspace.options.grid.params.type" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:172" ], + "translations" : { + "en" : "Type" + } + }, + "workspace.options.grid.params.type.center" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:176" ], + "translations" : { + "en" : "Center" + } + }, + "workspace.options.grid.params.type.left" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:175" ], + "translations" : { + "en" : "Left" + } + }, + "workspace.options.grid.params.type.right" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:177" ], + "translations" : { + "en" : "Right" + } + }, + "workspace.options.grid.params.type.stretch" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:174" ], + "translations" : { + "en" : "Stretch" + } + }, + "workspace.options.grid.params.use-default" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:206" ], + "translations" : { + "en" : "Use default" + } + }, + "workspace.options.grid.params.width" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:183" ], + "translations" : { + "en" : "Width" + } + }, + "workspace.options.grid.row" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:126" ], + "translations" : { + "en" : "Rows" + } + }, + "workspace.options.grid.square" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:124" ], + "translations" : { + "en" : "Square" + } + }, + "workspace.options.grid.title" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:220" ], + "translations" : { + "en" : "Grid & Layouts" } }, "workspace.options.line-height-letter-spacing" : { @@ -1097,13 +1233,13 @@ "unused" : true }, "workspace.options.navigate-to" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:51" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:58" ], "translations" : { "en" : "Navigate to" } }, "workspace.options.none" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:64" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:71" ], "translations" : { "en" : "None" } @@ -1116,7 +1252,7 @@ "unused" : true }, "workspace.options.position" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:135", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:126" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:127", "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:138" ], "translations" : { "en" : "Position", "fr" : "Position" @@ -1129,14 +1265,14 @@ } }, "workspace.options.radius" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:183" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:179" ], "translations" : { "en" : "Radius", "fr" : "TODO" } }, "workspace.options.rotation" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:159" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:154" ], "translations" : { "en" : "Rotation", "fr" : "TODO" @@ -1150,78 +1286,78 @@ "unused" : true }, "workspace.options.select-a-shape" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:45" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:52" ], "translations" : { "en" : "Select a shape, artboard or group to drag a connection to other artboard." } }, "workspace.options.select-artboard" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:57" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:64" ], "translations" : { "en" : "Select artboard" } }, "workspace.options.size" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:79", "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:107", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:101" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:102", "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:110" ], "translations" : { "en" : "Size", "fr" : "Taille" } }, "workspace.options.size-presets" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:83" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:84" ], "translations" : { "en" : "Size presets" } }, "workspace.options.stroke" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:109", "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:173" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:111", "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:175" ], "translations" : { "en" : "Stroke", "fr" : null } }, "workspace.options.stroke.center" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:159" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:161" ], "translations" : { "en" : "Center" } }, "workspace.options.stroke.dashed" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:167" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:169" ], "translations" : { "en" : "Dashed", "fr" : "Tiré" } }, "workspace.options.stroke.dotted" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:166" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:168" ], "translations" : { "en" : "Dotted", "fr" : "Pointillé" } }, "workspace.options.stroke.inner" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:160" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:162" ], "translations" : { "en" : "Inside" } }, "workspace.options.stroke.mixed" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:168" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:170" ], "translations" : { "en" : "Mixed", "fr" : "Mixte" } }, "workspace.options.stroke.outer" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:161" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:163" ], "translations" : { "en" : "Outside" } }, "workspace.options.stroke.solid" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:165" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:167" ], "translations" : { "en" : "Solid", "fr" : "Solide" @@ -1242,7 +1378,7 @@ "unused" : true }, "workspace.options.use-play-button" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:47" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:54" ], "translations" : { "en" : "Use the play button at the header to run the prototype view." } @@ -1366,7 +1502,7 @@ } }, "workspace.viewport.click-to-close-path" : { - "used-in" : [ "src/uxbox/main/ui/workspace/drawarea.cljs:360" ], + "used-in" : [ "src/uxbox/main/ui/workspace/drawarea.cljs:357" ], "translations" : { "en" : "Click to close the path" } diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index 06bf218c13..965a449ecc 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -366,6 +366,9 @@ ul.slider-dots { // Input amounts &.pixels { + & input { + padding-right: 20px; + } &::after { content: "px"; diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 7acfadd063..183a68e616 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -279,63 +279,103 @@ font-size: $fs13; } - .custom-select-dropdown { - position: absolute; - left: 0; - z-index: 12; + + } + .custom-select-dropdown { + position: absolute; + left: 0; + z-index: 12; + max-height: 30rem; + min-width: 7rem; + overflow-y: auto; + + background-color: $color-white; + border-radius: $br-small; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); + + .presets { width: 200px; - max-height: 30rem; - min-width: 7rem; - overflow-y: auto; + } - background-color: $color-white; - border-radius: $br-small; - box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); + hr { + margin: 0; + border-color: $color-gray-20; + } - li { - color: $color-gray-60; - cursor: pointer; - font-size: $fs14; - display: flex; - padding: $small; + li { + color: $color-gray-60; + cursor: pointer; + font-size: $fs14; + display: flex; + padding: $small; - span { - color: $color-gray-20; - margin-left: auto; - } + span { + color: $color-gray-20; + margin-left: auto; + } - &.dropdown-separator:not(:last-child) { - border-bottom: 1px solid $color-gray-10; - } + &.dropdown-separator:not(:last-child) { + border-bottom: 1px solid $color-gray-10; + } - &.dropdown-label:not(:first-child) { - border-top: 1px solid $color-gray-10; - } + &.dropdown-label:not(:first-child) { + border-top: 1px solid $color-gray-10; + } - &.dropdown-label span { - margin-left: 0; - } + &.dropdown-label span { + margin-left: 0; + } - &:hover { - background-color: $color-primary-lighter; - } + &:hover { + background-color: $color-primary-lighter; + } + } + } + + & li.checked-element { + padding-left: 0; + + & span { + margin: 0; + color: $color-black; + } + + & svg { + visibility: hidden; + width: 8px; + height: 8px; + background: none; + margin: 0.25rem; + fill: $color-black; + } + + &.is-selected { + & svg { + visibility: visible; } } } .editable-select { + position: relative; height: 38px; margin-right: $small; position: relative; width: 60%; + svg { + fill: $color-gray-40; + height: 10px; + width: 10px; + } + .input-text { left: 0; position: absolute; top: -1px; width: 60%; } - + .input-select { background-color: transparent; border: none; @@ -352,6 +392,45 @@ font-size: $fs12; } } + + .dropdown-button { + position: absolute; + top: 7px; + right: 0; + } + + &.input-option { + height: 2rem; + border-bottom: 1px solid #64666A; + width: 100%; + margin-left: 0.25rem; + + .input-text { + border: none; + margin: 0; + width: calc(100% - 12px); + height: 100%; + top: auto; + color: #b1b2b5; + } + } + } +} + +.element-set-content .grid-option-main { + .editable-select.input-option .input-text { + padding: 0; + padding-top: 0.18rem; + } + + .input-element { + width: 55px; + margin: 0 0.2rem; + } + + .input-text { + padding-left: 0; + color: #b1b2b5; } } @@ -386,7 +465,12 @@ } } +} +.presets { + .custom-select-dropdown { + width: 200px; + } } .row-flex.align-icons { @@ -527,7 +611,7 @@ height: 18px; position: relative; width: 18px; - + svg { fill: $color-gray-30; height: 16px; @@ -535,3 +619,129 @@ } } } + +.custom-button { + cursor: pointer; + background: none; + border: none; + + & svg { + width: 1rem; + height: 1rem; + fill: $color-gray-20; + } + + &:hover svg, &.is-active svg { + fill: $color-primary; + } +} + +.element-set-content .input-row { + & .element-set-subtitle { + width: 5.5rem; + } +} + +.grid-option { + margin-bottom: 0.5rem; +} + +.element-set-content .custom-select.input-option { + border-top: none; + border-left: none; + border-right: none; + margin-left: 0.25rem; +} + +.element-set-content .grid-option-main { + display: flex; + padding: 0.5rem 0; + border: 1px solid $color-black; + border-radius: 4px; + + &:hover { + background: #1F1F1F; + } + + & .custom-select { + min-width: 4.75rem; + height: 2rem; + border: none; + border-bottom: 1px solid #65666A; + } + + & .input-element { + width: 50px; + overflow: hidden; + } + + & .custom-select-dropdown { + width: 96px; + } + + & .input-option { + margin-left: 0.5rem; + + & .custom-select-dropdown { + width: 56px; + min-width: 56px; + max-height: 10rem; + } + + } +} + +.grid-option-main-actions { + display: flex; + visibility: hidden; + + .grid-option:hover & { + visibility: visible; + } +} + +.focus-overlay { + background: $color-black; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + opacity: 0.4; +} + +.element-set-content .advanced-options { + background-color: #303236; + border-radius: 4px; + left: -8px; + padding: 0.5rem; + position: relative; + top: 2px; + width: calc(100% + 16px); +} + +.btn-options { + cursor: pointer; + border: 1px solid $color-black; + background: $color-gray-60; + border-radius: 2px; + color: $color-gray-20; + font-size: 11px; + line-height: 16px; + flex-grow: 1; + padding: 0.25rem 0; + + &:first-child { + margin-right: 0.5rem; + } + + &:not([disabled]):hover { + background: $color-primary; + color: $color-black; + } + + &[disabled] { + opacity: 0.4; + cursor: auto; + } +} diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 2e40877952..11962eb434 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -66,7 +66,9 @@ :layers :element-options :rules - :dynamic-alignment}) + :dynamic-alignment + :display-grid + :snap-grid}) (s/def ::options-mode #{:design :prototype}) diff --git a/frontend/src/uxbox/main/data/workspace/grid.cljs b/frontend/src/uxbox/main/data/workspace/grid.cljs new file mode 100644 index 0000000000..31e666e7b5 --- /dev/null +++ b/frontend/src/uxbox/main/data/workspace/grid.cljs @@ -0,0 +1,83 @@ +;; 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.data.workspace.grid + (:require + [beicon.core :as rx] + [potok.core :as ptk] + [uxbox.common.data :as d] + [uxbox.main.data.workspace.common :as dwc])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Grid +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defonce ^:private default-square-params + {:size 16 + :color {:value "#59B9E2" + :opacity 0.9}}) + +(defonce ^:private default-layout-params + {:size 12 + :type :stretch + :item-length nil + :gutter 8 + :margin 0 + :color {:value "#DE4762" + :opacity 0.1}}) + +(defonce default-grid-params + {:square default-square-params + :column default-layout-params + :row default-layout-params}) + +(defn add-frame-grid [frame-id] + (ptk/reify ::set-frame-grid + dwc/IBatchedChange + ptk/UpdateEvent + (update [_ state] + (let [pid (:current-page-id state) + default-params (or + (get-in state [:workspace-data pid :options :saved-grids :square]) + (:square default-grid-params)) + prop-path [:workspace-data pid :objects frame-id :grids] + grid {:type :square + :params default-params + :display true}] + (-> state + (update-in prop-path #(if (nil? %) [grid] (conj % grid)))))))) + +(defn remove-frame-grid [frame-id index] + (ptk/reify ::set-frame-grid + dwc/IBatchedChange + ptk/UpdateEvent + (update [_ state] + (let [pid (:current-page-id state)] + (-> state + (update-in [:workspace-data pid :objects frame-id :grids] #(d/remove-at-index % index))))))) + +(defn set-frame-grid [frame-id index data] + (ptk/reify ::set-frame-grid + dwc/IBatchedChange + ptk/UpdateEvent + (update [_ state] + (let [pid (:current-page-id state)] + (-> + state + (assoc-in [:workspace-data pid :objects frame-id :grids index] data)))))) + +(defn set-default-grid [type params] + (ptk/reify ::set-default-grid + ptk/WatchEvent + (watch [_ state stream] + (rx/of (dwc/commit-changes [{:type :set-option + :option [:saved-grids type] + :value params}] + [] + {:commit-local? true}))))) diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index f0e7aab192..2437461b37 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -68,9 +68,18 @@ (get-in % [:workspace-data page-id])) (l/derived st/state))) +(def workspace-page-options + (l/derived :options workspace-data)) + +(def workspace-saved-grids + (l/derived :saved-grids workspace-page-options)) + (def workspace-objects (l/derived :objects workspace-data)) +(def workspace-frames + (l/derived cp/select-frames workspace-objects)) + (defn object-by-id [id] (letfn [(selector [state] diff --git a/frontend/src/uxbox/main/snap.cljs b/frontend/src/uxbox/main/snap.cljs index 70c0be0c9e..8e5c2c85c8 100644 --- a/frontend/src/uxbox/main/snap.cljs +++ b/frontend/src/uxbox/main/snap.cljs @@ -18,10 +18,10 @@ (def ^:private snap-accuracy 5) -(defn- remove-from-snap-points [ids-to-remove] +(defn- remove-from-snap-points [remove-id?] (fn [query-result] (->> query-result - (map (fn [[value data]] [value (remove (comp ids-to-remove second) data)])) + (map (fn [[value data]] [value (remove (comp remove-id? second) data)])) (filter (fn [[_ data]] (not (empty? data))))))) (defn- flatten-to-points @@ -90,24 +90,32 @@ (defn closest-snap-point [page-id shapes layout point] - (if (layout :dynamic-alignment) - (let [frame-id (snap-frame-id shapes) - filter-shapes (into #{} (map :id shapes))] - (->> (closest-snap page-id frame-id [point] filter-shapes) - (rx/map #(gpt/add point %)))) - (rx/of point))) + (let [frame-id (snap-frame-id shapes) + filter-shapes (into #{} (map :id shapes)) + filter-shapes (fn [id] (if (= id :layout) + (or (not (contains? layout :display-grid)) + (not (contains? layout :snap-grid))) + (or (filter-shapes id) + (not (contains? layout :dynamic-alignment)))))] + (->> (closest-snap page-id frame-id [point] filter-shapes) + (rx/map #(gpt/add point %)) + (rx/map gpt/round)))) (defn closest-snap-move [page-id shapes layout movev] - (if (layout :dynamic-alignment) - (let [frame-id (snap-frame-id shapes) - filter-shapes (into #{} (map :id shapes)) - shapes-points (->> shapes - ;; Unroll all the possible snap-points - (mapcat (partial sp/shape-snap-points)) + (let [frame-id (snap-frame-id shapes) + filter-shapes (into #{} (map :id shapes)) + filter-shapes (fn [id] (if (= id :layout) + (or (not (contains? layout :display-grid)) + (not (contains? layout :snap-grid))) + (or (filter-shapes id) + (not (contains? layout :dynamic-alignment))))) + shapes-points (->> shapes + ;; Unroll all the possible snap-points + (mapcat (partial sp/shape-snap-points)) - ;; Move the points in the translation vector - (map #(gpt/add % movev)))] - (->> (closest-snap page-id frame-id shapes-points filter-shapes) - (rx/map #(gpt/add movev %)))) - (rx/of movev))) + ;; Move the points in the translation vector + (map #(gpt/add % movev)))] + (->> (closest-snap page-id frame-id shapes-points filter-shapes) + (rx/map #(gpt/add movev %)) + (rx/map gpt/round)))) diff --git a/frontend/src/uxbox/main/ui/colorpicker.cljs b/frontend/src/uxbox/main/ui/colorpicker.cljs index 308cee66cd..ce7b333364 100644 --- a/frontend/src/uxbox/main/ui/colorpicker.cljs +++ b/frontend/src/uxbox/main/ui/colorpicker.cljs @@ -25,7 +25,6 @@ :onChangeComplete on-change-complete :style {:box-shadow "none"}}])) - (def most-used-colors (letfn [(selector [{:keys [objects]}] (as-> {} $ diff --git a/frontend/src/uxbox/main/ui/components/context_menu.cljs b/frontend/src/uxbox/main/ui/components/context_menu.cljs index a9ae18334c..df6fa4f691 100644 --- a/frontend/src/uxbox/main/ui/components/context_menu.cljs +++ b/frontend/src/uxbox/main/ui/components/context_menu.cljs @@ -1,3 +1,12 @@ +;; 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.components.context-menu (:require [rumext.alpha :as mf] diff --git a/frontend/src/uxbox/main/ui/components/editable_label.cljs b/frontend/src/uxbox/main/ui/components/editable_label.cljs index 8ce7c65079..07b36cf267 100644 --- a/frontend/src/uxbox/main/ui/components/editable_label.cljs +++ b/frontend/src/uxbox/main/ui/components/editable_label.cljs @@ -1,3 +1,12 @@ +;; 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.components.editable-label (:require [rumext.alpha :as mf] diff --git a/frontend/src/uxbox/main/ui/components/editable_select.cljs b/frontend/src/uxbox/main/ui/components/editable_select.cljs new file mode 100644 index 0000000000..5d2994d4c9 --- /dev/null +++ b/frontend/src/uxbox/main/ui/components/editable_select.cljs @@ -0,0 +1,70 @@ +;; 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.components.editable-select + (:require + [rumext.alpha :as mf] + [uxbox.common.uuid :as uuid] + [uxbox.common.data :as d] + [uxbox.util.dom :as dom] + [uxbox.main.ui.icons :as i] + [uxbox.main.ui.components.dropdown :refer [dropdown]])) + +(mf/defc editable-select [{:keys [value type options class on-change]}] + (let [state (mf/use-state {:id (uuid/next) + :is-open? false + :current-value value}) + open-dropdown #(swap! state assoc :is-open? true) + close-dropdown #(swap! state assoc :is-open? false) + + select-item (fn [value] + (fn [event] + (swap! state assoc :current-value value) + (when on-change (on-change value)))) + + as-key-value (fn [item] (if (map? item) [(:value item) (:label item)] [item item])) + + labels-map (into {} (->> options (map as-key-value))) + + value->label (fn [value] (get labels-map value value)) + + handle-change-input (fn [event] + (let [value (-> event dom/get-target dom/get-value) + value (or (d/parse-integer value) value)] + (swap! state assoc :current-value value) + (when on-change (on-change value))))] + + (mf/use-effect + (mf/deps value) + #(reset! state {:current-value value})) + + (mf/use-effect + (mf/deps options) + #(reset! state {:is-open? false + :current-value value})) + + [:div.editable-select {:class class} + [:input.input-text {:value (or (-> @state :current-value value->label) "") + :on-change handle-change-input + :type type}] + [:span.dropdown-button {:on-click open-dropdown} i/arrow-down] + + [:& dropdown {:show (get @state :is-open? false) + :on-close close-dropdown} + [:ul.custom-select-dropdown + (for [[index item] (map-indexed vector options)] + (cond + (= :separator item) [:hr {:key (str (:id @state) "-" index)}] + :else (let [[value label] (as-key-value item)] + [:li.checked-element + {:key (str (:id @state) "-" index) + :class (when (= value (-> @state :current-value)) "is-selected") + :on-click (select-item value)} + [:span.check-icon i/tick] + [:span label]])))]]])) diff --git a/frontend/src/uxbox/main/ui/components/select.cljs b/frontend/src/uxbox/main/ui/components/select.cljs new file mode 100644 index 0000000000..fd733ea465 --- /dev/null +++ b/frontend/src/uxbox/main/ui/components/select.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.components.select + (:require + [rumext.alpha :as mf] + [uxbox.common.uuid :as uuid] + [uxbox.main.ui.icons :as i] + [uxbox.main.ui.components.dropdown :refer [dropdown]])) + +(mf/defc select [{:keys [default-value options class on-change]}] + (let [state (mf/use-state {:id (uuid/next) + :is-open? false + :current-value default-value}) + open-dropdown #(swap! state assoc :is-open? true) + close-dropdown #(swap! state assoc :is-open? false) + select-item (fn [value] (fn [event] + (swap! state assoc :current-value value) + (when on-change (on-change value)))) + as-key-value (fn [item] (if (map? item) [(:value item) (:label item)] [item item])) + value->label (into {} (->> options + (map as-key-value))) ] + + (mf/use-effect + (mf/deps options) + #(reset! state {:is-open? false + :current-value default-value})) + + [:div.custom-select {:on-click open-dropdown + :class class} + [:span (-> @state :current-value value->label)] + [:span.dropdown-button i/arrow-down] + [:& dropdown {:show (:is-open? @state) + :on-close close-dropdown} + [:ul.custom-select-dropdown + (for [[index item] (map-indexed vector options)] + (cond + (= :separator item) [:hr {:key (str (:id @state) "-" index)}] + :else (let [[value label] (as-key-value item)] + [:li.checked-element + {:key (str (:id @state) "-" index) + :class (when (= value (-> @state :current-value)) "is-selected") + :on-click (select-item value)} + [:span.check-icon i/tick] + [:span label]])))]]])) diff --git a/frontend/src/uxbox/main/ui/components/tab_container.cljs b/frontend/src/uxbox/main/ui/components/tab_container.cljs index 78accbbcc9..29292600b0 100644 --- a/frontend/src/uxbox/main/ui/components/tab_container.cljs +++ b/frontend/src/uxbox/main/ui/components/tab_container.cljs @@ -1,3 +1,12 @@ +;; 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.components.tab-container (:require [rumext.alpha :as mf])) diff --git a/frontend/src/uxbox/main/ui/icons.cljs b/frontend/src/uxbox/main/ui/icons.cljs index b8d8efcc03..85f11ea14b 100644 --- a/frontend/src/uxbox/main/ui/icons.cljs +++ b/frontend/src/uxbox/main/ui/icons.cljs @@ -110,6 +110,7 @@ (def unlock (icon-xref :unlock)) (def uppercase (icon-xref :uppercase)) (def user (icon-xref :user)) +(def tick (icon-xref :tick)) (def loader-pencil (mf/html diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index ac12a0887a..f33c86e769 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -73,7 +73,8 @@ [:& viewport {:page page :key (:id page) :file file - :local local}]]] + :local local + :layout layout}]]] [:& left-toolbar {:page page :layout layout}] diff --git a/frontend/src/uxbox/main/ui/workspace/frame_grid.cljs b/frontend/src/uxbox/main/ui/workspace/frame_grid.cljs new file mode 100644 index 0000000000..1707d03a5d --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/frame_grid.cljs @@ -0,0 +1,76 @@ +;; 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.frame-grid + (:require + [rumext.alpha :as mf] + [uxbox.main.refs :as refs] + [uxbox.common.pages :as cp] + [uxbox.util.geom.shapes :as gsh] + [uxbox.util.geom.grid :as gg])) + +(mf/defc square-grid [{:keys [frame zoom grid] :as props}] + (let [{:keys [color size] :as params} (-> grid :params) + {color-value :value color-opacity :opacity} (-> grid :params :color) + {frame-width :width frame-height :height :keys [x y]} frame] + (when (> size 0) + [:g.grid + [:* + (for [xs (range size frame-width size)] + [:line {:key (str (:id frame) "-y-" xs) + :x1 (+ x xs) + :y1 y + :x2 (+ x xs) + :y2 (+ y frame-height) + :style {:stroke color-value + :stroke-opacity color-opacity + :stroke-width (str (/ 1 zoom))}}]) + (for [ys (range size frame-height size)] + [:line {:key (str (:id frame) "-x-" ys) + :x1 x + :y1 (+ y ys) + :x2 (+ x frame-width) + :y2 (+ y ys) + :style {:stroke color-value + :stroke-opacity color-opacity + :stroke-width (str (/ 1 zoom))}}])]]))) + +(mf/defc layout-grid [{:keys [key frame zoom grid]}] + (let [{color-value :value color-opacity :opacity} (-> grid :params :color)] + [:g.grid + (for [{:keys [x y width height]} (gg/grid-areas frame grid)] + [:rect {:key (str key "-" x "-" y) + :x x + :y y + :width width + :height height + :style {:fill color-value + :opacity color-opacity}}])])) + +(mf/defc grid-display-frame [{:keys [frame zoom]}] + (let [grids (:grids frame)] + (for [[index {:keys [type display] :as grid}] (map-indexed vector grids)] + (let [props #js {:key (str (:id frame) "-grid-" index) + :frame frame + :zoom zoom + :grid grid}] + (when display + (case type + :square [:> square-grid props] + :column [:> layout-grid props] + :row [:> layout-grid props])))))) + + +(mf/defc frame-grid [{:keys [zoom]}] + (let [frames (mf/deref refs/workspace-frames)] + [:g.grid-display {:style {:pointer-events "none"}} + (for [frame frames] + [:& grid-display-frame {:key (str "grid-" (:id frame)) + :zoom zoom + :frame (gsh/transform-shape frame)}])])) diff --git a/frontend/src/uxbox/main/ui/workspace/grid.cljs b/frontend/src/uxbox/main/ui/workspace/grid.cljs deleted file mode 100644 index 31b1c30aab..0000000000 --- a/frontend/src/uxbox/main/ui/workspace/grid.cljs +++ /dev/null @@ -1,43 +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) 2020 UXBOX Labs SL - -(ns uxbox.main.ui.workspace.grid - (:require - [cuerdas.core :as str] - [okulary.core :as l] - [rumext.alpha :as mf] - [uxbox.main.constants :as c] - [uxbox.main.refs :as refs])) - -;; --- Grid (Component) - -(def options-iref - (l/derived :options refs/workspace-data)) - -(mf/defc grid - {:wrap [mf/memo]} - [props] - (let [options (mf/deref options-iref) - width (:grid-x options 10) - height (:grid-y options 10) - color (:grid-color options "#cccccc")] - [:g.grid - [:defs - [:pattern {:id "grid-pattern" - :x "0" :y "0" - :width width :height height - :patternUnits "userSpaceOnUse"} - [:path {:d (str/format "M 0 %s L %s %s L %s 0" height width height width) - :fill "transparent" - :stroke color}]]] - [:rect {:style {:pointer-events "none"} - :x 0 :y 0 - :width "100%" - :height "100%" - :fill "url(#grid-pattern)"}]])) diff --git a/frontend/src/uxbox/main/ui/workspace/header.cljs b/frontend/src/uxbox/main/ui/workspace/header.cljs index b01d66bba0..fb7ab40b47 100644 --- a/frontend/src/uxbox/main/ui/workspace/header.cljs +++ b/frontend/src/uxbox/main/ui/workspace/header.cljs @@ -21,7 +21,7 @@ [uxbox.main.ui.workspace.images :refer [import-image-modal]] [uxbox.main.ui.components.dropdown :refer [dropdown]] [uxbox.main.ui.workspace.presence :as presence] - [uxbox.util.i18n :as i18n :refer [tr t]] + [uxbox.util.i18n :as i18n :refer [t]] [uxbox.util.data :refer [classnames]] [uxbox.util.math :as mth] [uxbox.util.router :as rt])) @@ -72,42 +72,42 @@ :on-close #(reset! show-menu? false)} [:ul.menu [:li {:on-click #(st/emit! (dw/toggle-layout-flag :rules))} - [:span i/ruler] [:span (if (contains? layout :rules) (t locale "workspace.header.menu.hide-rules") (t locale "workspace.header.menu.show-rules"))]] - [:li {:on-click #(st/emit! (dw/toggle-layout-flag :grid))} - [:span i/grid] + [:li {:on-click #(st/emit! (dw/toggle-layout-flag :display-grid))} [:span - (if (contains? layout :grid) + (if (contains? layout :display-grid) (t locale "workspace.header.menu.hide-grid") (t locale "workspace.header.menu.show-grid"))]] + [:li {:on-click #(st/emit! (dw/toggle-layout-flag :snap-grid))} + [:span + (if (contains? layout :snap-grid) + (t locale "workspace.header.menu.disable-snap-grid") + (t locale "workspace.header.menu.enable-snap-grid"))]] + [:li {:on-click #(st/emit! (dw/toggle-layout-flag :sitemap :layers))} - [:span i/layers] [:span (if (or (contains? layout :sitemap) (contains? layout :layers)) (t locale "workspace.header.menu.hide-layers") (t locale "workspace.header.menu.show-layers"))]] [:li {:on-click #(st/emit! (dw/toggle-layout-flag :colorpalette))} - [:span i/palette] [:span (if (contains? layout :colorpalette) (t locale "workspace.header.menu.hide-palette") (t locale "workspace.header.menu.show-palette"))]] [:li {:on-click #(st/emit! (dw/toggle-layout-flag :libraries))} - [:span i/icon-set] [:span (if (contains? layout :libraries) (t locale "workspace.header.menu.hide-libraries") (t locale "workspace.header.menu.show-libraries"))]] [:li {:on-click #(st/emit! (dw/toggle-layout-flag :dynamic-alignment))} - [:span i/shape-halign-left] [:span (if (contains? layout :dynamic-alignment) (t locale "workspace.header.menu.disable-dynamic-alignment") diff --git a/frontend/src/uxbox/main/ui/workspace/ruler.cljs b/frontend/src/uxbox/main/ui/workspace/ruler.cljs deleted file mode 100644 index e4ec96db03..0000000000 --- a/frontend/src/uxbox/main/ui/workspace/ruler.cljs +++ /dev/null @@ -1,79 +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) 2015-2017 Juan de la Cruz -;; Copyright (c) 2015-2019 Andrey Antukh - -(ns uxbox.main.ui.workspace.ruler - (:require - [rumext.alpha :as mf] - [uxbox.main.constants :as c] - [uxbox.main.data.workspace :as udw] - [uxbox.main.store :as st] - [uxbox.util.dom :as dom] - [uxbox.util.geom.point :as gpt] - [uxbox.util.math :as mth])) - -(mf/defc ruler-text - [{:keys [zoom ruler] :as props}] - #_(let [{:keys [start end]} ruler - distance (-> (gpt/distance (gpt/divide end zoom) - (gpt/divide start zoom)) - (mth/precision 2)) - angle (-> (gpt/angle end start) - (mth/precision 2)) - transform1 (str "translate(" (+ (:x end) 35) "," (- (:y end) 10) ")") - transform2 (str "translate(" (+ (:x end) 25) "," (- (:y end) 30) ")")] - [:g - [:rect {:fill "black" - :fill-opacity "0.4" - :rx "3" - :ry "3" - :width "90" - :height "50" - :transform transform2}] - [:text {:transform transform1 - :fill "white"} - [:tspan {:x "0"} - (str distance " px")] - [:tspan {:x "0" :y "20"} - (str angle "°")]]])) - -(mf/defc ruler-line - [{:keys [zoom ruler] :as props}] - #_(let [{:keys [start end]} ruler] - [:line {:x1 (:x start) - :y1 (:y start) - :x2 (:x end) - :y2 (:y end) - :style {:cursor "cell"} - :stroke-width "1" - :stroke "red"}])) - -(mf/defc ruler - [{:keys [ruler zoom] :as props}] - #_(letfn [(on-mouse-down [event] - (dom/stop-propagation event) - (st/emit! :interrupt - (udw/assign-cursor-tooltip nil) - (udw/start-ruler))) - (on-mouse-up [event] - (dom/stop-propagation event) - (st/emit! :interrupt)) - (on-unmount [] - (st/emit! :interrupt - (udw/clear-ruler)))] - (mf/use-effect (constantly on-unmount)) - [:svg {:on-mouse-down on-mouse-down - :on-mouse-up on-mouse-up} - [:rect {:style {:fill "transparent" - :stroke "transparent" - :cursor "cell"} - :width c/viewport-width - :height c/viewport-height}] - (when ruler - [:g - [:& ruler-line {:ruler ruler}] - [:& ruler-text {:ruler ruler :zoom zoom}]])])) - diff --git a/frontend/src/uxbox/main/ui/workspace/shapes/frame.cljs b/frontend/src/uxbox/main/ui/workspace/shapes/frame.cljs index c4471b231b..e39abfe6c1 100644 --- a/frontend/src/uxbox/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/uxbox/main/ui/workspace/shapes/frame.cljs @@ -64,15 +64,14 @@ on-context-menu (mf/use-callback (mf/deps shape) #(common/on-context-menu % shape)) - + shape (geom/transform-shape 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)) + label-pos (gpt/point x (- y (/ 10 zoom))) on-double-click (mf/use-callback @@ -104,7 +103,6 @@ :on-click on-double-click} (:name shape)] [:& frame-shape - {:shape (geom/transform-shape shape) + {:shape shape :childs childs}]]))))) - diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/frame.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/frame.cljs index d0ad8bec07..1c49bb35f1 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/frame.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/frame.cljs @@ -12,16 +12,17 @@ (:require [rumext.alpha :as mf] [uxbox.common.data :as d] - [uxbox.main.ui.icons :as i] - [uxbox.main.data.workspace :as udw] - [uxbox.main.store :as st] - [uxbox.main.ui.components.dropdown :refer [dropdown]] - [uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]] - [uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]] [uxbox.util.dom :as dom] [uxbox.util.geom.point :as gpt] [uxbox.util.i18n :refer [tr]] - [uxbox.util.math :as math])) + [uxbox.util.math :as math] + [uxbox.main.store :as st] + [uxbox.main.data.workspace :as udw] + [uxbox.main.ui.icons :as i] + [uxbox.main.ui.components.dropdown :refer [dropdown]] + [uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]] + [uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]] + [uxbox.main.ui.workspace.sidebar.options.frame-grid :refer [frame-grid]])) (declare +size-presets+) @@ -37,10 +38,10 @@ on-orientation-clicked (fn [orientation] - (let [width (:width shape) - height (:height shape) - new-width (if (= orientation :horiz) (max width height) (min width height)) - new-height (if (= orientation :horiz) (min width height) (max width height))] + (let [width (:width shape) + height (:height shape) + new-width (if (= orientation :horiz) (max width height) (min width height)) + new-height (if (= orientation :horiz) (min width height) (max width height))] (st/emit! (udw/update-rect-dimensions (:id shape) :width new-width) (udw/update-rect-dimensions (:id shape) :height new-height)))) @@ -79,14 +80,14 @@ [:div.element-set-content [:div.row-flex - [:div.custom-select.flex-grow {:on-click #(reset! show-presets-dropdown? true)} + [:div.presets.custom-select.flex-grow {:on-click #(reset! show-presets-dropdown? true)} [:span (tr "workspace.options.size-presets")] [:span.dropdown-button i/arrow-down] - [:& dropdown {:show @show-presets-dropdown? - :on-close #(reset! show-presets-dropdown? false)} - [:ul.custom-select-dropdown - (for [size-preset +size-presets+] - (if-not (:width size-preset) + [:& dropdown {:show @show-presets-dropdown? + :on-close #(reset! show-presets-dropdown? false)} + [:ul.custom-select-dropdown + (for [size-preset +size-presets+] + (if-not (:width size-preset) [:li.dropdown-label {:key (:name size-preset)} [:span (:name size-preset)]] [:li {:key (:name size-preset) @@ -202,4 +203,6 @@ [:div [:& measures-menu {:shape shape}] [:& fill-menu {:shape shape}] - [:& stroke-menu {:shape shape}]]) + [:& stroke-menu {:shape shape}] + [:& frame-grid {:shape shape}]]) + diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs new file mode 100644 index 0000000000..5e2fc522f5 --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs @@ -0,0 +1,233 @@ +;; 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.sidebar.options.frame-grid + (:require + [rumext.alpha :as mf] + [uxbox.util.dom :as dom] + [uxbox.util.data :as d] + [uxbox.util.math :as mth] + [uxbox.common.data :refer [parse-integer]] + [uxbox.main.store :as st] + [uxbox.main.refs :as refs] + [uxbox.main.data.workspace.grid :as dw] + [uxbox.util.geom.grid :as gg] + [uxbox.main.ui.icons :as i] + [uxbox.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] + [uxbox.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row]] + [uxbox.main.ui.components.select :refer [select]] + [uxbox.main.ui.components.editable-select :refer [editable-select]] + [uxbox.main.ui.components.dropdown :refer [dropdown]] + [uxbox.util.i18n :as i18n :refer [tr t]])) + +(mf/defc advanced-options [{:keys [visible? on-close children]}] + (when visible? + [:* + [:div.focus-overlay {:on-click #(when on-close + (do + (dom/stop-propagation %) + (on-close)))}] + [:div.advanced-options {} + children]])) + +(def ^:private size-options + [{:value :auto :label (tr "workspace.options.grid.auto")} + :separator + 18 12 10 8 6 4 3 2]) + +(mf/defc grid-options [{:keys [frame grid default-grid-params on-change on-remove on-save-grid]}] + (let [locale (i18n/use-locale) + state (mf/use-state {:show-advanced-options false + :changes {}}) + {:keys [type display params] :as grid} (d/deep-merge grid (:changes @state)) + + toggle-advanced-options #(swap! state update :show-advanced-options not) + + emit-changes! + (fn [update-fn] + (swap! state update :changes update-fn) + (when on-change (on-change (d/deep-merge grid (-> @state :changes update-fn))))) + + handle-toggle-visibility + (fn [event] + (emit-changes! (fn [changes] (update changes :display #(if (nil? %) false (not %)))))) + + handle-remove-grid + (fn [event] + (when on-remove (on-remove))) + + handle-change-type + (fn [type] + (let [defaults (type default-grid-params) + keys (keys defaults) + params (->> @state :changes params (select-keys keys) (merge defaults)) + to-merge {:type type :params params}] + (emit-changes! #(d/deep-merge % to-merge)))) + + handle-change + (fn [& keys] + (fn [value] + (emit-changes! #(assoc-in % keys value)))) + + handle-change-event + (fn [& keys] + (fn [event] + (let [change-fn (apply handle-change keys)] + (-> event dom/get-target dom/get-value parse-integer change-fn)))) + + handle-change-size + (fn [size] + (let [grid (d/deep-merge grid (:changes @state)) + {:keys [margin gutter item-length]} (:params grid) + frame-length (if (= :column (:type grid)) (:width frame) (:height frame)) + item-length (if (or (nil? size) (= :auto size)) + (-> (gg/calculate-default-item-length frame-length margin gutter) + (mth/round)) + item-length)] + (emit-changes! #(-> % + (assoc-in [:params :size] size) + (assoc-in [:params :item-length] item-length))))) + + handle-change-item-length + (fn [item-length] + (let [{:keys [margin gutter size]} (->> @state :changes :params (d/deep-merge (:params grid))) + size (if (and (nil? item-length) (or (nil? size) (= :auto size))) 12 size)] + (emit-changes! #(-> % + (assoc-in [:params :size] size) + (assoc-in [:params :item-length] item-length))))) + + handle-use-default + (fn [] + (emit-changes! #(hash-map :params ((:type grid) default-grid-params)))) + + handle-set-as-default + (fn [] + (let [current-grid (d/deep-merge grid (-> @state :changes))] + (on-save-grid current-grid))) + + is-default (= (->> @state :changes (d/deep-merge grid) :params) + (->> grid :type default-grid-params))] + + [:div.grid-option + [:div.grid-option-main + [:button.custom-button {:class (when (:show-advanced-options @state) "is-active") + :on-click toggle-advanced-options} i/actions] + + [:& select {:class "flex-grow" + :default-value type + :options [{:value :square :label (t locale "workspace.options.grid.square")} + {:value :column :label (t locale "workspace.options.grid.column")} + {:value :row :label (t locale "workspace.options.grid.row")}] + :on-change handle-change-type}] + + (if (= type :square) + [:div.input-element.pixels + [:input.input-text {:type "number" + :min "1" + :no-validate true + :value (:size params) + :on-change (handle-change-event :params :size)}]] + [:& editable-select {:value (:size params) + :type (when (number? (:size params)) "number" ) + :class "input-option" + :options size-options + :on-change handle-change-size}]) + + [:div.grid-option-main-actions + [:button.custom-button {:on-click handle-toggle-visibility} (if display i/eye i/eye-closed)] + [:button.custom-button {:on-click handle-remove-grid} i/trash]]] + + [:& advanced-options {:visible? (:show-advanced-options @state) + :on-close toggle-advanced-options} + (when (= :square type) + [:& input-row {:label (t locale "workspace.options.grid.params.size") + :class "pixels" + :min 1 + :value (:size params) + :on-change (handle-change :params :size)}]) + + (when (= :row type) + [:& input-row {:label (t locale "workspace.options.grid.params.rows") + :type :editable-select + :options size-options + :value (:size params) + :min 1 + :on-change handle-change-size}]) + + (when (= :column type) + [:& input-row {:label (t locale "workspace.options.grid.params.columns") + :type :editable-select + :options size-options + :value (:size params) + :min 1 + :on-change handle-change-size}]) + + (when (#{:row :column} type) + [:& input-row {:label (t locale "workspace.options.grid.params.type") + :type :select + :options [{:value :stretch :label (t locale "workspace.options.grid.params.type.stretch")} + {:value :left :label (t locale "workspace.options.grid.params.type.left")} + {:value :center :label (t locale "workspace.options.grid.params.type.center")} + {:value :right :label (t locale "workspace.options.grid.params.type.right")}] + :value (:type params) + :on-change (handle-change :params :type)}]) + + (when (#{:row :column} type) + [:& input-row {:label (if (= :row type) + (t locale "workspace.options.grid.params.height") + (t locale "workspace.options.grid.params.width")) + :class "pixels" + :value (or (:item-length params) "") + :on-change handle-change-item-length}]) + + (when (#{:row :column} type) + [:* + [:& input-row {:label (t locale "workspace.options.grid.params.gutter") + :class "pixels" + :value (:gutter params) + :min 0 + :on-change (handle-change :params :gutter)}] + [:& input-row {:label (t locale "workspace.options.grid.params.margin") + :class "pixels" + :min 0 + :value (:margin params) + :on-change (handle-change :params :margin)}]]) + + [:& color-row {:value (:color params) + :on-change (handle-change :params :color)}] + [:div.row-flex + [:button.btn-options {:disabled is-default + :on-click handle-use-default} (t locale "workspace.options.grid.params.use-default")] + [:button.btn-options {:disabled is-default + :on-click handle-set-as-default} (t locale "workspace.options.grid.params.set-default")]]]])) + +(mf/defc frame-grid [{:keys [shape]}] + (let [locale (i18n/use-locale) + id (:id shape) + default-grid-params (merge dw/default-grid-params (mf/deref refs/workspace-saved-grids)) + handle-create-grid #(st/emit! (dw/add-frame-grid id)) + handle-remove-grid (fn [index] #(st/emit! (dw/remove-frame-grid id index))) + handle-edit-grid (fn [index] #(st/emit! (dw/set-frame-grid id index %))) + handle-save-grid (fn [grid] (st/emit! (dw/set-default-grid (:type grid) (:params grid))))] + [:div.element-set + [:div.element-set-title + [:span (t locale "workspace.options.grid.title")] + [:div.add-page {:on-click handle-create-grid} i/close]] + + (when (not (empty? (:grids shape))) + [:div.element-set-content + (for [[index grid] (map-indexed vector (:grids shape))] + [:& grid-options {:key (str (:id shape) "-" index) + :grid grid + :default-grid-params default-grid-params + :frame shape + :on-change (handle-edit-grid index) + :on-remove (handle-remove-grid index) + :on-save-grid handle-save-grid}])])])) + diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/page.cljs index 5eb3bcb439..98b3c22520 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/page.cljs @@ -10,95 +10,14 @@ (ns uxbox.main.ui.workspace.sidebar.options.page "Page options menu entries." (:require - [cuerdas.core :as str] [rumext.alpha :as mf] [okulary.core :as l] - [uxbox.common.data :as d] - [uxbox.main.ui.icons :as i] - [uxbox.main.data.workspace :as dw] - [uxbox.main.refs :as refs] - [uxbox.main.store :as st] - [uxbox.main.ui.modal :as modal] - [uxbox.main.ui.workspace.colorpicker :refer [colorpicker-modal]] - [uxbox.util.dom :as dom] - [uxbox.util.i18n :refer [tr]])) - -(def default-options - "Default data for page metadata." - {:grid-x 10 - :grid-y 10 - :grid-color "#cccccc"}) + [uxbox.main.refs :as refs])) (def options-iref (l/derived :options refs/workspace-data)) -(mf/defc grid-options - {:wrap [mf/memo]} - [props] - (let [options (->> (mf/deref options-iref) - (merge default-options)) - on-x-change - (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/parse-integer 0))] - (st/emit! (dw/update-options {:grid-x value})))) - - on-y-change - (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/parse-integer 0))] - (st/emit! (dw/update-options {:grid-y value})))) - - change-color - (fn [color] - (st/emit! (dw/update-options {:grid-color color}))) - - on-color-input-change - (fn [event] - (let [input (dom/get-target event) - value (dom/get-value input)] - (when (dom/valid? input) - (change-color value)))) - - show-color-picker - (fn [event] - (let [x (.-clientX event) - y (.-clientY event) - props {:x x :y y - :transparent? true - :default "#cccccc" - :attr :grid-color - :on-change change-color}] - (modal/show! colorpicker-modal props)))] - [:div.element-set - [:div.element-set-title (tr "workspace.options.grid-options")] - [:div.element-set-content - [:div.row-flex - [:span.element-set-subtitle (tr "workspace.options.size")] - [:div.input-element.pixels - [:input.input-text {:type "number" - :value (:grid-x options) - :on-change on-x-change}]] - [:div.input-element.pixels - [:input.input-text {:type "number" - :value (:grid-y options) - :on-change on-y-change}]]] - [:div.row-flex.color-data - [:span.element-set-subtitle (tr "workspace.options.color")] - [:span.color-th {:style {:background-color (:grid-color options)} - :on-click show-color-picker}] - [:div.color-info - [:input {:default-value (:grid-color options) - :ref (fn [el] - (when el - (set! (.-value el) (:grid-color options)))) - :pattern "^#(?:[0-9a-fA-F]{3}){1,2}$" - :on-change on-color-input-change}]]]]])) - (mf/defc options - [{:keys [page] :as props}] - [:div - [:& grid-options {:page page}]]) + ;; TODO: Define properties for page + [{:keys [page] :as props}]) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs new file mode 100644 index 0000000000..b5f0d7b314 --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -0,0 +1,96 @@ +;; 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.sidebar.options.rows.color-row + (:require + [rumext.alpha :as mf] + [uxbox.util.math :as math] + [uxbox.util.dom :as dom] + [uxbox.main.ui.modal :as modal] + [uxbox.main.ui.workspace.colorpicker :refer [colorpicker-modal]] + [uxbox.common.data :as d])) + +(defn color-picker-callback [color handle-change-color] + (fn [event] + (let [x (.-clientX event) + y (.-clientY event) + props {:x x + :y y + :on-change handle-change-color + :value (:value color) + :transparent? true}] + (modal/show! colorpicker-modal props)))) + +(defn opacity->string [opacity] + (str (-> opacity + (d/coalesce 1) + (* 100) + (math/round)))) + +(defn string->opacity [opacity-str] + (-> opacity-str + (d/parse-integer 1) + (/ 100))) + +(mf/defc color-row [{:keys [value on-change]}] + (let [value (or value {:value "#FFFFFF" :opacity 1}) + state (mf/use-state value) + change-color (fn [color] + (let [update-color (fn [state] (assoc state :value color))] + (swap! state update-color) + (when on-change (on-change (update-color @state))))) + + change-opacity (fn [opacity] + (let [update-opacity (fn [state] (assoc state :opacity opacity))] + (swap! state update-opacity) + (when on-change (on-change (update-opacity @state))))) + + handle-pick-color (fn [color] + (change-color color)) + + handle-input-color-change (fn [event] + (let [target (dom/get-target event) + value (dom/get-value target)] + (when (dom/valid? target) + (change-color value)))) + handle-opacity-change (fn [event] + (-> event + dom/get-target + dom/get-value + string->opacity + change-opacity))] + + (mf/use-effect + (mf/deps value) + #(reset! state value)) + + [:div.row-flex.color-data + [:span.color-th + {:style {:background-color (-> @state :value)} + :on-click (color-picker-callback @state handle-pick-color)}] + + [:div.color-info + [:input {:value (-> @state :value) + :pattern "^#(?:[0-9a-fA-F]{3}){1,2}$" + :on-change handle-input-color-change}]] + + [:div.input-element.percentail + [:input.input-text {:type "number" + :value (-> @state :opacity opacity->string) + :on-change handle-opacity-change + :min "0" + :max "100"}]] + + [:input.slidebar {:type "range" + :min "0" + :max "100" + :value (-> @state :opacity opacity->string) + :step "1" + :on-change handle-opacity-change}]])) + diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/rows/input_row.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/rows/input_row.cljs new file mode 100644 index 0000000000..b6557e5b49 --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/rows/input_row.cljs @@ -0,0 +1,49 @@ +;; 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.sidebar.options.rows.input-row + (:require + [rumext.alpha :as mf] + [uxbox.common.data :as d] + [uxbox.main.ui.components.select :refer [select]] + [uxbox.main.ui.components.editable-select :refer [editable-select]] + [uxbox.util.dom :as dom])) + +(mf/defc input-row [{:keys [label options value class min max on-change type]}] + [:div.row-flex.input-row + [:span.element-set-subtitle label] + [:div.input-element {:class class} + + (case type + :select + [:& select {:default-value value + :class "input-option" + :options options + :on-change on-change}] + :editable-select + [:& editable-select {:value value + :class "input-option" + :options options + :type (when (number? value) "number") + :on-change on-change}] + + (let [handle-change + (fn [event] + (let [value (-> event dom/get-target dom/get-value d/parse-integer)] + (when (and (not (nil? on-change)) + (or (not min) (>= value min)) + (or (not max) (<= value max))) + (on-change value))))] + [:input.input-text + {:placeholder label + :type "number" + :on-change handle-change + :value value}])) + + ]]) diff --git a/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs b/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs index 53e7289af6..a708663078 100644 --- a/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs +++ b/frontend/src/uxbox/main/ui/workspace/snap_feedback.cljs @@ -45,8 +45,15 @@ (mf/defc snap-feedback-points [{:keys [shapes page-id filter-shapes zoom] :as props}] (let [state (mf/use-state []) - subject (mf/use-memo #(rx/subject))] + subject (mf/use-memo #(rx/subject)) + ;; We use sets to store points/lines so there are no points/lines repeated + ;; can cause problems with react keys + snap-points (into #{} (mapcat (fn [[point snaps coord]] + (when (not-empty snaps) (concat [point] snaps))) @state)) + + snap-lines (into #{} (mapcat (fn [[point snaps coord]] + (when (not-empty snaps) (map #(vector point %) snaps))) @state))] (mf/use-effect (fn [] (->> subject @@ -61,31 +68,29 @@ (fn [] (rx/push! subject props))) + [:g.snap-feedback - (for [[point snaps coord] @state] - (if (not-empty snaps) - [:g.point {:key (str "point-" (:x point) "-" (:y point) "-" (name coord))} - [:& snap-point {:key (str "point-" (:x point) "-" (:y point) "-" (name coord)) - :point point - :zoom zoom}] + (for [[from-point to-point] snap-lines] + [:& snap-line {:key (str "line-" (:x from-point) "-" (:y from-point) "-" (:x to-point) "-" (:y to-point) "-") + :snap from-point + :point to-point + :zoom zoom}]) + (for [point snap-points] + [:& snap-point {:key (str "point-" (:x point) "-" (:y point)) + :point point + :zoom zoom}])])) - (for [snap snaps] - [:& snap-point {:key (str "snap-" (:x point) "-" (:y point) "-" (:x snap) "-" (:y snap) "-" (name coord)) - :point snap - :zoom zoom}]) - - (for [snap snaps] - [:& snap-line {:key (str "line-" (:x point) "-" (:y point) "-" (:x snap) "-" (:y snap) "-" (name coord)) - :snap snap - :point point - :zoom zoom}])]))])) - -(mf/defc snap-feedback [{:keys []}] +(mf/defc snap-feedback [{:keys [layout]}] (let [page-id (mf/deref refs/workspace-page-id) selected (mf/deref refs/selected-shapes) selected-shapes (mf/deref (refs/objects-by-id selected)) drawing (mf/deref refs/current-drawing-shape) filter-shapes (mf/deref refs/selected-shapes-with-children) + filter-shapes (fn [id] (if (= id :layout) + (or (not (contains? layout :display-grid)) + (not (contains? layout :snap-grid))) + (or (filter-shapes id) + (not (contains? layout :dynamic-alignment))))) current-transform (mf/deref refs/current-transform) snap-data (mf/deref refs/workspace-snap-data) shapes (if drawing [drawing] selected-shapes) diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index 4baa5c6fc1..8cd0e06627 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -26,11 +26,10 @@ [uxbox.main.ui.workspace.shapes :refer [shape-wrapper frame-wrapper]] [uxbox.main.ui.workspace.shapes.interactions :refer [interactions]] [uxbox.main.ui.workspace.drawarea :refer [draw-area start-drawing]] - [uxbox.main.ui.workspace.grid :refer [grid]] - [uxbox.main.ui.workspace.ruler :refer [ruler]] [uxbox.main.ui.workspace.selection :refer [selection-handlers]] [uxbox.main.ui.workspace.presence :as presence] [uxbox.main.ui.workspace.snap-feedback :refer [snap-feedback]] + [uxbox.main.ui.workspace.frame-grid :refer [frame-grid]] [uxbox.util.math :as mth] [uxbox.util.dom :as dom] [uxbox.util.object :as obj] @@ -127,7 +126,7 @@ :key (:id item)}]))])) (mf/defc viewport - [{:keys [page local] :as props}] + [{:keys [page local layout] :as props}] (let [{:keys [drawing-tool options-mode zoom @@ -354,6 +353,7 @@ ] (mf/use-effect on-mount) + [:svg.viewport {:preserveAspectRatio "xMidYMid meet" :width (:width vport 0) @@ -381,22 +381,18 @@ :zoom zoom :edition edition}]) - (when-let [drawing-shape (:drawing local)] [:& draw-area {:shape drawing-shape :zoom zoom :modifiers (:modifiers local)}]) - [:& snap-feedback] + (when (contains? layout :display-grid) + [:& frame-grid {:zoom zoom}]) - (when (contains? flags :grid) - [:& grid])] + [:& snap-feedback {:layout layout}] - (when tooltip - [:& cursor-tooltip {:zoom zoom :tooltip tooltip}]) - - (when (contains? flags :ruler) - [:& ruler {:zoom zoom :ruler (:ruler local)}]) + (when tooltip + [:& cursor-tooltip {:zoom zoom :tooltip tooltip}])] [:& presence/active-cursors {:page page}] [:& selection-rect {:data (:selrect local)}] diff --git a/frontend/src/uxbox/util/geom/grid.cljs b/frontend/src/uxbox/util/geom/grid.cljs new file mode 100644 index 0000000000..b3706e25ae --- /dev/null +++ b/frontend/src/uxbox/util/geom/grid.cljs @@ -0,0 +1,116 @@ +;; 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.util.geom.grid + (:require + [uxbox.util.math :as mth] + [uxbox.util.geom.point :as gpt])) + +(def ^:private default-items 12) + +(defn calculate-default-item-length + "Calculates the item-length so the default number of items fits inside the frame-length" + [frame-length margin gutter] + (/ (- frame-length (+ margin (- margin gutter)) (* gutter default-items)) default-items)) + +(defn calculate-size + "Calculates the number of rows/columns given the other grid parameters" + [frame-length item-length margin gutter] + (let [item-length (or item-length (calculate-default-item-length frame-length margin gutter)) + frame-length-no-margins (- frame-length (+ margin (- margin gutter)))] + (mth/floor (/ frame-length-no-margins (+ item-length gutter))))) + +(defn- calculate-column-grid + [{:keys [width height x y] :as frame} {:keys [size gutter margin item-length type] :as params}] + (let [size (if (number? size) size (calculate-size width item-length margin gutter)) + parts (/ width size) + item-width (or item-length (+ parts (- gutter) (/ gutter size) (- (/ (* margin 2) size)))) + item-height height + initial-offset (case type + :right (- width (* item-width size) (* gutter (dec size)) margin) + :center (/ (- width (* item-width size) (* gutter (dec size))) 2) + margin) + gutter (if (= :stretch type) (/ (- width (* item-width size) (* margin 2)) (dec size)) gutter) + next-x (fn [cur-val] (+ initial-offset x (* (+ item-width gutter) cur-val))) + next-y (fn [cur-val] y)] + [size item-width item-height next-x next-y])) + +(defn- calculate-row-grid + [{:keys [width height x y] :as frame} {:keys [size gutter margin item-length type] :as params}] + (let [size (if (number? size) size (calculate-size height item-length margin gutter)) + parts (/ height size) + item-width width + item-height (or item-length (+ parts (- gutter) (/ gutter size) (- (/ (* margin 2) size)))) + initial-offset (case type + :right (- height (* item-height size) (* gutter (dec size)) margin) + :center (/ (- height (* item-height size) (* gutter (dec size))) 2) + margin) + gutter (if (= :stretch type) (/ (- height (* item-height size) (* margin 2)) (dec size)) gutter) + next-x (fn [cur-val] x) + next-y (fn [cur-val] (+ initial-offset y (* (+ item-height gutter) cur-val)))] + [size item-width item-height next-x next-y])) + +(defn- calculate-square-grid + [{:keys [width height x y] :as frame} {:keys [size] :as params}] + (let [col-size (quot width size) + row-size (quot height size) + as-row-col (fn [value] [(quot value col-size) (rem value col-size)]) + next-x (fn [cur-val] + (let [[_ col] (as-row-col cur-val)] (+ x (* col size)))) + next-y (fn [cur-val] + (let [[row _] (as-row-col cur-val)] (+ y (* row size))))] + [(* col-size row-size) size size next-x next-y])) + +(defn grid-areas + "Given a frame and the grid parameters returns the areas defined on the grid" + [frame grid] + (let [grid-fn (case (-> grid :type) + :column calculate-column-grid + :row calculate-row-grid + :square calculate-square-grid) + [num-items item-width item-height next-x next-y] (grid-fn frame (-> grid :params))] + (->> + (range 0 num-items) + (map #(hash-map :x (next-x %) + :y (next-y %) + :width item-width + :height item-height))))) + +(defn grid-area-points + [{:keys [x y width height]}] + [(gpt/point x y) + (gpt/point (+ x width) y) + (gpt/point (+ x width) (+ y height)) + (gpt/point x (+ y height))]) + +(defn grid-snap-points + "Returns the snap points for a given grid" + ([shape coord] (mapcat #(grid-snap-points shape % coord) (:grids shape))) + ([shape {:keys [type display params] :as grid} coord] + (when (:display grid) + (case type + :square + (let [{:keys [x y width height]} shape + size (-> params :size)] + (when (> size 0) + (if (= coord :x) + (mapcat #(vector (gpt/point (+ x %) y) + (gpt/point (+ x %) (+ y height))) (range size width size)) + (mapcat #(vector (gpt/point x (+ y %)) + (gpt/point (+ x width) (+ y %))) (range size height size))))) + + :column + (when (= coord :x) + (->> (grid-areas shape grid) + (mapcat grid-area-points))) + + :row + (when (= coord :y) + (->> (grid-areas shape grid) + (mapcat grid-area-points))))))) diff --git a/frontend/src/uxbox/util/geom/point.cljs b/frontend/src/uxbox/util/geom/point.cljs index de1b3744d3..5433334b2d 100644 --- a/frontend/src/uxbox/util/geom/point.cljs +++ b/frontend/src/uxbox/util/geom/point.cljs @@ -151,11 +151,12 @@ (defn round "Change the precision of the point coordinates." - [{:keys [x y] :as p} decimanls] - (assert (point? p)) - (assert (number? decimanls)) - (Point. (mth/precision x decimanls) - (mth/precision y decimanls))) + ([point] (round point 0)) + ([{:keys [x y] :as p} decimanls] + (assert (point? p)) + (assert (number? decimanls)) + (Point. (mth/precision x decimanls) + (mth/precision y decimanls)))) (defn transform "Transform a point applying a matrix transfomation." diff --git a/frontend/src/uxbox/util/geom/snap_points.cljs b/frontend/src/uxbox/util/geom/snap_points.cljs index a8274a5662..ebdfbc54d0 100644 --- a/frontend/src/uxbox/util/geom/snap_points.cljs +++ b/frontend/src/uxbox/util/geom/snap_points.cljs @@ -14,36 +14,15 @@ [uxbox.util.geom.shapes :as gsh] [uxbox.util.geom.point :as gpt])) -(defn- frame-snap-points [{:keys [x y width height]}] - #{(gpt/point x y) - (gpt/point (+ x (/ width 2)) y) - (gpt/point (+ x width) y) - (gpt/point (+ x width) (+ y (/ height 2))) - (gpt/point (+ x width) (+ y height)) - (gpt/point (+ x (/ width 2)) (+ y height)) - (gpt/point x (+ y height)) - (gpt/point x (+ y (/ height 2)))}) - -(defn- frame-snap-points-resize [{:keys [x y width height]} handler] - (case handler - :top-left (gpt/point x y) - :top (gpt/point (+ x (/ width 2)) y) - :top-right (gpt/point (+ x width) y) - :right (gpt/point (+ x width) (+ y (/ height 2))) - :bottom-right (gpt/point (+ x width) (+ y height)) - :bottom (gpt/point (+ x (/ width 2)) (+ y height)) - :bottom-left (gpt/point x (+ y height)) - :left (gpt/point x (+ y (/ height 2))))) - -(def ^:private handler->point-idx - {:top-left 0 - :top 0 - :top-right 1 - :right 1 - :bottom-right 2 - :bottom 2 - :bottom-left 3 - :left 3}) +(defn- frame-snap-points [{:keys [x y width height] :as frame}] + (into #{(gpt/point x y) + (gpt/point (+ x (/ width 2)) y) + (gpt/point (+ x width) y) + (gpt/point (+ x width) (+ y (/ height 2))) + (gpt/point (+ x width) (+ y height)) + (gpt/point (+ x (/ width 2)) (+ y height)) + (gpt/point x (+ y height)) + (gpt/point x (+ y (/ height 2)))})) (defn shape-snap-points [shape] diff --git a/frontend/src/uxbox/worker/snaps.cljs b/frontend/src/uxbox/worker/snaps.cljs index b1f7c70e1f..e314acda09 100644 --- a/frontend/src/uxbox/worker/snaps.cljs +++ b/frontend/src/uxbox/worker/snaps.cljs @@ -14,17 +14,24 @@ [uxbox.common.pages :as cp] [uxbox.worker.impl :as impl] [uxbox.util.range-tree :as rt] - [uxbox.util.geom.snap-points :as snap])) + [uxbox.util.geom.snap-points :as snap] + [uxbox.util.geom.grid :as gg])) (defonce state (l/atom {})) (defn- create-coord-data "Initializes the range tree given the shapes" - [shapes coord] + [frame-id shapes coord] (let [process-shape (fn [coord] (fn [shape] - (let [points (snap/shape-snap-points shape)] - (map #(vector % (:id shape)) points)))) + (concat + (let [points (snap/shape-snap-points shape)] + (map #(vector % (:id shape)) points)) + + ;; The grid points are only added by the "root" of the coord-dat + (if (= (:id shape) frame-id) + (let [points (gg/grid-snap-points shape coord)] + (map #(vector % :layout) points)))))) into-tree (fn [tree [point _ :as data]] (rt/insert tree (coord point) data))] (->> shapes @@ -34,7 +41,7 @@ (defn- mapm "Map over the values of a map" [mfn coll] - (into {} (map (fn [[key val]] [key (mfn val)]) coll))) + (into {} (map (fn [[key val]] [key (mfn key val)]) coll))) (defn- initialize-snap-data "Initialize the snap information with the current workspace information" @@ -44,8 +51,8 @@ (group-by :frame-id)) frame-shapes (->> (cp/select-frames objects) (reduce #(update %1 (:id %2) conj %2) frame-shapes))] - (mapm (fn [shapes] {:x (create-coord-data shapes :x) - :y (create-coord-data shapes :y)}) + (mapm (fn [frame-id shapes] {:x (create-coord-data frame-id shapes :x) + :y (create-coord-data frame-id shapes :y)}) frame-shapes))) (defn- log-state