mirror of
https://github.com/penpot/penpot.git
synced 2025-01-31 19:39:07 -05:00
✨ Copy/paste properties an CSS
This commit is contained in:
parent
80d6968156
commit
714a274789
11 changed files with 1485 additions and 23 deletions
|
@ -12,6 +12,8 @@
|
|||
|
||||
- New gradients UI with multi-stop support.
|
||||
- Shareable link pointing to an specific board.
|
||||
- Copy styles in CSS
|
||||
- Copy/paste shape styles (fills, strokes, shadows, etc..)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#?(:clj [app.common.fressian :as fres])
|
||||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.proportions :as gpr]
|
||||
|
@ -17,6 +18,7 @@
|
|||
[app.common.record :as cr]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.text :as txt]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.common.types.grid :as ctg]
|
||||
|
@ -570,3 +572,136 @@
|
|||
:class Shape
|
||||
:wfn fres/write-map-like
|
||||
:rfn (comp map->Shape fres/read-map-like)}))
|
||||
|
||||
;; --- SHAPE COPY/PASTE PROPS
|
||||
|
||||
;; Copy/paste properties:
|
||||
;; - Fill
|
||||
;; - Stroke
|
||||
;; - Opacity
|
||||
;; - Layout (Grid & Flex)
|
||||
;; - Flex element
|
||||
;; - Flex board
|
||||
;; - Text properties
|
||||
;; - Contraints
|
||||
;; - Shadow
|
||||
;; - Blur
|
||||
;; - Border radius
|
||||
(def ^:private basic-extract-props
|
||||
[:fills
|
||||
:strokes
|
||||
:opacity
|
||||
|
||||
;; Layout Item
|
||||
:layout-item-margin
|
||||
:layout-item-margin-type
|
||||
:layout-item-h-sizing
|
||||
:layout-item-v-sizing
|
||||
:layout-item-max-h
|
||||
:layout-item-min-h
|
||||
:layout-item-max-w
|
||||
:layout-item-min-w
|
||||
:layout-item-absolute
|
||||
:layout-item-z-index
|
||||
|
||||
;; Constraints
|
||||
:constraints-h
|
||||
:constraints-v
|
||||
|
||||
:shadow
|
||||
:blur
|
||||
|
||||
;; Radius
|
||||
:r1
|
||||
:r2
|
||||
:r3
|
||||
:r4])
|
||||
|
||||
(def ^:private layout-extract-props
|
||||
[:layout
|
||||
:layout-flex-dir
|
||||
:layout-gap-type
|
||||
:layout-gap
|
||||
:layout-wrap-type
|
||||
:layout-align-items
|
||||
:layout-align-content
|
||||
:layout-justify-items
|
||||
:layout-justify-content
|
||||
:layout-padding-type
|
||||
:layout-padding
|
||||
:layout-grid-dir
|
||||
:layout-grid-rows
|
||||
:layout-grid-columns
|
||||
:layout-grid-cells])
|
||||
|
||||
(defn extract-props
|
||||
"Retrieves an object with the 'pasteable' properties for a shape."
|
||||
[shape]
|
||||
(letfn [(assoc-props
|
||||
[props node attrs]
|
||||
(->> attrs
|
||||
(reduce
|
||||
(fn [props attr]
|
||||
(cond-> props
|
||||
(and (not (contains? props attr))
|
||||
(some? (get node attr)))
|
||||
(assoc attr (get node attr))))
|
||||
props)))
|
||||
|
||||
(extract-text-props
|
||||
[props shape]
|
||||
(->> (txt/node-seq (:content shape))
|
||||
(reduce
|
||||
(fn [result node]
|
||||
(cond-> result
|
||||
(txt/is-root-node? node)
|
||||
(assoc-props node txt/root-attrs)
|
||||
|
||||
(txt/is-paragraph-node? node)
|
||||
(assoc-props node txt/paragraph-attrs)
|
||||
|
||||
(txt/is-text-node? node)
|
||||
(assoc-props node txt/text-node-attrs)))
|
||||
props)))
|
||||
|
||||
(extract-layout-props
|
||||
[props shape]
|
||||
(d/patch-object props (select-keys shape layout-extract-props)))]
|
||||
|
||||
(-> shape
|
||||
(select-keys basic-extract-props)
|
||||
(cond-> (cfh/text-shape? shape) (extract-text-props shape))
|
||||
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape)))))
|
||||
|
||||
(defn patch-props
|
||||
"Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary"
|
||||
[shape props objects]
|
||||
|
||||
(letfn [(patch-text-props [shape props]
|
||||
(-> shape
|
||||
(update
|
||||
:content
|
||||
(fn [content]
|
||||
(->> content
|
||||
(txt/transform-nodes
|
||||
(fn [node]
|
||||
(cond-> node
|
||||
(txt/is-root-node? node)
|
||||
(d/patch-object (select-keys props txt/root-attrs))
|
||||
|
||||
(txt/is-paragraph-node? node)
|
||||
(d/patch-object (select-keys props txt/paragraph-attrs))
|
||||
|
||||
(txt/is-text-node? node)
|
||||
(d/patch-object (select-keys props txt/text-node-attrs))))))))))
|
||||
|
||||
(patch-layout-props [shape props]
|
||||
(let [shape (d/patch-object shape (select-keys props layout-extract-props))]
|
||||
(cond-> shape
|
||||
(ctsl/grid-layout? shape)
|
||||
(ctsl/assign-cells objects))))]
|
||||
|
||||
(-> shape
|
||||
(d/patch-object (select-keys props basic-extract-props))
|
||||
(cond-> (cfh/text-shape? shape) (patch-text-props props))
|
||||
(cond-> (cfh/frame-shape? shape) (patch-layout-props props)))))
|
||||
|
|
|
@ -0,0 +1,920 @@
|
|||
{
|
||||
"~:id": "~u870f9f10-87b5-8137-8005-93487d148645",
|
||||
"~:file-id": "~u870f9f10-87b5-8137-8005-934804124660",
|
||||
"~:created-at": "~m1736778551374",
|
||||
"~:data": {
|
||||
"~:options": {},
|
||||
"~:objects": {
|
||||
"~u00000000-0000-0000-0000-000000000000": {
|
||||
"~#shape": {
|
||||
"~:y": 0,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:name": "Root Frame",
|
||||
"~:width": 0.01,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.01
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 0,
|
||||
"~:proportion": 1.0,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 0,
|
||||
"~:y": 0,
|
||||
"~:width": 0.01,
|
||||
"~:height": 0.01,
|
||||
"~:x1": 0,
|
||||
"~:y1": 0,
|
||||
"~:x2": 0.01,
|
||||
"~:y2": 0.01
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 0.01,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": [
|
||||
"~u0eef4dd0-b39b-807a-8005-934805578f93",
|
||||
"~u0eef4dd0-b39b-807a-8005-93484afe7510",
|
||||
"~u0eef4dd0-b39b-807a-8005-93484d68344f",
|
||||
"~u0eef4dd0-b39b-807a-8005-9348531e39f7",
|
||||
"~u0eef4dd0-b39b-807a-8005-934855b02e52",
|
||||
"~ud4467b11-7129-80c3-8005-934871e790b5"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u0eef4dd0-b39b-807a-8005-934805578f93": {
|
||||
"~#shape": {
|
||||
"~:y": 283,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Board",
|
||||
"~:width": 406,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 460,
|
||||
"~:y": 283
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 866,
|
||||
"~:y": 283
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 866,
|
||||
"~:y": 766
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 460,
|
||||
"~:y": 766
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u0eef4dd0-b39b-807a-8005-934805578f93",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 460,
|
||||
"~:proportion": 1,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 460,
|
||||
"~:y": 283,
|
||||
"~:width": 406,
|
||||
"~:height": 483,
|
||||
"~:x1": 460,
|
||||
"~:y1": 283,
|
||||
"~:x2": 866,
|
||||
"~:y2": 766
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 483,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": [
|
||||
"~u0eef4dd0-b39b-807a-8005-934808365fa2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u0eef4dd0-b39b-807a-8005-934808365fa2": {
|
||||
"~#shape": {
|
||||
"~:y": 339,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Rectangle",
|
||||
"~:width": 117,
|
||||
"~:type": "~:rect",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 530,
|
||||
"~:y": 339
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 647,
|
||||
"~:y": 339
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 647,
|
||||
"~:y": 453
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 530,
|
||||
"~:y": 453
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u0eef4dd0-b39b-807a-8005-934808365fa2",
|
||||
"~:parent-id": "~u0eef4dd0-b39b-807a-8005-934805578f93",
|
||||
"~:frame-id": "~u0eef4dd0-b39b-807a-8005-934805578f93",
|
||||
"~:strokes": [
|
||||
{
|
||||
"~:stroke-alignment": "~:outer",
|
||||
"~:stroke-style": "~:solid",
|
||||
"~:stroke-color": "#000000",
|
||||
"~:stroke-opacity": 1,
|
||||
"~:stroke-width": 3
|
||||
}
|
||||
],
|
||||
"~:x": 530,
|
||||
"~:proportion": 1,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 530,
|
||||
"~:y": 339,
|
||||
"~:width": 117,
|
||||
"~:height": 114,
|
||||
"~:x1": 530,
|
||||
"~:y1": 339,
|
||||
"~:x2": 647,
|
||||
"~:y2": 453
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color-gradient": {
|
||||
"~:start-x": 0.5,
|
||||
"~:start-y": 0,
|
||||
"~:end-x": 0.5,
|
||||
"~:end-y": 1,
|
||||
"~:width": 1,
|
||||
"~:type": "~:linear",
|
||||
"~:stops": [
|
||||
{
|
||||
"~:color": "#ff0000",
|
||||
"~:file-id": null,
|
||||
"~:offset": 0,
|
||||
"~:opacity": 1,
|
||||
"~:id": null
|
||||
},
|
||||
{
|
||||
"~:color": "#ff8600",
|
||||
"~:file-id": null,
|
||||
"~:offset": 0.17,
|
||||
"~:opacity": 1,
|
||||
"~:id": null
|
||||
},
|
||||
{
|
||||
"~:color": "#f6ff00",
|
||||
"~:file-id": null,
|
||||
"~:offset": 0.33,
|
||||
"~:opacity": 1,
|
||||
"~:id": null
|
||||
},
|
||||
{
|
||||
"~:color": "#24ff00",
|
||||
"~:file-id": null,
|
||||
"~:offset": 0.5,
|
||||
"~:opacity": 1,
|
||||
"~:id": null
|
||||
},
|
||||
{
|
||||
"~:color": "#00ffe6",
|
||||
"~:file-id": null,
|
||||
"~:offset": 0.67,
|
||||
"~:opacity": 1,
|
||||
"~:id": null
|
||||
},
|
||||
{
|
||||
"~:color": "#2500ff",
|
||||
"~:file-id": null,
|
||||
"~:offset": 0.83,
|
||||
"~:opacity": 1,
|
||||
"~:id": null
|
||||
},
|
||||
{
|
||||
"~:color": "#bf00ff",
|
||||
"~:file-id": null,
|
||||
"~:offset": 1,
|
||||
"~:opacity": 1,
|
||||
"~:id": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 114,
|
||||
"~:flip-y": null
|
||||
}
|
||||
},
|
||||
"~u0eef4dd0-b39b-807a-8005-93484afe7510": {
|
||||
"~#shape": {
|
||||
"~:y": 314,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Ellipse",
|
||||
"~:width": 103,
|
||||
"~:type": "~:circle",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1151,
|
||||
"~:y": 314
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1254,
|
||||
"~:y": 314
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1254,
|
||||
"~:y": 417
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1151,
|
||||
"~:y": 417
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:id": "~u0eef4dd0-b39b-807a-8005-93484afe7510",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 1151,
|
||||
"~:proportion": 1,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 1151,
|
||||
"~:y": 314,
|
||||
"~:width": 103,
|
||||
"~:height": 103,
|
||||
"~:x1": 1151,
|
||||
"~:y1": 314,
|
||||
"~:x2": 1254,
|
||||
"~:y2": 417
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 103,
|
||||
"~:flip-y": null
|
||||
}
|
||||
},
|
||||
"~u0eef4dd0-b39b-807a-8005-93484d68344f": {
|
||||
"~#shape": {
|
||||
"~:y": null,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:content": [
|
||||
{
|
||||
"~:command": "~:move-to",
|
||||
"~:params": {
|
||||
"~:x": 1121,
|
||||
"~:y": 554
|
||||
}
|
||||
},
|
||||
{
|
||||
"~:command": "~:line-to",
|
||||
"~:params": {
|
||||
"~:x": 1229,
|
||||
"~:y": 458
|
||||
}
|
||||
},
|
||||
{
|
||||
"~:command": "~:curve-to",
|
||||
"~:params": {
|
||||
"~:x": 1303,
|
||||
"~:y": 518,
|
||||
"~:c1x": 1229,
|
||||
"~:c1y": 458,
|
||||
"~:c2x": 1320,
|
||||
"~:c2y": 492
|
||||
}
|
||||
},
|
||||
{
|
||||
"~:command": "~:curve-to",
|
||||
"~:params": {
|
||||
"~:x": 1219,
|
||||
"~:y": 584,
|
||||
"~:c1x": 1286,
|
||||
"~:c1y": 544,
|
||||
"~:c2x": 1258,
|
||||
"~:c2y": 572
|
||||
}
|
||||
},
|
||||
{
|
||||
"~:command": "~:curve-to",
|
||||
"~:params": {
|
||||
"~:x": 1121,
|
||||
"~:y": 554,
|
||||
"~:c1x": 1180,
|
||||
"~:c1y": 596,
|
||||
"~:c2x": 1121,
|
||||
"~:c2y": 554
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:name": "Path",
|
||||
"~:width": null,
|
||||
"~:type": "~:path",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1121,
|
||||
"~:y": 458
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1305.1163606979621,
|
||||
"~:y": 458
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1305.1163606979621,
|
||||
"~:y": 586.15625
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1121,
|
||||
"~:y": 586.15625
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:id": "~u0eef4dd0-b39b-807a-8005-93484d68344f",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [
|
||||
{
|
||||
"~:stroke-style": "~:solid",
|
||||
"~:stroke-alignment": "~:inner",
|
||||
"~:stroke-width": 2,
|
||||
"~:stroke-color": "#000000",
|
||||
"~:stroke-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:x": null,
|
||||
"~:proportion": 1,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 1121,
|
||||
"~:y": 458,
|
||||
"~:width": 184.11636069796214,
|
||||
"~:height": 128.15625,
|
||||
"~:x1": 1121,
|
||||
"~:y1": 458,
|
||||
"~:x2": 1305.1163606979621,
|
||||
"~:y2": 586.15625
|
||||
}
|
||||
},
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": null,
|
||||
"~:flip-y": null
|
||||
}
|
||||
},
|
||||
"~u0eef4dd0-b39b-807a-8005-9348531e39f7": {
|
||||
"~#shape": {
|
||||
"~:y": 645,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Board",
|
||||
"~:width": 140,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1150,
|
||||
"~:y": 645
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1290,
|
||||
"~:y": 645
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1290,
|
||||
"~:y": 766
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1150,
|
||||
"~:y": 766
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u0eef4dd0-b39b-807a-8005-9348531e39f7",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 1150,
|
||||
"~:proportion": 1,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 1150,
|
||||
"~:y": 645,
|
||||
"~:width": 140,
|
||||
"~:height": 121,
|
||||
"~:x1": 1150,
|
||||
"~:y1": 645,
|
||||
"~:x2": 1290,
|
||||
"~:y2": 766
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 121,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": []
|
||||
}
|
||||
},
|
||||
"~u0eef4dd0-b39b-807a-8005-934855b02e52": {
|
||||
"~#shape": {
|
||||
"~:y": 188,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Rectangle",
|
||||
"~:width": 100,
|
||||
"~:type": "~:rect",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1154,
|
||||
"~:y": 188
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1254,
|
||||
"~:y": 188
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1254,
|
||||
"~:y": 283
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1154,
|
||||
"~:y": 283
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u0eef4dd0-b39b-807a-8005-934855b02e52",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 1154,
|
||||
"~:proportion": 1,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 1154,
|
||||
"~:y": 188,
|
||||
"~:width": 100,
|
||||
"~:height": 95,
|
||||
"~:x1": 1154,
|
||||
"~:y1": 188,
|
||||
"~:x2": 1254,
|
||||
"~:y2": 283
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 95,
|
||||
"~:flip-y": null
|
||||
}
|
||||
},
|
||||
"~ud4467b11-7129-80c3-8005-934871e790b5": {
|
||||
"~#shape": {
|
||||
"~:y": 856,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:auto-width",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:children": [
|
||||
{
|
||||
"~:type": "paragraph-set",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "44",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "Uno dos tres cuatro"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "dqq7j",
|
||||
"~:font-size": "44",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:type": "paragraph",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Uno dos tres cuatro",
|
||||
"~:width": 360,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1094,
|
||||
"~:y": 856
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1454,
|
||||
"~:y": 856
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1454,
|
||||
"~:y": 909
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1094,
|
||||
"~:y": 909
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:id": "~ud4467b11-7129-80c3-8005-934871e790b5",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:position-data": [
|
||||
{
|
||||
"~#rect": {
|
||||
"~:y": 910,
|
||||
"~:font-style": "normal",
|
||||
"~:text-transform": "none",
|
||||
"~:font-size": "44px",
|
||||
"~:font-weight": "400",
|
||||
"~:y1": -3,
|
||||
"~:width": 359.546875,
|
||||
"~:text-decoration": "none solid rgb(0, 0, 0)",
|
||||
"~:letter-spacing": "normal",
|
||||
"~:x": 1094,
|
||||
"~:x1": 0,
|
||||
"~:y2": 54,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:x2": 359.546875,
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 57,
|
||||
"~:text": "Uno dos tres cuatro"
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:x": 1094,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 1094,
|
||||
"~:y": 856,
|
||||
"~:width": 360,
|
||||
"~:height": 53,
|
||||
"~:x1": 1094,
|
||||
"~:y1": 856,
|
||||
"~:x2": 1454,
|
||||
"~:y2": 909
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 53,
|
||||
"~:flip-y": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u870f9f10-87b5-8137-8005-934804124661",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
}
|
49
frontend/playwright/data/workspace/get-file-copy-paste.json
Normal file
49
frontend/playwright/data/workspace/get-file-copy-paste.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"layout/grid",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "New File 6",
|
||||
"~:revn": 13,
|
||||
"~:modified-at": "~m1736778551377",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u870f9f10-87b5-8137-8005-934804124660",
|
||||
"~:is-shared": false,
|
||||
"~:version": 60,
|
||||
"~:project-id": "~u3ffbd505-2f26-800f-8004-f34da98bdad8",
|
||||
"~:created-at": "~m1736778427466",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u870f9f10-87b5-8137-8005-934804124661"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u870f9f10-87b5-8137-8005-934804124661": {
|
||||
"~#penpot/pointer": [
|
||||
"~u870f9f10-87b5-8137-8005-93487d148645",
|
||||
{
|
||||
"~:created-at": "~m1736778551378"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~:id": "~u870f9f10-87b5-8137-8005-934804124660",
|
||||
"~:options": {
|
||||
"~:components-v2": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -250,3 +250,60 @@ test("User have edition menu entries", async ({ page }) => {
|
|||
await expect(page.getByText("Undo")).toBeVisible();
|
||||
await expect(page.getByText("Redo")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Copy/paste properties", async ({ page, context }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
await workspacePage.mockRPC(
|
||||
/get\-file\?/,
|
||||
"workspace/get-file-copy-paste.json",
|
||||
);
|
||||
await workspacePage.mockRPC(
|
||||
"get-file-fragment?file-id=*&fragment-id=*",
|
||||
"workspace/get-file-copy-paste-fragment.json",
|
||||
);
|
||||
|
||||
await workspacePage.goToWorkspace({
|
||||
fileId: "870f9f10-87b5-8137-8005-934804124660",
|
||||
pageId: "870f9f10-87b5-8137-8005-934804124661",
|
||||
});
|
||||
|
||||
// Access to the read/write clipboard necesary for this functionality
|
||||
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
|
||||
await page.getByTestId("layer-item").getByRole("button").first().click();
|
||||
await page
|
||||
.getByTestId("children-0eef4dd0-b39b-807a-8005-934805578f93")
|
||||
.getByText("Rectangle")
|
||||
.click({ button: "right" });
|
||||
await page.getByText("Copy/Paste as").hover();
|
||||
await page.getByText("Copy properties").click();
|
||||
|
||||
await page
|
||||
.getByTestId("layer-item")
|
||||
.getByText("Uno dos tres cuatro")
|
||||
.click({ button: "right" });
|
||||
await page.getByText("Copy/Paste as").hover();
|
||||
await page.getByText("Paste properties").click();
|
||||
|
||||
await page.getByText("Rectangle").first().click({ button: "right" });
|
||||
await page.getByText("Copy/Paste as").hover();
|
||||
await page.getByText("Paste properties").click();
|
||||
|
||||
await page.getByText("Board").nth(2).click({ button: "right" });
|
||||
await page.getByText("Copy/Paste as").hover();
|
||||
await page.getByText("Paste properties").click();
|
||||
|
||||
await page
|
||||
.getByTestId("layer-item")
|
||||
.locator("div")
|
||||
.filter({ hasText: "Path" })
|
||||
.nth(1)
|
||||
.click({ button: "right" });
|
||||
await page.getByText("Copy/Paste as").hover();
|
||||
await page.getByText("Paste properties").click();
|
||||
|
||||
await page.getByText("Ellipse").click({ button: "right" });
|
||||
await page.getByText("Copy/Paste as").hover();
|
||||
await page.getByText("Paste properties").click();
|
||||
});
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
[app.main.streams :as ms]
|
||||
[app.main.worker :as uw]
|
||||
[app.render-wasm :as wasm]
|
||||
[app.util.code-gen.style-css :as css]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.http :as http]
|
||||
|
@ -1411,7 +1412,8 @@
|
|||
(rx/catch on-copy-error)
|
||||
(rx/ignore))))))))))
|
||||
|
||||
(declare ^:private paste-transit)
|
||||
(declare ^:private paste-transit-shapes)
|
||||
(declare ^:private paste-transit-props)
|
||||
(declare ^:private paste-html-text)
|
||||
(declare ^:private paste-text)
|
||||
(declare ^:private paste-image)
|
||||
|
@ -1441,7 +1443,7 @@
|
|||
(rx/of (paste-text data)))
|
||||
|
||||
:transit
|
||||
(rx/of (paste-transit data))))
|
||||
(rx/of (paste-transit-shapes data))))
|
||||
|
||||
(on-error [cause]
|
||||
(let [data (ex-data cause)]
|
||||
|
@ -1462,7 +1464,6 @@
|
|||
(rx/take 1)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
(defn paste-from-event
|
||||
"Perform a `paste` operation from user emmited event."
|
||||
[event in-viewport?]
|
||||
|
@ -1491,7 +1492,7 @@
|
|||
(rx/map paste-image))
|
||||
|
||||
(coll? transit-data)
|
||||
(rx/of (paste-transit (assoc transit-data :in-viewport in-viewport?)))
|
||||
(rx/of (paste-transit-shapes (assoc transit-data :in-viewport in-viewport?)))
|
||||
|
||||
(string? html-data)
|
||||
(rx/of (paste-html-text html-data text-data))
|
||||
|
@ -1502,6 +1503,122 @@
|
|||
:else
|
||||
(rx/empty))))))))
|
||||
|
||||
(defn copy-selected-css
|
||||
[]
|
||||
(ptk/reify ::copy-selected-css
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
selected (->> (wsh/lookup-selected state) (mapv (d/getf objects)))
|
||||
css (css/generate-style objects selected selected {:with-prelude? false})]
|
||||
(wapi/write-to-clipboard css)))))
|
||||
|
||||
(defn copy-selected-css-nested
|
||||
[]
|
||||
(ptk/reify ::copy-selected-css-nested
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
selected (->> (wsh/lookup-selected state)
|
||||
(cfh/selected-with-children objects)
|
||||
(mapv (d/getf objects)))
|
||||
css (css/generate-style objects selected selected {:with-prelude? false})]
|
||||
(wapi/write-to-clipboard css)))))
|
||||
|
||||
(defn copy-selected-props
|
||||
[]
|
||||
(ptk/reify ::copy-selected-props
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(letfn [(fetch-image [entry]
|
||||
(let [url (cf/resolve-file-media entry)]
|
||||
(->> (http/send! {:method :get
|
||||
:uri url
|
||||
:response-type :blob})
|
||||
(rx/map :body)
|
||||
(rx/mapcat wapi/read-file-as-data-url)
|
||||
(rx/map #(assoc entry :data %)))))
|
||||
|
||||
(resolve-images [data]
|
||||
(let [images
|
||||
(concat
|
||||
(->> data :props :fills (keep :fill-image))
|
||||
(->> data :props :strokes (keep :stroke-image)))]
|
||||
|
||||
(if (seq images)
|
||||
(->> (rx/from images)
|
||||
(rx/mapcat fetch-image)
|
||||
(rx/reduce conj #{})
|
||||
(rx/map #(assoc data :images %)))
|
||||
(rx/of data))))
|
||||
|
||||
(on-copy-error [error]
|
||||
(js/console.error "clipboard blocked:" error)
|
||||
(rx/empty))]
|
||||
|
||||
(let [selected (->> (wsh/lookup-selected state) first)
|
||||
objects (wsh/lookup-page-objects state)]
|
||||
|
||||
(when-let [shape (get objects selected)]
|
||||
(let [props (cts/extract-props shape)
|
||||
features (-> (features/get-team-enabled-features state)
|
||||
(set/difference cfeat/frontend-only-features))
|
||||
version (dm/get-in state [:workspace-file :version])
|
||||
|
||||
copy-data {:type :copied-props
|
||||
:features features
|
||||
:version version
|
||||
:props props
|
||||
:images #{}}]
|
||||
|
||||
;; The clipboard API doesn't handle well asynchronous calls because it expects to use
|
||||
;; the clipboard in an user interaction. If you do an async call the callback is outside
|
||||
;; the thread of the UI and so Safari blocks the copying event.
|
||||
;; We use the API `ClipboardItem` that allows promises to be passed and so the event
|
||||
;; will wait for the promise to resolve and everything should work as expected.
|
||||
;; This only works in the current versions of the browsers.
|
||||
(if (some? (unchecked-get ug/global "ClipboardItem"))
|
||||
(let [resolve-data-promise
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(->> (rx/of copy-data)
|
||||
(rx/mapcat resolve-images)
|
||||
(rx/map #(t/encode-str % {:type :json-verbose}))
|
||||
(rx/map #(wapi/create-blob % "text/plain"))
|
||||
(rx/subs! resolve reject))))]
|
||||
|
||||
(->> (rx/from (wapi/write-to-clipboard-promise "text/plain" resolve-data-promise))
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore)))
|
||||
;; FIXME: this is to support Firefox versions below 116 that don't support
|
||||
;; `ClipboardItem` after the version 116 is less common we could remove this.
|
||||
;; https://caniuse.com/?search=ClipboardItem
|
||||
(->> (rx/of copy-data)
|
||||
(rx/mapcat resolve-images)
|
||||
(rx/map #(wapi/write-to-clipboard (t/encode-str % {:type :json-verbose})))
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore))))))))))
|
||||
|
||||
(defn paste-selected-props
|
||||
[]
|
||||
(ptk/reify ::paste-selected-props
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(letfn [(decode-entry [entry]
|
||||
(-> entry t/decode-str paste-transit-props))
|
||||
|
||||
(on-error [cause]
|
||||
(let [data (ex-data cause)]
|
||||
(if (:not-implemented data)
|
||||
(rx/of (ntf/warn (tr "errors.clipboard-not-implemented")))
|
||||
(js/console.error "Clipboard error:" cause))
|
||||
(rx/empty)))]
|
||||
|
||||
(->> (wapi/read-from-clipboard)
|
||||
(rx/map decode-entry)
|
||||
(rx/take 1)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn selected-frame? [state]
|
||||
(let [selected (wsh/lookup-selected state)
|
||||
objects (wsh/lookup-page-objects state)]
|
||||
|
@ -1530,8 +1647,8 @@
|
|||
(:width (:selrect frame-obj)))))
|
||||
|
||||
(def ^:private
|
||||
schema:paste-data
|
||||
[:map {:title "paste-data"}
|
||||
schema:paste-data-shapes
|
||||
[:map {:title "paste-data-shapes"}
|
||||
[:type [:= :copied-shapes]]
|
||||
[:features ::sm/set-of-strings]
|
||||
[:version :int]
|
||||
|
@ -1542,12 +1659,26 @@
|
|||
[:images [:set :map]]
|
||||
[:position {:optional true} ::gpt/point]])
|
||||
|
||||
(def ^:private
|
||||
schema:paste-data-props
|
||||
[:map {:title "paste-data-props"}
|
||||
[:type [:= :copied-props]]
|
||||
[:features ::sm/set-of-strings]
|
||||
[:version :int]
|
||||
[:props
|
||||
;; todo type the properties
|
||||
[:map-of :keyword :any]]])
|
||||
|
||||
(def schema:paste-data
|
||||
[:multi {:title "paste-data" :dispatch :type}
|
||||
[:copied-shapes schema:paste-data-shapes]
|
||||
[:copied-props schema:paste-data-props]])
|
||||
|
||||
(def paste-data-valid?
|
||||
(sm/lazy-validator schema:paste-data))
|
||||
|
||||
(defn- paste-transit
|
||||
(defn- paste-transit-shapes
|
||||
[{:keys [images] :as pdata}]
|
||||
|
||||
(letfn [(upload-media [file-id imgpart]
|
||||
(->> (http/send! {:uri (:data imgpart)
|
||||
:response-type :blob
|
||||
|
@ -1562,7 +1693,7 @@
|
|||
(rx/mapcat (partial rp/cmd! :upload-file-media-object))
|
||||
(rx/map #(assoc % :prev-id (:id imgpart)))))]
|
||||
|
||||
(ptk/reify ::paste-transit
|
||||
(ptk/reify ::paste-transit-shapes
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
|
@ -1574,14 +1705,88 @@
|
|||
:hibt "invalid paste data found"))
|
||||
|
||||
(cfeat/check-paste-features! features (:features pdata))
|
||||
(if (= file-id (:file-id pdata))
|
||||
(let [pdata (assoc pdata :images [])]
|
||||
(rx/of (paste-shapes pdata)))
|
||||
(->> (rx/from images)
|
||||
|
||||
(case (:type pdata)
|
||||
:copied-shapes
|
||||
(if (= file-id (:file-id pdata))
|
||||
(let [pdata (assoc pdata :images [])]
|
||||
(rx/of (paste-shapes pdata)))
|
||||
(->> (rx/from images)
|
||||
(rx/merge-map (partial upload-media file-id))
|
||||
(rx/reduce conj [])
|
||||
(rx/map #(assoc pdata :images %))
|
||||
(rx/map paste-shapes)))
|
||||
nil))))))
|
||||
|
||||
(defn- paste-transit-props
|
||||
[pdata]
|
||||
|
||||
(letfn [(upload-media [file-id imgpart]
|
||||
(->> (http/send! {:uri (:data imgpart)
|
||||
:response-type :blob
|
||||
:method :get})
|
||||
(rx/map :body)
|
||||
(rx/map
|
||||
(fn [blob]
|
||||
{:name (:name imgpart)
|
||||
:file-id file-id
|
||||
:content blob
|
||||
:is-local true}))
|
||||
(rx/mapcat (partial rp/cmd! :upload-file-media-object))
|
||||
(rx/map #(vector (:id imgpart) %))))
|
||||
|
||||
(update-image-data
|
||||
[pdata media-map]
|
||||
(update
|
||||
pdata :props
|
||||
(fn [props]
|
||||
(-> props
|
||||
(d/update-when
|
||||
:fills
|
||||
(fn [fills]
|
||||
(mapv (fn [fill]
|
||||
(cond-> fill
|
||||
(some? (:fill-image fill))
|
||||
(update-in [:fill-image :id] #(get media-map % %))))
|
||||
fills)))
|
||||
(d/update-when
|
||||
:strokes
|
||||
(fn [strokes]
|
||||
(mapv (fn [stroke]
|
||||
(cond-> stroke
|
||||
(some? (:stroke-image stroke))
|
||||
(update-in [:stroke-image :id] #(get media-map % %))))
|
||||
strokes)))))))
|
||||
|
||||
(upload-images
|
||||
[file-id pdata]
|
||||
(->> (rx/from (:images pdata))
|
||||
(rx/merge-map (partial upload-media file-id))
|
||||
(rx/reduce conj [])
|
||||
(rx/map #(assoc pdata :images %))
|
||||
(rx/map paste-shapes))))))))
|
||||
(rx/reduce conj {})
|
||||
(rx/map (partial update-image-data pdata))))]
|
||||
|
||||
(ptk/reify ::paste-transit-props
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (features/get-team-enabled-features state)
|
||||
selected (wsh/lookup-selected state)]
|
||||
|
||||
(when (paste-data-valid? pdata)
|
||||
(cfeat/check-paste-features! features (:features pdata))
|
||||
(case (:type pdata)
|
||||
:copied-props
|
||||
|
||||
(rx/concat
|
||||
(->> (rx/of pdata)
|
||||
(rx/mapcat (partial upload-images (:current-file-id state)))
|
||||
(rx/map
|
||||
#(dwsh/update-shapes
|
||||
selected
|
||||
(fn [shape objects] (cts/patch-props shape (:props pdata) objects))
|
||||
{:with-objects? true})))
|
||||
(rx/of (ptk/data-event :layout/update {:ids selected})))
|
||||
;;
|
||||
(rx/empty))))))))
|
||||
|
||||
(defn paste-shapes
|
||||
[{in-viewport? :in-viewport :as pdata}]
|
||||
|
|
|
@ -85,8 +85,8 @@
|
|||
:subsections [:edit]
|
||||
:fn #(st/emit! (dw/copy-selected))}
|
||||
|
||||
:copy-link {:tooltip (ds/meta (ds/alt "C"))
|
||||
:command (ds/c-mod "alt+c")
|
||||
:copy-link {:tooltip (ds/shift (ds/alt "C"))
|
||||
:command "shift+alt+c"
|
||||
:subsections [:edit]
|
||||
:fn #(st/emit! (dw/copy-link-to-clipboard))}
|
||||
|
||||
|
@ -103,6 +103,16 @@
|
|||
:subsections [:edit]
|
||||
:fn (constantly nil)}
|
||||
|
||||
:copy-props {:tooltip (ds/meta (ds/alt "c"))
|
||||
:command (ds/c-mod "alt+c")
|
||||
:subsections [:edit]
|
||||
:fn #(st/emit! (dw/copy-selected-props))}
|
||||
|
||||
:paste-props {:tooltip (ds/meta (ds/alt "v"))
|
||||
:command (ds/c-mod "alt+v")
|
||||
:subsections [:edit]
|
||||
:fn #(st/emit! (dw/paste-selected-props))}
|
||||
|
||||
:delete {:tooltip (ds/supr)
|
||||
:command ["del" "backspace"]
|
||||
:subsections [:edit]
|
||||
|
|
|
@ -321,7 +321,7 @@
|
|||
|
||||
.comment-input {
|
||||
@include bodySmallTypography;
|
||||
white-space: pre;
|
||||
white-space: pre-line;
|
||||
background: var(--input-background-color);
|
||||
border-radius: $br-8;
|
||||
border: $s-1 solid var(--input-border-color);
|
||||
|
|
|
@ -11,10 +11,12 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.shortcuts :as scd]
|
||||
|
@ -35,6 +37,8 @@
|
|||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr] :as i18n]
|
||||
[app.util.timers :as timers]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[okulary.core :as l]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
@ -141,12 +145,44 @@
|
|||
do-cut #(st/emit! (dw/copy-selected)
|
||||
(dw/delete-selected))
|
||||
do-paste #(st/emit! (dw/paste-from-clipboard))
|
||||
do-duplicate #(st/emit! (dw/duplicate-selected true))]
|
||||
do-duplicate #(st/emit! (dw/duplicate-selected true))
|
||||
|
||||
enabled-paste-props* (mf/use-state false)
|
||||
|
||||
handle-copy-css
|
||||
(mf/use-callback #(st/emit! (dw/copy-selected-css)))
|
||||
|
||||
handle-copy-css-nested
|
||||
(mf/use-callback #(st/emit! (dw/copy-selected-css-nested)))
|
||||
|
||||
handle-copy-props
|
||||
(mf/use-callback #(st/emit! (dw/copy-selected-props)))
|
||||
|
||||
handle-paste-props
|
||||
(mf/use-callback #(st/emit! (dw/paste-selected-props)))
|
||||
|
||||
handle-hover-copy-paste
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(->> (wapi/read-from-clipboard)
|
||||
(rx/take 1)
|
||||
(rx/subs!
|
||||
(fn [data]
|
||||
(try
|
||||
(let [pdata (t/decode-str data)]
|
||||
(reset! enabled-paste-props*
|
||||
(and (dw/paste-data-valid? pdata)
|
||||
(= :copied-props (:type pdata)))))
|
||||
(catch :default _
|
||||
(reset! enabled-paste-props* false))))
|
||||
(fn []
|
||||
(reset! enabled-paste-props* false))))))]
|
||||
|
||||
[:*
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy")
|
||||
:shortcut (sc/get-tooltip :copy)
|
||||
:on-click do-copy}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy_link")
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-link")
|
||||
:shortcut (sc/get-tooltip :copy-link)
|
||||
:on-click do-copy-link}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.cut")
|
||||
|
@ -159,6 +195,23 @@
|
|||
:shortcut (sc/get-tooltip :duplicate)
|
||||
:on-click do-duplicate}]
|
||||
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-paste-as")
|
||||
:on-pointer-enter (when (cf/check-browser? :chrome) handle-hover-copy-paste)}
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-css")
|
||||
:on-click handle-copy-css}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-css-nested")
|
||||
:on-click handle-copy-css-nested}]
|
||||
|
||||
[:> menu-separator* {}]
|
||||
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-props")
|
||||
:shortcut (sc/get-tooltip :copy-props)
|
||||
:on-click handle-copy-props}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.paste-props")
|
||||
:shortcut (sc/get-tooltip :paste-props)
|
||||
:disabled (and (cf/check-browser? :chrome) (not @enabled-paste-props*))
|
||||
:on-click handle-paste-props}]]
|
||||
|
||||
[:> menu-separator* {}]]))
|
||||
|
||||
(mf/defc context-menu-layer-position*
|
||||
|
|
|
@ -5991,9 +5991,24 @@ msgid "workspace.shape.menu.copy"
|
|||
msgstr "Copy"
|
||||
|
||||
#: src/app/main/ui/workspace/context_menu.cljs:146
|
||||
msgid "workspace.shape.menu.copy_link"
|
||||
msgid "workspace.shape.menu.copy-link"
|
||||
msgstr "Copy link to clipboard"
|
||||
|
||||
msgid "workspace.shape.menu.copy-paste-as"
|
||||
msgstr "Copy/Paste as ..."
|
||||
|
||||
msgid "workspace.shape.menu.copy-css"
|
||||
msgstr "Copy as CSS"
|
||||
|
||||
msgid "workspace.shape.menu.copy-css-nested"
|
||||
msgstr "Copy as CSS (nested layers)"
|
||||
|
||||
msgid "workspace.shape.menu.copy-props"
|
||||
msgstr "Copy properties"
|
||||
|
||||
msgid "workspace.shape.menu.paste-props"
|
||||
msgstr "Paste properties"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/common.cljs:431
|
||||
msgid "workspace.shape.menu.create-annotation"
|
||||
msgstr "Create annotation"
|
||||
|
@ -6956,3 +6971,4 @@ msgstr "Notifications"
|
|||
|
||||
msgid "comments.mentions.not-found"
|
||||
msgstr "No people found for @%s"
|
||||
|
||||
|
|
|
@ -5990,9 +5990,24 @@ msgstr "Enviar atrás"
|
|||
msgid "workspace.shape.menu.copy"
|
||||
msgstr "Copiar"
|
||||
|
||||
msgid "workspace.shape.menu.copy_link"
|
||||
msgid "workspace.shape.menu.copy-link"
|
||||
msgstr "Copiar enlace al portapapeles"
|
||||
|
||||
msgid "workspace.shape.menu.copy-paste-as"
|
||||
msgstr "Copiar/Pegar como ..."
|
||||
|
||||
msgid "workspace.shape.menu.copy-css"
|
||||
msgstr "Copiar como CSS"
|
||||
|
||||
msgid "workspace.shape.menu.copy-css-nested"
|
||||
msgstr "Copiar como CSS (capas anidadas)"
|
||||
|
||||
msgid "workspace.shape.menu.copy-props"
|
||||
msgstr "Copiar propiedades"
|
||||
|
||||
msgid "workspace.shape.menu.paste-props"
|
||||
msgstr "Pegar propiedades"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/common.cljs:427
|
||||
msgid "workspace.shape.menu.create-annotation"
|
||||
msgstr "Crear una nota"
|
||||
|
|
Loading…
Add table
Reference in a new issue