Merge pull request #219 from uxbox/256/grids

Grids
This commit is contained in:
Hirunatan 2020-05-19 16:17:08 +02:00 committed by GitHub
commit fe08810340
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1441 additions and 433 deletions

View File

@ -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)

View File

@ -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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -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)

View File

@ -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"
}

View File

@ -366,6 +366,9 @@ ul.slider-dots {
// Input amounts
&.pixels {
& input {
padding-right: 20px;
}
&::after {
content: "px";

View File

@ -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;
}
}

View File

@ -66,7 +66,9 @@
:layers
:element-options
:rules
:dynamic-alignment})
:dynamic-alignment
:display-grid
:snap-grid})
(s/def ::options-mode #{:design :prototype})

View File

@ -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})))))

View File

@ -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]

View File

@ -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))))

View File

@ -25,7 +25,6 @@
:onChangeComplete on-change-complete
:style {:box-shadow "none"}}]))
(def most-used-colors
(letfn [(selector [{:keys [objects]}]
(as-> {} $

View File

@ -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]

View File

@ -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]

View File

@ -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]])))]]]))

View File

@ -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]])))]]]))

View File

@ -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]))

View File

@ -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

View File

@ -73,7 +73,8 @@
[:& viewport {:page page
:key (:id page)
:file file
:local local}]]]
:local local
:layout layout}]]]
[:& left-toolbar {:page page :layout layout}]

View File

@ -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)}])]))

View File

@ -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)"}]]))

View File

@ -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")

View File

@ -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 <delacruzgarciajuan@gmail.com>
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
(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}]])]))

View File

@ -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}]])))))

View File

@ -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}]])

View File

@ -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}])])]))

View File

@ -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}])

View File

@ -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}]]))

View File

@ -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}]))
]])

View File

@ -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)

View File

@ -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)}]

View File

@ -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)))))))

View File

@ -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."

View File

@ -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]

View File

@ -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