mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
Several actions built their changes in frontend events, step by step, each step its own commit, so the pure runner could not run them and one undo could revert half an action. - Renaming a variant member in the layers panel sent three events, each in its own undo transaction: update the properties from the typed formula, remove the properties no member gives a value, and set or clear the member's error. One undo reverted only the last. clvp/generate-rename-member builds the three steps in one changes (with generate-update-properties and generate-remove-empty-properties), and dwv/rename-member commits it once with the same analytics events. update-properties-names-and-values goes; remove-empty-properties and update-error stay for the design tab. - Transforming a component into a variant sent about fourteen events, and combining components as variants that transform and eight more. clv/generate-transform-in-variant and clv/generate-combine-as-variants build those steps as one changes, from the generators common already had, and the events commit them once. The flex layout initializer moves to common as cls/layout-initializer. Combine still sizes the board with two frontend dimension updates inside its undo transaction: the frontend applies them through modifiers, which common has no step for. - update-component split the changes of an update main (the copy's, marked :local-change?, from the main's, which may belong to a library) with two transducers; cll/split-local-changes does it. Its undo-group argument, never passed, goes, and the main's library is the copy head's :component-file, which it looked up through the file data only to read its id back. - dwv/valid-variant-switch? read the libraries from the state and repeated the lookup of clv/find-switch-target. clv/valid-switch? takes the libraries and asks find-switch-target; the plugin's switchVariant and the frontend test call it. Behavior changes: - One undo after a rename of a member, a transform or a combine reverts all of it. After a transform, it reverted only the sync of the copies' names (F6 in case AT); its pins go. - The transform no longer sends the "create-board" analytics event of the board it creates around the main. In the composable suites, renaming a member in the layers panel, removing the empty properties and setting a member's error are variant property edits both runners run, and case AW joins the JVM set. Its rename node drives the real layers-panel rename, which removes the empty properties too, so the step that removed them goes. The golden masters are recorded again for AW (the JVM one gains the case, the frontend one loses that step) and for AT (its final states no longer list the F6 undo failures). AI-assisted-by: claude-opus-5-5
2154 lines
92 KiB
Clojure
2154 lines
92 KiB
Clojure
;; 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) KALEIDOS SUBSIDIARY SL
|
|
|
|
(ns app.plugins.shape
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.files.helpers :as cfh]
|
|
[app.common.geom.rect :as grc]
|
|
[app.common.geom.shapes :as gsh]
|
|
[app.common.json :as json]
|
|
[app.common.logic.variants :as clv]
|
|
[app.common.path-names :as cpn]
|
|
[app.common.record :as crc]
|
|
[app.common.schema :as sm]
|
|
[app.common.svg.path :as svg.path]
|
|
[app.common.types.color :as clr]
|
|
[app.common.types.component :as ctk]
|
|
[app.common.types.container :as ctn]
|
|
[app.common.types.file :as ctf]
|
|
[app.common.types.fills :as types.fills]
|
|
[app.common.types.grid :as ctg]
|
|
[app.common.types.path :as path]
|
|
[app.common.types.shape :as cts]
|
|
[app.common.types.shape.background-blur :as ctsbb]
|
|
[app.common.types.shape.blur :as ctsb]
|
|
[app.common.types.shape.export :as ctse]
|
|
[app.common.types.shape.interactions :as ctsi]
|
|
[app.common.types.shape.layout :as ctl]
|
|
[app.common.types.shape.radius :as ctsr]
|
|
[app.common.types.shape.shadow :as ctss]
|
|
[app.common.types.stroke :as types.stroke]
|
|
[app.common.types.text :as txt]
|
|
[app.common.types.token :as ctt]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.exports.assets :as de]
|
|
[app.main.data.exports.wasm :as wasm.exports]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.data.persistence :as dwp]
|
|
[app.main.data.plugins :as dp]
|
|
[app.main.data.workspace :as dw]
|
|
[app.main.data.workspace.groups :as dwg]
|
|
[app.main.data.workspace.guides :as dwgu]
|
|
[app.main.data.workspace.interactions :as dwi]
|
|
[app.main.data.workspace.libraries :as dwl]
|
|
[app.main.data.workspace.selection :as dws]
|
|
[app.main.data.workspace.shape-layout :as dwsl]
|
|
[app.main.data.workspace.shapes :as dwsh]
|
|
[app.main.data.workspace.texts :as dwt]
|
|
[app.main.data.workspace.tokens.application :as dwta]
|
|
[app.main.data.workspace.variants :as dwv]
|
|
[app.main.features :as features]
|
|
[app.main.repo :as rp]
|
|
[app.main.store :as st]
|
|
[app.plugins.exports :as exports]
|
|
[app.plugins.fills :as fills]
|
|
[app.plugins.flex :as flex]
|
|
[app.plugins.format :as format]
|
|
[app.plugins.grid :as grid]
|
|
[app.plugins.parser :as parser]
|
|
[app.plugins.reflow :as wrfp]
|
|
[app.plugins.register :as r]
|
|
[app.plugins.ruler-guides :as rg]
|
|
[app.plugins.shadows :as shadows]
|
|
[app.plugins.strokes :as strokes]
|
|
[app.plugins.system-events :as se]
|
|
[app.plugins.text :as text]
|
|
[app.plugins.tokens :refer [applied-tokens-plugin->applied-tokens expand-token-attrs per-side-stroke-attrs-restricted? per-side-stroke-widths-message token-attr? valid-token-resolution?]]
|
|
[app.plugins.utils :as u]
|
|
[app.util.http :as http]
|
|
[app.util.object :as obj]
|
|
[beicon.v2.core :as rx]))
|
|
|
|
(declare shape-proxy)
|
|
(declare shape-proxy?)
|
|
;; This is injected from plugin/librraies
|
|
(def variant-proxy nil)
|
|
|
|
(defn interaction-proxy? [p]
|
|
(obj/type-of? p "InteractionProxy"))
|
|
|
|
(defn- valid-interaction-action?
|
|
[file-id page-id source raw-action interaction]
|
|
(let [page (u/locate-page file-id page-id)
|
|
destination-proxy (obj/get raw-action "destination")
|
|
destination-id (:destination interaction)
|
|
animation-type (get-in interaction [:animation :animation-type])]
|
|
(and (sm/validate ctsi/schema:interaction interaction)
|
|
(ctsi/valid-delay? interaction)
|
|
;; Interactions keep a stale destination after switching to an action
|
|
;; without one, so only check it for actions that use it
|
|
(or (not (ctsi/has-destination interaction))
|
|
(and (or (nil? destination-proxy)
|
|
(and (shape-proxy? destination-proxy)
|
|
(= file-id (obj/get destination-proxy "$file"))
|
|
(= page-id (obj/get destination-proxy "$page"))))
|
|
(ctsi/valid-destination? (:objects page) source destination-id)))
|
|
(ctsi/allowed-animation? (:action-type interaction) animation-type))))
|
|
|
|
(defn interaction-proxy
|
|
"Proxy over one interaction of a shape.
|
|
|
|
Interactions are addressed by position, which shifts as interactions are added
|
|
or removed, so the position is resolved on each access from `interaction`,
|
|
kept up to date with the writes made through the proxy."
|
|
[plugin-id file-id page-id shape-id interaction index]
|
|
(let [current (atom interaction)
|
|
locate-index (fn [] (u/locate-interaction-index file-id page-id shape-id @current index))]
|
|
(obj/reify {:name "InteractionProxy"}
|
|
:$plugin {:enumerable false :get (fn [] plugin-id)}
|
|
:$file {:enumerable false :get (fn [] file-id)}
|
|
:$page {:enumerable false :get (fn [] page-id)}
|
|
:$shape {:enumerable false :get (fn [] shape-id)}
|
|
:$index {:enumerable false :get locate-index}
|
|
|
|
;; Not enumerable so we don't have an infinite loop
|
|
:shape
|
|
{:enumerable false
|
|
:get (fn [] (shape-proxy plugin-id file-id page-id shape-id))}
|
|
|
|
:trigger
|
|
{:this true
|
|
:get #(-> % u/proxy->interaction :event-type format/format-key)
|
|
:set
|
|
(fn [_ value]
|
|
(let [value (parser/parse-keyword value)
|
|
shape (u/locate-shape file-id page-id shape-id)
|
|
index (locate-index)]
|
|
(cond
|
|
(not (ctsi/valid-event-type-for-shape? shape value))
|
|
(u/not-valid plugin-id :trigger value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :trigger "Plugin doesn't have 'content:write' permission")
|
|
|
|
(nil? index)
|
|
(u/not-valid plugin-id :trigger "The interaction is not part of the shape anymore")
|
|
|
|
:else
|
|
(do
|
|
(st/emit! (dwi/update-interaction
|
|
shape
|
|
index
|
|
#(ctsi/set-event-type % value shape)
|
|
{:page-id page-id}))
|
|
(swap! current ctsi/set-event-type value shape)))))}
|
|
|
|
:delay
|
|
{:this true
|
|
:get #(-> % u/proxy->interaction :delay)
|
|
:set
|
|
(fn [_ value]
|
|
(let [index (locate-index)]
|
|
(cond
|
|
(or (not (sm/valid-safe-int? value)) (neg? value))
|
|
(u/not-valid plugin-id :delay value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :delay "Plugin doesn't have 'content:write' permission")
|
|
|
|
(nil? index)
|
|
(u/not-valid plugin-id :delay "The interaction is not part of the shape anymore")
|
|
|
|
:else
|
|
(do
|
|
(st/emit! (dwi/update-interaction
|
|
(u/locate-shape file-id page-id shape-id)
|
|
index
|
|
#(assoc % :delay value)
|
|
{:page-id page-id}))
|
|
(swap! current assoc :delay value)))))}
|
|
|
|
:action
|
|
{:this true
|
|
:get #(-> % u/proxy->interaction (format/format-action plugin-id file-id page-id))
|
|
:set
|
|
(fn [self value]
|
|
(let [shape (u/locate-shape file-id page-id shape-id)
|
|
params (parser/parse-action value)
|
|
index (locate-index)
|
|
interaction (-> (u/proxy->interaction self)
|
|
(d/patch-object params))]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :action "Plugin doesn't have 'content:write' permission")
|
|
|
|
;; Precedes the schema check, which sees only the partial map that
|
|
;; patching a missing interaction produces.
|
|
(nil? index)
|
|
(u/not-valid plugin-id :action "The interaction is not part of the shape anymore")
|
|
|
|
(not (valid-interaction-action? file-id page-id shape value interaction))
|
|
(u/not-valid plugin-id :action interaction)
|
|
|
|
:else
|
|
(do
|
|
(st/emit! (dwi/update-interaction
|
|
shape
|
|
index
|
|
#(d/patch-object % params)
|
|
{:page-id page-id}))
|
|
(reset! current interaction)))))}
|
|
|
|
:remove
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(st/emit! (dwi/remove-interaction {:id shape-id} (locate-index))))))))
|
|
|
|
(def lib-typography-proxy? nil)
|
|
(def lib-component-proxy nil)
|
|
|
|
(defn text-props
|
|
[shape]
|
|
(d/merge
|
|
(dwt/current-root-values {:shape shape :attrs txt/root-attrs})
|
|
(dwt/current-paragraph-values {:shape shape :attrs txt/paragraph-attrs})
|
|
(dwt/current-text-values {:shape shape :attrs txt/text-node-attrs})))
|
|
|
|
(defn- shadow-defaults
|
|
[shadow]
|
|
(d/patch-object
|
|
{:id (uuid/next)
|
|
:style :drop-shadow
|
|
:color {:color clr/black :opacity 0.2}
|
|
:offset-x 4
|
|
:offset-y 4
|
|
:blur 4
|
|
:spread 0
|
|
:hidden false}
|
|
shadow))
|
|
|
|
(defn- blur-defaults
|
|
[blur]
|
|
(d/patch-object
|
|
{:id (uuid/next)
|
|
:type :layer-blur
|
|
:value 4
|
|
:hidden false}
|
|
blur))
|
|
|
|
(defn- background-blur-defaults
|
|
[blur]
|
|
(d/patch-object
|
|
{:id (uuid/next)
|
|
:type :background-blur
|
|
:value 4
|
|
:hidden false}
|
|
blur))
|
|
|
|
(defn commit-fills!
|
|
[plugin-id ^js self value]
|
|
(let [shape (u/proxy->shape self)
|
|
id (:id shape)
|
|
value (parser/parse-fills value)]
|
|
(cond
|
|
(not (sm/validate [:vector types.fills/schema:fill] value))
|
|
(u/not-valid plugin-id :fills value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :fills "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? (obj/get self "$page")))
|
|
(u/not-valid plugin-id :fills "Cannot modify a page that is not currently active")
|
|
|
|
(cfh/text-shape? shape)
|
|
(st/emit! (dwt/update-attrs id {:fills value}))
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :fills value))))))
|
|
|
|
(defn commit-strokes!
|
|
([plugin-id self value]
|
|
(commit-strokes! plugin-id self value nil))
|
|
([plugin-id ^js self value options]
|
|
(let [id (obj/get self "$id")
|
|
value (parser/parse-strokes value)]
|
|
(cond
|
|
(not (sm/validate [:vector cts/schema:stroke] value))
|
|
(u/not-valid plugin-id :strokes value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :strokes "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? (obj/get self "$page")))
|
|
(u/not-valid plugin-id :strokes "Cannot modify a page that is not currently active")
|
|
|
|
(and (not (ctt/per-side-stroke-shape? (:type (u/proxy->shape self))))
|
|
(some #(= :multiple (types.stroke/width-type %)) value))
|
|
(u/not-valid plugin-id :strokes per-side-stroke-widths-message)
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value) options))))))
|
|
|
|
(defn commit-shadows!
|
|
[plugin-id ^js self value]
|
|
(let [id (obj/get self "$id")
|
|
value (mapv #(shadow-defaults (parser/parse-shadow %)) value)]
|
|
(cond
|
|
(not (sm/validate [:vector ctss/schema:shadow] value))
|
|
(u/not-valid plugin-id :shadows value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :shadows "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? (obj/get self "$page")))
|
|
(u/not-valid plugin-id :shadows "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :shadow value))))))
|
|
|
|
(defn commit-exports!
|
|
[plugin-id ^js self value]
|
|
(let [id (obj/get self "$id")
|
|
value (parser/parse-exports value)]
|
|
(cond
|
|
(not (sm/validate [:vector ctse/schema:export] value))
|
|
(u/not-valid plugin-id :exports value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :exports "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? (obj/get self "$page")))
|
|
(u/not-valid plugin-id :exports "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :exports value))))))
|
|
|
|
(defn shape-proxy? [p]
|
|
(obj/type-of? p "ShapeProxy"))
|
|
|
|
;; Cannot use token/token-proxy? here because of circular dependency in applyToShapes in token proxy
|
|
(defn token-proxy? [t]
|
|
(obj/type-of? t "TokenProxy"))
|
|
|
|
(defn- z-order-location
|
|
"Map a plugin z-order intent (:top/:bottom/:up/:down) to the internal
|
|
vertical-order location. Flex layouts store their children in reverse of the
|
|
order the plugin exposes (see the `:children` getter), so front/back and
|
|
forward/backward are inverted for a flex parent under natural child ordering."
|
|
[plugin-id file-id page-id id loc]
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
parent (u/locate-shape file-id page-id (:parent-id shape))
|
|
reversed? (and (u/natural-child-ordering? plugin-id)
|
|
(ctl/flex-layout? parent))]
|
|
(if reversed?
|
|
(case loc :top :bottom :bottom :top :up :down :down :up)
|
|
loc)))
|
|
|
|
(defn shape-proxy
|
|
([plugin-id id]
|
|
(shape-proxy plugin-id (:current-file-id @st/state) (:current-page-id @st/state) id))
|
|
|
|
([plugin-id page-id id]
|
|
(shape-proxy plugin-id (:current-file-id @st/state) page-id id))
|
|
|
|
([plugin-id file-id page-id id]
|
|
(assert (uuid? file-id))
|
|
(assert (uuid? page-id))
|
|
(assert (uuid? id))
|
|
|
|
(let [data (u/locate-shape file-id page-id id)]
|
|
(-> (obj/reify {:name "ShapeProxy"
|
|
:on-error (u/handle-error plugin-id)}
|
|
:$plugin {:enumerable false :get (fn [] plugin-id)}
|
|
:$id {:enumerable false :get (fn [] id)}
|
|
:$file {:enumerable false :get (fn [] file-id)}
|
|
:$page {:enumerable false :get (fn [] page-id)}
|
|
|
|
:id
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :id str)}
|
|
|
|
:type
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :type format/shape-type)}
|
|
|
|
:name
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :name)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")
|
|
value (cpn/normalize-path-name value)]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :name "Cannot modify a page that is not currently active")
|
|
|
|
(nil? value)
|
|
(u/not-valid plugin-id :name value)
|
|
|
|
:else
|
|
(st/emit! (dw/rename-shape-or-variant file-id page-id id value)))))}
|
|
|
|
:blocked
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :blocked boolean)
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (boolean? value))
|
|
(u/not-valid plugin-id :blocked value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :blocked "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :blocked "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")]
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :blocked value))))))}
|
|
|
|
:hidden
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :hidden boolean)
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (boolean? value))
|
|
(u/not-valid plugin-id :hidden value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :hidden "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :hidden "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")]
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :hidden value))))))}
|
|
|
|
:visible
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :hidden boolean not)
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (boolean? value))
|
|
(u/not-valid plugin-id :visible value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :visible "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :visible "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")]
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :hidden (not value)))))))}
|
|
|
|
:proportionLock
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :proportion-lock boolean)
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (boolean? value))
|
|
(u/not-valid plugin-id :proportionLock value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :proportionLock "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :proportionLock "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")]
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :proportion-lock value))))))}
|
|
|
|
:constraintsHorizontal
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :constraints-h d/name)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")
|
|
value (keyword value)]
|
|
(cond
|
|
(not (contains? cts/horizontal-constraint-types value))
|
|
(u/not-valid plugin-id :constraintsHorizontal value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :constraintsHorizontal "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :constraintsHorizontal "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-h value))))))}
|
|
|
|
:constraintsVertical
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :constraints-v d/name)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")
|
|
value (keyword value)]
|
|
(cond
|
|
(not (contains? cts/vertical-constraint-types value))
|
|
(u/not-valid plugin-id :constraintsVertical value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :constraintsVertical "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :constraintsVertical "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-v value))))))}
|
|
|
|
:fixedWhenScrolling
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :fixed-scroll boolean)
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (boolean? value))
|
|
(u/not-valid plugin-id :fixedWhenScrolling value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :fixedWhenScrolling "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")]
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :fixed-scroll value))))))}
|
|
|
|
:borderRadius
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :r1)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")]
|
|
(cond
|
|
(or (not (sm/valid-safe-number? value)) (< value 0))
|
|
(u/not-valid plugin-id :borderRadius value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :borderRadius "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :borderRadius "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-all-corners % value))))))}
|
|
|
|
:borderRadiusTopLeft
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :r1)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")]
|
|
(cond
|
|
(not (sm/valid-non-negative-safe-number? value))
|
|
(u/not-valid plugin-id :borderRadiusTopLeft value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :borderRadiusTopLeft "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :borderRadiusTopLeft "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r1 value))))))}
|
|
|
|
:borderRadiusTopRight
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :r2)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")]
|
|
(cond
|
|
(not (sm/valid-non-negative-safe-number? value))
|
|
(u/not-valid plugin-id :borderRadiusTopRight value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :borderRadiusTopRight "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :borderRadiusTopRight "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r2 value))))))}
|
|
|
|
:borderRadiusBottomRight
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :r3)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")]
|
|
(cond
|
|
(not (sm/valid-non-negative-safe-number? value))
|
|
(u/not-valid plugin-id :borderRadiusBottomRight value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :borderRadiusBottomRight "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :borderRadiusBottomRight "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r3 value))))))}
|
|
|
|
:borderRadiusBottomLeft
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :r4)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")]
|
|
(cond
|
|
(not (sm/valid-non-negative-safe-number? value))
|
|
(u/not-valid plugin-id :borderRadiusBottomLeft value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :borderRadiusBottomLeft "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :borderRadiusBottomLeft "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r4 value))))))}
|
|
|
|
:opacity
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :opacity)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")]
|
|
(cond
|
|
(or (not (sm/valid-safe-number? value)) (< value 0) (> value 1))
|
|
(u/not-valid plugin-id :opacity value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :opacity "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :opacity "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :opacity value))))))}
|
|
|
|
:blendMode
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :blend-mode (d/nilv :normal) d/name)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")
|
|
value (keyword value)]
|
|
(cond
|
|
(not (contains? cts/blend-modes value))
|
|
(u/not-valid plugin-id :blendMode value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :blendMode "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :blendMode "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :blend-mode value))))))}
|
|
|
|
:shadows
|
|
{:this true
|
|
:get (fn [^js self]
|
|
(shadows/format-shadows (-> self u/proxy->shape :shadow)
|
|
#(commit-shadows! plugin-id self %)))
|
|
:set (fn [self value] (commit-shadows! plugin-id self value))}
|
|
|
|
:blur
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :blur format/format-blur)
|
|
:set
|
|
(fn [self value]
|
|
(if (nil? value)
|
|
(st/emit! (dwsh/update-shapes [id] #(dissoc % :blur)))
|
|
(let [id (obj/get self "$id")
|
|
value (blur-defaults (parser/parse-blur value))
|
|
value (assoc value :type :layer-blur)]
|
|
(cond
|
|
(not (sm/validate ctsb/schema:blur value))
|
|
(u/not-valid plugin-id :blur value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :blur "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :blur "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))}
|
|
|
|
:backgroundBlur
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :background-blur format/format-blur)
|
|
:set
|
|
(fn [self value]
|
|
(if (nil? value)
|
|
(st/emit! (dwsh/update-shapes [id] #(dissoc % :background-blur)))
|
|
(let [id (obj/get self "$id")
|
|
value (background-blur-defaults (parser/parse-blur value))
|
|
value (assoc value :type :background-blur)]
|
|
(cond
|
|
(not (sm/validate ctsbb/schema:background-blur value))
|
|
(u/not-valid plugin-id :background-blur value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :background-blur "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :background-blur "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :background-blur value)))))))}
|
|
|
|
:exports
|
|
{:this true
|
|
:get (fn [^js self]
|
|
(exports/format-exports (-> self u/proxy->shape :exports)
|
|
#(commit-exports! plugin-id self %)))
|
|
:set (fn [self value] (commit-exports! plugin-id self value))}
|
|
|
|
;; Geometry properties
|
|
:x
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :points grc/points->rect :x)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")]
|
|
(cond
|
|
(not (sm/valid-safe-number? value))
|
|
(u/not-valid plugin-id :x value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :x "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :x "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dw/update-position id
|
|
{:x value}
|
|
{:absolute? true})))))}
|
|
|
|
:y
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :points grc/points->rect :y)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")]
|
|
(cond
|
|
(not (sm/valid-safe-number? value))
|
|
(u/not-valid plugin-id :y value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :y "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :y "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dw/update-position id
|
|
{:y value}
|
|
{:absolute? true})))))}
|
|
|
|
:parent
|
|
{:this true
|
|
;; not enumerable so there are no infinite loops
|
|
:enumerable false
|
|
:get (fn [self]
|
|
(let [shape (u/proxy->shape self)]
|
|
(when-not (cfh/root? shape)
|
|
(let [parent-id (:parent-id shape)]
|
|
(shape-proxy plugin-id (obj/get self "$file") (obj/get self "$page") parent-id)))))}
|
|
|
|
:parentIndex
|
|
{:this true
|
|
:get
|
|
(fn [self]
|
|
(let [shape (u/proxy->shape self)]
|
|
(if (cfh/root? shape)
|
|
0
|
|
(let [file-id (obj/get self "$file")
|
|
page-id (obj/get self "$page")
|
|
parent (u/locate-shape file-id page-id (:parent-id shape))
|
|
index (d/index-of (:shapes parent) id)]
|
|
index))))}
|
|
|
|
:parentX
|
|
{:this true
|
|
:get (fn [self]
|
|
(let [shape (u/proxy->shape self)
|
|
shape-x (-> shape :points grc/points->rect :x)
|
|
parent-id (:parent-id shape)
|
|
parent (u/locate-shape (obj/get self "$file") (obj/get self "$page") parent-id)]
|
|
(- shape-x (:x parent))))
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (sm/valid-safe-number? value))
|
|
(u/not-valid plugin-id :parentX value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :parentX "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :parentX "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")
|
|
parent-id (-> self u/proxy->shape :parent-id)
|
|
parent (u/locate-shape (obj/get self "$file") (obj/get self "$page") parent-id)
|
|
parent-x (:x parent)]
|
|
(st/emit! (dw/update-position id
|
|
{:x (+ parent-x value)}
|
|
{:absolute? true})))))}
|
|
|
|
:parentY
|
|
{:this true
|
|
:get (fn [self]
|
|
(let [shape (u/proxy->shape self)
|
|
shape-y (-> shape :points grc/points->rect :y)
|
|
parent-id (:parent-id shape)
|
|
parent (u/locate-shape (obj/get self "$file") (obj/get self "$page") parent-id)
|
|
parent-y (:y parent)]
|
|
(- shape-y parent-y)))
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (sm/valid-safe-number? value))
|
|
(u/not-valid plugin-id :parentY value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :parentY "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :parentY "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")
|
|
parent-id (-> self u/proxy->shape :parent-id)
|
|
parent (u/locate-shape (obj/get self "$file") (obj/get self "$page") parent-id)
|
|
parent-y (:y parent)]
|
|
(st/emit! (dw/update-position id
|
|
{:y (+ parent-y value)}
|
|
{:absolute? true})))))}
|
|
|
|
:boardX
|
|
{:this true
|
|
:get (fn [self]
|
|
(let [shape (u/proxy->shape self)
|
|
shape-x (-> shape :points grc/points->rect :x)
|
|
frame-id (:parent-id shape)
|
|
frame (u/locate-shape (obj/get self "$file") (obj/get self "$page") frame-id)
|
|
frame-x (:x frame)]
|
|
(- shape-x frame-x)))
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (sm/valid-safe-number? value))
|
|
(u/not-valid plugin-id :frameX value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :frameX "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :frameX "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")
|
|
frame-id (-> self u/proxy->shape :frame-id)
|
|
frame (u/locate-shape (obj/get self "$file") (obj/get self "$page") frame-id)
|
|
frame-x (:x frame)]
|
|
(st/emit! (dw/update-position id
|
|
{:x (+ frame-x value)}
|
|
{:absolute? true})))))}
|
|
|
|
:boardY
|
|
{:this true
|
|
:get (fn [self]
|
|
(let [shape (u/proxy->shape self)
|
|
shape-y (-> shape :points grc/points->rect :y)
|
|
frame-id (:parent-id shape)
|
|
frame (u/locate-shape (obj/get self "$file") (obj/get self "$page") frame-id)
|
|
frame-y (:y frame)]
|
|
(- shape-y frame-y)))
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (sm/valid-safe-number? value))
|
|
(u/not-valid plugin-id :frameY value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :frameY "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :frameY "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")
|
|
frame-id (-> self u/proxy->shape :frame-id)
|
|
frame (u/locate-shape (obj/get self "$file") (obj/get self "$page") frame-id)
|
|
frame-y (:y frame)]
|
|
(st/emit! (dw/update-position id
|
|
{:y (+ frame-y value)}
|
|
{:absolute? true})))))}
|
|
|
|
:width
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :selrect :width)}
|
|
|
|
:height
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :selrect :height)}
|
|
|
|
:bounds
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :points grc/points->rect format/format-bounds)}
|
|
|
|
:center
|
|
{:this true
|
|
:get #(-> % u/proxy->shape gsh/shape->center format/format-point)}
|
|
|
|
:rotation
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :rotation)
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (sm/valid-safe-number? value))
|
|
(u/not-valid plugin-id :rotation value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :rotation "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :rotation "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [shape (u/proxy->shape self)]
|
|
(st/emit! (dw/increase-rotation #{(:id shape)} value)))))}
|
|
|
|
:flipX
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :flip-x boolean)
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (boolean? value))
|
|
(u/not-valid plugin-id :flipX value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :flipX "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :flipX "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")]
|
|
(st/emit! (dw/flip-horizontal-selected #{id})))))}
|
|
|
|
:flipY
|
|
{:this true
|
|
:get #(-> % u/proxy->shape :flip-y boolean)
|
|
:set
|
|
(fn [self value]
|
|
(cond
|
|
(not (boolean? value))
|
|
(u/not-valid plugin-id :flipY value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :flipY "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :flipY "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [id (obj/get self "$id")]
|
|
(st/emit! (dw/flip-vertical-selected #{id})))))}
|
|
|
|
;; Strokes and fills
|
|
:fills
|
|
{:this true
|
|
:get (fn [^js self]
|
|
(let [fill-data (if (cfh/text-shape? data)
|
|
(-> self u/proxy->shape text-props :fills)
|
|
(-> self u/proxy->shape :fills))]
|
|
(fills/format-fills fill-data #(commit-fills! plugin-id self %))))
|
|
:set (fn [self value] (commit-fills! plugin-id self value))}
|
|
|
|
:strokes
|
|
{:this true
|
|
:get (fn [^js self]
|
|
(let [shape (u/proxy->shape self)]
|
|
(strokes/format-strokes
|
|
(:strokes shape)
|
|
#(commit-strokes! plugin-id self %1 %2)
|
|
{:per-side-allowed? (ctt/per-side-stroke-shape? (:type shape))
|
|
:not-valid (fn [code value] (u/not-valid plugin-id code value))})))
|
|
:set (fn [self value] (commit-strokes! plugin-id self value))}
|
|
|
|
:layoutChild
|
|
{:this true
|
|
:get
|
|
(fn [self]
|
|
(let [file-id (obj/get self "$file")
|
|
page-id (obj/get self "$page")
|
|
id (obj/get self "$id")
|
|
objects (u/locate-objects file-id page-id)]
|
|
(when (ctl/any-layout-immediate-child-id? objects id)
|
|
(flex/layout-child-proxy plugin-id file-id page-id id))))}
|
|
|
|
:layoutCell
|
|
{:this true
|
|
:get
|
|
(fn [self]
|
|
(let [file-id (obj/get self "$file")
|
|
page-id (obj/get self "$page")
|
|
id (obj/get self "$id")
|
|
objects (u/locate-objects file-id page-id)]
|
|
(when (ctl/grid-layout-immediate-child-id? objects id)
|
|
(grid/layout-cell-proxy plugin-id file-id page-id id))))}
|
|
|
|
;; Interactions
|
|
|
|
:interactions
|
|
{:this true
|
|
:get
|
|
(fn [self]
|
|
(let [interactions (-> self u/proxy->shape :interactions)]
|
|
(format/format-array
|
|
(fn [[index interaction]]
|
|
(interaction-proxy plugin-id file-id page-id id interaction index))
|
|
(d/enumerate interactions))))}
|
|
|
|
;; Methods
|
|
:resize
|
|
(fn [width height]
|
|
(cond
|
|
(or (not (sm/valid-safe-number? width)) (<= width 0))
|
|
(u/not-valid plugin-id :resize width)
|
|
|
|
(or (not (sm/valid-safe-number? height)) (<= height 0))
|
|
(u/not-valid plugin-id :resize height)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :resize "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :resize "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
;; A layout container that hugs its content (or a hugging/filling
|
|
;; layout child) ignores explicit dimensions and snaps back, so
|
|
;; switch the non-fixed axes to fixed sizing first, mirroring an
|
|
;; interactive drag resize. The sizing change must commit before
|
|
;; the resize, otherwise the layout reflows the new size away.
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
objects (u/locate-objects file-id page-id)
|
|
layout? (or (ctl/any-layout-immediate-child? objects shape)
|
|
(ctl/any-layout? shape))
|
|
sizing (cond-> {}
|
|
(and layout? (not= (:layout-item-h-sizing shape) :fix))
|
|
(assoc :layout-item-h-sizing :fix)
|
|
|
|
(and layout? (not= (:layout-item-v-sizing shape) :fix))
|
|
(assoc :layout-item-v-sizing :fix))]
|
|
(apply st/emit!
|
|
(cond-> []
|
|
(seq sizing)
|
|
(conj (dwsl/update-layout #{id} sizing))
|
|
|
|
:always
|
|
(conj (dw/update-dimensions [id] :width width)
|
|
(dw/update-dimensions [id] :height height)))))))
|
|
|
|
:rotate
|
|
(fn [angle center]
|
|
(let [center (when center {:x (obj/get center "x") :y (obj/get center "y")})]
|
|
(cond
|
|
(not (sm/valid-safe-number? angle))
|
|
(u/not-valid plugin-id :rotate-angle angle)
|
|
|
|
(and (some? center) (or (not (sm/valid-safe-number? (:x center))) (not (sm/valid-safe-number? (:y center)))))
|
|
(u/not-valid plugin-id :rotate-center center)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :rotate "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :rotate "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dw/increase-rotation [id] angle {:center center :delta? true})))))
|
|
|
|
:clone
|
|
(fn []
|
|
(let [ret-v (atom nil)]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :clone "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :clone "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(do (st/emit! (dws/duplicate-shapes #{id} :change-selection? false :return-ref ret-v))
|
|
(shape-proxy plugin-id (deref ret-v))))))
|
|
|
|
:remove
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :remove "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/delete-shapes #{id}))))
|
|
|
|
:waitForLayoutUpdate
|
|
(fn [timeout]
|
|
;; Wait for layout work that can affect this shape.
|
|
(let [objects (u/locate-objects file-id page-id)]
|
|
(wrfp/wait-for-layout-update
|
|
(wrfp/shape-wait-ids objects file-id id)
|
|
timeout)))
|
|
|
|
;; Plugin data
|
|
:getPluginData
|
|
(fn [key]
|
|
(cond
|
|
(not (string? key))
|
|
(u/not-valid plugin-id :getPluginData key)
|
|
|
|
:else
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(dm/get-in shape [:plugin-data (keyword "plugin" (str plugin-id)) key]))))
|
|
|
|
:setPluginData
|
|
(fn [key value]
|
|
(cond
|
|
(not (string? key))
|
|
(u/not-valid plugin-id :setPluginData-key key)
|
|
|
|
(and (some? value) (not (string? value)))
|
|
(u/not-valid plugin-id :setPluginData-value value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :setPluginData "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "plugin" (str plugin-id)) key value))))
|
|
|
|
:getPluginDataKeys
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(apply array (keys (dm/get-in shape [:plugin-data (keyword "plugin" (str plugin-id))])))))
|
|
|
|
:getSharedPluginData
|
|
(fn [namespace key]
|
|
(cond
|
|
(not (string? namespace))
|
|
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
|
|
|
|
(not (string? key))
|
|
(u/not-valid plugin-id :getSharedPluginData-key key)
|
|
|
|
:else
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(dm/get-in shape [:plugin-data (keyword "shared" namespace) key]))))
|
|
|
|
:setSharedPluginData
|
|
(fn [namespace key value]
|
|
(cond
|
|
(not (string? namespace))
|
|
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
|
|
|
(not (string? key))
|
|
(u/not-valid plugin-id :setSharedPluginData-key key)
|
|
|
|
(and (some? value) (not (string? value)))
|
|
(u/not-valid plugin-id :setSharedPluginData-value value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :setSharedPluginData "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "shared" namespace) key value))))
|
|
|
|
:getSharedPluginDataKeys
|
|
(fn [namespace]
|
|
(cond
|
|
(not (string? namespace))
|
|
(u/not-valid plugin-id :getSharedPluginDataKeys namespace)
|
|
|
|
:else
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(apply array (keys (dm/get-in shape [:plugin-data (keyword "shared" namespace)]))))))
|
|
|
|
;; Only for frames + groups + booleans
|
|
:getChildren
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(cond
|
|
(and (not (cfh/frame-shape? shape))
|
|
(not (cfh/group-shape? shape))
|
|
(not (cfh/svg-raw-shape? shape))
|
|
(not (cfh/bool-shape? shape)))
|
|
(u/not-valid plugin-id :getChildren (:type shape))
|
|
|
|
:else
|
|
(let [is-reversed? (ctl/flex-layout? shape)
|
|
reverse-fn
|
|
(if (and (u/natural-child-ordering? plugin-id) is-reversed?)
|
|
reverse identity)]
|
|
(->> (u/locate-shape file-id page-id id)
|
|
(:shapes)
|
|
(reverse-fn)
|
|
(format/format-array #(shape-proxy plugin-id file-id page-id %)))))))
|
|
|
|
:appendChild
|
|
(fn [child]
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
valid-child? (shape-proxy? child)
|
|
child-page (when valid-child? (obj/get child "$page"))
|
|
child-id (when valid-child? (obj/get child "$id"))
|
|
objects (when valid-child? (u/locate-objects file-id page-id))
|
|
child-shape (when valid-child? (u/locate-shape file-id page-id child-id))
|
|
is-reversed? (ctl/flex-layout? shape)
|
|
index (if (or (not (u/natural-child-ordering? plugin-id)) is-reversed?)
|
|
0
|
|
(count (:shapes shape)))]
|
|
(cond
|
|
(and (not (cfh/frame-shape? shape))
|
|
(not (cfh/group-shape? shape))
|
|
(not (cfh/svg-raw-shape? shape))
|
|
(not (cfh/bool-shape? shape)))
|
|
(u/not-valid plugin-id :appendChild (:type shape))
|
|
|
|
(not valid-child?)
|
|
(u/not-valid plugin-id :appendChild-child child)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :appendChild "Plugin doesn't have 'content:write' permission")
|
|
|
|
(or (not (u/page-active? page-id))
|
|
(not (u/page-active? child-page)))
|
|
(u/not-valid plugin-id :appendChild "Cannot modify a page that is not currently active")
|
|
|
|
(u/changes-component-copy-structure? objects shape child-shape)
|
|
(u/not-valid plugin-id :appendChild "Cannot change the structure of a component copy")
|
|
|
|
:else
|
|
(st/emit!
|
|
(dwsh/relocate-shapes #{child-id} id index)
|
|
(se/event plugin-id (if (ctl/any-layout? shape) "add-layout-element" "add-element")
|
|
:type (:type child-shape)
|
|
:parent-type (:type shape))))))
|
|
|
|
:insertChild
|
|
(fn [index child]
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
valid-child? (shape-proxy? child)
|
|
child-page (when valid-child? (obj/get child "$page"))
|
|
child-id (when valid-child? (obj/get child "$id"))
|
|
objects (when valid-child? (u/locate-objects file-id page-id))
|
|
child-shape (when valid-child? (u/locate-shape file-id page-id child-id))
|
|
is-reversed? (ctl/flex-layout? shape)
|
|
index (if (or (not (u/natural-child-ordering? plugin-id)) is-reversed?)
|
|
(- (count (:shapes shape)) index)
|
|
index)]
|
|
(cond
|
|
(and (not (cfh/frame-shape? shape))
|
|
(not (cfh/group-shape? shape))
|
|
(not (cfh/svg-raw-shape? shape))
|
|
(not (cfh/bool-shape? shape)))
|
|
(u/not-valid plugin-id :insertChild (:type shape))
|
|
|
|
(not valid-child?)
|
|
(u/not-valid plugin-id :insertChild-child child)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :insertChild "Plugin doesn't have 'content:write' permission")
|
|
|
|
(or (not (u/page-active? page-id))
|
|
(not (u/page-active? child-page)))
|
|
(u/not-valid plugin-id :insertChild "Cannot modify a page that is not currently active")
|
|
|
|
(u/changes-component-copy-structure? objects shape child-shape)
|
|
(u/not-valid plugin-id :insertChild "Cannot change the structure of a component copy")
|
|
|
|
:else
|
|
(st/emit!
|
|
(dwsh/relocate-shapes #{child-id} id index)
|
|
(se/event plugin-id (if (ctl/any-layout? shape) "add-layout-element" "add-element")
|
|
:type (:type child-shape)
|
|
:parent-type (:type shape))))))
|
|
|
|
;; Only for frames
|
|
:addFlexLayout
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(cond
|
|
(not (cfh/frame-shape? shape))
|
|
(u/not-valid plugin-id :addFlexLayout (:type shape))
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :addFlexLayout "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :addFlexLayout "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(do (st/emit!
|
|
(dwsl/create-layout-from-id id :flex :from-frame? true :calculate-params? false)
|
|
(se/event plugin-id "create-shape-layout" :layout "flex"))
|
|
(flex/flex-layout-proxy plugin-id file-id page-id id)))))
|
|
|
|
:addGridLayout
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(cond
|
|
(not (cfh/frame-shape? shape))
|
|
(u/not-valid plugin-id :addGridLayout (:type shape))
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :addGridLayout "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :addGridLayout "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(do (st/emit!
|
|
(dwsl/create-layout-from-id id :grid :from-frame? true :calculate-params? false)
|
|
(se/event plugin-id "create-shape-layout" :layout "grid"))
|
|
(grid/grid-layout-proxy plugin-id file-id page-id id)))))
|
|
|
|
;; Make masks for groups
|
|
:makeMask
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(cond
|
|
(not (cfh/group-shape? shape))
|
|
(u/not-valid plugin-id :makeMask (:type shape))
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :makeMask "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :makeMask "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit!
|
|
(dwg/mask-group #{id})
|
|
(se/event plugin-id "create-shape" :type "mask")))))
|
|
|
|
:removeMask
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(cond
|
|
(not (cfh/mask-shape? shape))
|
|
(u/not-valid plugin-id :removeMask (:type shape))
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :removeMask "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :removeMask "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwg/unmask-group #{id})))))
|
|
|
|
:isMask
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(boolean (cfh/mask-shape? shape))))
|
|
|
|
;; Only for path and bool shapes
|
|
:toD
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(cond
|
|
(and (not (cfh/path-shape? shape)) (not (cfh/bool-shape? shape)))
|
|
(u/not-valid plugin-id :toD (:type shape))
|
|
|
|
:else
|
|
(.toString (:content shape)))))
|
|
|
|
;; Text shapes
|
|
:getRange
|
|
(fn [start end]
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(cond
|
|
(not (cfh/text-shape? shape))
|
|
(u/not-valid plugin-id :getRange-shape "shape is not text")
|
|
|
|
(or (not (sm/valid-safe-int? start)) (< start 0) (> start end))
|
|
(u/not-valid plugin-id :getRange-start start)
|
|
|
|
(not (sm/valid-safe-int? end))
|
|
(u/not-valid plugin-id :getRange-end end)
|
|
|
|
:else
|
|
;; Clamp the end to the actual character count so an
|
|
;; out-of-bounds range yields the trailing text instead of
|
|
;; reading past the content.
|
|
(let [end (min end (count (txt/content->text (:content shape))))]
|
|
(text/text-range-proxy plugin-id file-id page-id id start end)))))
|
|
|
|
:applyTypography
|
|
(fn [typography]
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(cond
|
|
(not (lib-typography-proxy? typography))
|
|
(u/not-valid plugin-id :applyTypography-typography typography)
|
|
|
|
(not (cfh/text-shape? shape))
|
|
(u/not-valid plugin-id :applyTypography-shape (:type shape))
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :applyTypography "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :applyTypography "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [typography (u/proxy->library-typography typography)]
|
|
(st/emit! (dwt/apply-typography #{id} typography file-id))))))
|
|
|
|
;; Change index method
|
|
:setParentIndex
|
|
(fn [index]
|
|
(let [objects (u/locate-objects file-id page-id)
|
|
shape (get objects id)]
|
|
(cond
|
|
(not (sm/valid-safe-int? index))
|
|
(u/not-valid plugin-id :setParentIndex index)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :setParentIndex "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :setParentIndex "Cannot modify a page that is not currently active")
|
|
|
|
(u/inside-component-copy? objects shape)
|
|
(u/not-valid plugin-id :setParentIndex "Cannot change the structure of a component copy")
|
|
|
|
:else
|
|
(st/emit! (dw/set-shape-index file-id page-id id index)))))
|
|
|
|
:bringForward
|
|
(fn []
|
|
(st/emit! (dw/vertical-order-selected (z-order-location plugin-id file-id page-id id :up) [id])))
|
|
|
|
:sendBackward
|
|
(fn []
|
|
(st/emit! (dw/vertical-order-selected (z-order-location plugin-id file-id page-id id :down) [id])))
|
|
|
|
:bringToFront
|
|
(fn []
|
|
(st/emit! (dw/vertical-order-selected (z-order-location plugin-id file-id page-id id :top) [id])))
|
|
|
|
:sendToBack
|
|
(fn []
|
|
(st/emit! (dw/vertical-order-selected (z-order-location plugin-id file-id page-id id :bottom) [id])))
|
|
|
|
;; COMPONENTS
|
|
:isComponentInstance
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
objects (u/locate-objects file-id page-id)]
|
|
(ctn/in-any-component? objects shape)))
|
|
|
|
:isComponentMainInstance
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
objects (u/locate-objects file-id page-id)]
|
|
(ctn/inside-component-main? objects shape)))
|
|
|
|
:isComponentCopyInstance
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(ctk/in-component-copy? shape)))
|
|
|
|
:isComponentRoot
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(ctk/instance-root? shape)))
|
|
|
|
:isComponentHead
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(ctk/instance-head? shape)))
|
|
|
|
:componentRefShape
|
|
(fn []
|
|
(let [objects (u/locate-objects file-id page-id)
|
|
shape (u/locate-shape file-id page-id id)]
|
|
(when (ctn/in-any-component? objects shape)
|
|
(let [[root component] (u/locate-component objects shape)
|
|
component-page-id (:main-instance-page component)
|
|
component-file (u/locate-file (:component-file root))
|
|
ref-shape (ctf/get-ref-shape (:data component-file) component shape)]
|
|
(when (and (not (:deleted component)) (some? ref-shape) (some? component-file))
|
|
(shape-proxy plugin-id (:id component-file) component-page-id (:id ref-shape)))))))
|
|
|
|
:componentRoot
|
|
(fn []
|
|
(let [objects (u/locate-objects file-id page-id)
|
|
shape (u/locate-shape file-id page-id id)]
|
|
(when (ctn/in-any-component? objects shape)
|
|
(let [[root component] (u/locate-component objects shape)]
|
|
(shape-proxy plugin-id (:component-file root) (:main-instance-page component) (:id root))))))
|
|
|
|
:componentHead
|
|
(fn []
|
|
(let [objects (u/locate-objects file-id page-id)
|
|
shape (u/locate-shape file-id page-id id)]
|
|
(when (ctn/in-any-component? objects shape)
|
|
(let [head (ctn/get-head-shape (u/locate-objects file-id page-id) shape)]
|
|
(shape-proxy plugin-id file-id page-id (:id head))))))
|
|
|
|
:component
|
|
(fn []
|
|
(let [objects (u/locate-objects file-id page-id)
|
|
shape (u/locate-shape file-id page-id id)]
|
|
(when (ctn/in-any-component? objects shape)
|
|
(when-let [[head component] (u/locate-head-component objects shape)]
|
|
(lib-component-proxy plugin-id (:component-file head) (:id component))))))
|
|
|
|
:detach
|
|
(fn []
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :detach "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :detach "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwl/detach-component id))))
|
|
|
|
:swapComponent
|
|
(fn [component]
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
objects (u/locate-objects file-id page-id)
|
|
valid-component? (obj/type-of? component "LibraryComponentProxy")
|
|
target-file (when valid-component? (obj/get component "$file"))
|
|
target-id (when valid-component? (obj/get component "$id"))
|
|
target-data (some-> (u/locate-file target-file) :data)]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :swapComponent "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :swapComponent "Cannot modify a page that is not currently active")
|
|
|
|
(not valid-component?)
|
|
(u/not-valid plugin-id :swapComponent "Component not valid")
|
|
|
|
(not (ctk/in-component-copy? shape))
|
|
(u/not-valid plugin-id :swapComponent "The shape is not a component copy instance")
|
|
|
|
(clv/swap-nesting-loop? objects shape target-data target-id)
|
|
(u/not-valid plugin-id :swapComponent "The swap would create a component nesting loop")
|
|
|
|
:else
|
|
(st/emit! (dwl/component-swap shape
|
|
target-file
|
|
target-id
|
|
true)))))
|
|
|
|
:updateMain
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
objects (u/locate-objects file-id page-id)
|
|
component (when (:component-id shape)
|
|
(u/locate-library-component (:component-file shape) (:component-id shape)))]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :updateMain "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (r/check-permission plugin-id "library:write"))
|
|
(u/not-valid plugin-id :updateMain "Plugin doesn't have 'library:write' permission")
|
|
|
|
(not= file-id (:current-file-id @st/state))
|
|
(u/not-valid plugin-id :updateMain "Can only update a component from a copy in the current file")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :updateMain "Cannot modify a page that is not currently active")
|
|
|
|
(not (and (ctk/instance-head? shape) (ctk/in-component-copy? shape)))
|
|
(u/not-valid plugin-id :updateMain "The shape is not a component copy head")
|
|
|
|
(u/inside-component-copy? objects shape)
|
|
(u/not-valid plugin-id :updateMain "Cannot update the main from a shape inside another component copy")
|
|
|
|
(or (nil? component) (:deleted component))
|
|
(u/not-valid plugin-id :updateMain "The main component does not exist or is deleted")
|
|
|
|
:else
|
|
(st/emit! (-> (dwl/update-component-sync id (:component-file shape))
|
|
(se/add-event plugin-id))))))
|
|
|
|
:resetOverrides
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :resetOverrides "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :resetOverrides "Cannot modify a page that is not currently active")
|
|
|
|
(not (ctk/in-component-copy? shape))
|
|
(u/not-valid plugin-id :resetOverrides "The shape is not a component copy instance")
|
|
|
|
:else
|
|
(st/emit! (dwl/reset-component id)))))
|
|
|
|
;; Export
|
|
:export
|
|
(fn [value]
|
|
(let [value (parser/parse-export value)]
|
|
(cond
|
|
(not (sm/validate ctse/schema:export value))
|
|
(u/not-valid plugin-id :export value)
|
|
|
|
(not (r/check-permission plugin-id "content:read"))
|
|
(u/not-valid plugin-id :export "Plugin doesn't have 'content:read' permission")
|
|
|
|
:else
|
|
(let [export-type (:type value :png)
|
|
wasm-enabled? (features/active-feature? @st/state "render-wasm/v1")]
|
|
(if (and wasm-enabled?
|
|
(contains? #{:jpeg :webp :png :pdf :svg} export-type))
|
|
;; Render in the browser with render-wasm.
|
|
(let [export {:file-id file-id
|
|
:page-id page-id
|
|
:object-id id
|
|
:type export-type
|
|
:scale (:scale value 1)}
|
|
uri (case export-type
|
|
:pdf (wasm.exports/export-pdf-uri export)
|
|
:svg (wasm.exports/export-svg-uri export)
|
|
(wasm.exports/export-image-uri export))]
|
|
(js/Promise.
|
|
(fn [resolve reject]
|
|
(->> (http/send!
|
|
{:method :get
|
|
:uri uri
|
|
:response-type :blob
|
|
:omit-default-headers true})
|
|
(rx/map :body)
|
|
(rx/mapcat #(.arrayBuffer %))
|
|
(rx/map #(js/Uint8Array. %))
|
|
(rx/tap #(st/emit! (se/event plugin-id "export-shapes" :method "wasm")))
|
|
(rx/subs! resolve reject)))))
|
|
|
|
;; Render via the exporter service.
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
payload
|
|
{:cmd :export-shapes
|
|
:profile-id (:profile-id @st/state)
|
|
:wait true
|
|
:is-wasm wasm-enabled?
|
|
:exports [(de/normalize-export {:file-id file-id
|
|
:page-id page-id
|
|
:object-id id
|
|
:name (:name shape)
|
|
:type export-type
|
|
:suffix (:suffix value "")
|
|
:scale (:scale value 1)})]}]
|
|
(js/Promise.
|
|
(fn [resolve reject]
|
|
;; Exporter reads persisted file state; flush first
|
|
;; so the export is not stale. Browser WASM above
|
|
;; renders from local state and needs no flush.
|
|
(st/emit! ::dwp/force-persist)
|
|
(->> (rx/concat
|
|
(->> (dwp/wait-persisted 5000)
|
|
(rx/ignore))
|
|
(rp/cmd! :export payload))
|
|
(rx/mapcat (fn [{:keys [uri]}]
|
|
(->> (http/send! {:method :get
|
|
:uri uri
|
|
:response-type :blob
|
|
:omit-default-headers true})
|
|
(rx/map :body))))
|
|
(rx/mapcat #(.arrayBuffer %))
|
|
(rx/map #(js/Uint8Array. %))
|
|
(rx/tap #(st/emit! (se/event plugin-id "export-shapes" :method "exporter")))
|
|
(rx/subs! resolve reject))))))))))
|
|
|
|
;; Interactions
|
|
:addInteraction
|
|
(fn [trigger action delay]
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
event-type (parser/parse-keyword trigger)
|
|
interaction (when (ctsi/valid-event-type-for-shape? shape event-type)
|
|
(-> ctsi/default-interaction
|
|
(ctsi/set-event-type event-type shape)
|
|
(d/patch-object (parser/parse-action action))
|
|
(cond-> (some? delay) (assoc :delay delay))))]
|
|
(cond
|
|
(not (valid-interaction-action? file-id page-id shape action interaction))
|
|
(u/not-valid plugin-id :addInteraction interaction)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :addInteraction "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(let [index (-> (u/locate-shape file-id page-id id) (:interactions []) count)]
|
|
(st/emit!
|
|
(dwi/add-interaction page-id id interaction)
|
|
(se/event plugin-id "add-interaction"))
|
|
(interaction-proxy plugin-id file-id page-id id interaction index)))))
|
|
|
|
:removeInteraction
|
|
(fn [interaction]
|
|
(cond
|
|
(not (interaction-proxy? interaction))
|
|
(u/not-valid plugin-id :removeInteraction interaction)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :removeInteraction "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not= id (obj/get interaction "$shape"))
|
|
(u/not-valid plugin-id :removeInteraction "The interaction doesn't belong to this shape")
|
|
|
|
:else
|
|
(st/emit!
|
|
(dwi/remove-interaction {:id id} (obj/get interaction "$index"))
|
|
(se/event plugin-id "remove-interaction"))))
|
|
|
|
;; Ruler guides
|
|
:addRulerGuide
|
|
(fn [orientation value]
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(cond
|
|
(not (sm/valid-safe-number? value))
|
|
(u/not-valid plugin-id :addRulerGuide "Value not a safe number")
|
|
|
|
(not (contains? #{"vertical" "horizontal"} orientation))
|
|
(u/not-valid plugin-id :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
|
|
|
|
(not (cfh/frame-shape? shape))
|
|
(u/not-valid plugin-id :addRulerGuide "The shape is not a board")
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :addRulerGuide "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :addRulerGuide "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [ruler-id (uuid/next)
|
|
axis (parser/orientation->axis orientation)
|
|
objects (u/locate-objects file-id page-id)
|
|
frame (get objects id)
|
|
board-pos (get frame axis)
|
|
position (+ board-pos value)]
|
|
(st/emit!
|
|
(-> (dwgu/update-guides
|
|
{:id ruler-id
|
|
:axis axis
|
|
:position position
|
|
:frame-id id})
|
|
(se/add-event plugin-id)))
|
|
(rg/ruler-guide-proxy plugin-id file-id page-id ruler-id)))))
|
|
|
|
:removeRulerGuide
|
|
(fn [value]
|
|
(cond
|
|
(not (rg/ruler-guide-proxy? value))
|
|
(u/not-valid plugin-id :removeRulerGuide "Guide not provided")
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :removeRulerGuide "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :removeRulerGuide "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(let [guide (u/proxy->ruler-guide value)]
|
|
(st/emit! (-> (dwgu/remove-guide guide)
|
|
(se/add-event plugin-id))))))
|
|
|
|
:tokens
|
|
{:this true
|
|
:get
|
|
(fn [_]
|
|
(let [applied-tokens
|
|
(-> (u/locate-shape file-id page-id id)
|
|
(get :applied-tokens)
|
|
(applied-tokens-plugin->applied-tokens))]
|
|
(reduce
|
|
(fn [acc [prop name]]
|
|
(obj/set! acc (json/write-camel-key prop) name))
|
|
#js {}
|
|
applied-tokens)))}
|
|
|
|
:applyToken
|
|
{:enumerable false
|
|
:schema [:tuple
|
|
[:fn token-proxy?]
|
|
[:maybe [::sm/set [:and ::sm/keyword [:fn token-attr?]]]]]
|
|
:fn (fn [token attrs]
|
|
(let [set-id (obj/get token "$set-id")
|
|
token-id (obj/get token "$id")
|
|
token (u/locate-token file-id set-id token-id)
|
|
kw-attrs (expand-token-attrs attrs)]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :applyToken "Plugin doesn't have 'content:write' permission")
|
|
|
|
(nil? token)
|
|
(u/not-valid plugin-id :applyToken token-id)
|
|
|
|
(not (valid-token-resolution? file-id set-id token-id))
|
|
(u/not-valid plugin-id :applyToken (:value token))
|
|
|
|
(some #(not (token-attr? %)) kw-attrs)
|
|
(u/not-valid plugin-id :applyToken attrs)
|
|
|
|
;; The uniform `strokeWidth` alias is exempt: it expands
|
|
;; to a uniform width on every side.
|
|
(per-side-stroke-attrs-restricted? file-id page-id [id] attrs)
|
|
(u/not-valid plugin-id :applyToken per-side-stroke-widths-message)
|
|
|
|
:else
|
|
(st/emit!
|
|
(-> (dwta/toggle-token {:token token
|
|
:attrs kw-attrs
|
|
:shape-ids [id]
|
|
:expand-with-children false})
|
|
(se/add-event plugin-id))))))}
|
|
|
|
:isVariantHead
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)
|
|
component (u/locate-library-component file-id (:component-id shape))]
|
|
(and (ctk/instance-head? shape) (ctk/is-variant? component))))
|
|
|
|
:isVariantContainer
|
|
(fn []
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(ctk/is-variant-container? shape)))
|
|
|
|
:switchVariant
|
|
(fn [pos value]
|
|
(cond
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :switchVariant "Cannot modify a page that is not currently active")
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :switchVariant "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (nat-int? pos))
|
|
(u/not-valid plugin-id :pos pos)
|
|
|
|
(not (string? value))
|
|
(u/not-valid plugin-id :value value)
|
|
|
|
:else
|
|
(let [shape (u/locate-shape file-id page-id id)]
|
|
(if (clv/valid-switch? (dsh/lookup-libraries @st/state) shape pos value)
|
|
(st/emit! (-> (dwv/variants-switch {:shapes [shape] :pos pos :val value})
|
|
(se/add-event plugin-id)))
|
|
(u/not-valid plugin-id :switchVariant "Shape, property, or value is not valid")))))
|
|
|
|
:combineAsVariants
|
|
(fn [ids]
|
|
(cond
|
|
(or (not (seq ids)) (not (every? uuid/parse* ids)))
|
|
(u/not-valid plugin-id :ids ids)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :combineAsVariants "Plugin doesn't have 'content:write' permission")
|
|
|
|
:else
|
|
(let [;; Keep the input order (head shape first): it determines
|
|
;; the order of the resulting variant components (see
|
|
;; combine-as-variants)
|
|
ids
|
|
(into [id]
|
|
(comp (keep uuid/parse*)
|
|
(remove #{id})
|
|
(distinct))
|
|
ids)
|
|
|
|
valid? (dwv/valid-components-for-variants? @st/state page-id ids)]
|
|
|
|
(if valid?
|
|
(let [variant-id (uuid/next)]
|
|
(st/emit! (-> (dwv/combine-as-variants
|
|
ids
|
|
{:trigger "plugin:combine-as-variants" :variant-id variant-id})
|
|
(se/add-event plugin-id)))
|
|
(shape-proxy plugin-id file-id page-id variant-id))
|
|
|
|
(u/not-valid plugin-id :ids "One of the components is not on the same page or is already a variant"))))))
|
|
|
|
(cond-> (or (cfh/frame-shape? data) (cfh/group-shape? data) (cfh/svg-raw-shape? data) (cfh/bool-shape? data))
|
|
(crc/add-properties!
|
|
{:this true
|
|
:name "children"
|
|
:enumerable false
|
|
:get
|
|
(fn [^js self]
|
|
(.getChildren self))
|
|
|
|
:set
|
|
(fn [^js self children]
|
|
(let [valid-children? (every? shape-proxy? children)
|
|
shape (u/proxy->shape self)
|
|
file-id (obj/get self "$file")
|
|
page-id (obj/get self "$page")
|
|
reverse-fn (if (u/natural-child-ordering? plugin-id) reverse identity)
|
|
ids (when valid-children?
|
|
(->> children reverse-fn (map #(obj/get % "$id"))))]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :children "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :children "Cannot modify a page that is not currently active")
|
|
|
|
(not valid-children?)
|
|
(u/not-valid plugin-id :children "Every children needs to be shape proxies")
|
|
|
|
(u/component-copy-container? shape)
|
|
(u/not-valid plugin-id :children "Cannot change the structure of a component copy")
|
|
|
|
(not= (set ids) (set (:shapes shape)))
|
|
(u/not-valid plugin-id :children "Not all children are present in the input")
|
|
|
|
:else
|
|
(st/emit! (dw/reorder-children file-id page-id (:id shape) ids)))))}))
|
|
|
|
(cond-> (cfh/frame-shape? data)
|
|
(-> (crc/add-properties!
|
|
{:name "clipContent"
|
|
:get
|
|
(fn [self]
|
|
(-> self u/proxy->shape :show-content not))
|
|
|
|
:set
|
|
(fn [_ value]
|
|
(cond
|
|
(not (boolean? value))
|
|
(u/not-valid plugin-id :clipContent value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :clipContent "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :clipContent "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :show-content (not value))))))}
|
|
|
|
{:name "showInViewMode"
|
|
:get
|
|
(fn [self]
|
|
(-> self u/proxy->shape :hide-in-viewer not))
|
|
:set
|
|
(fn [_ value]
|
|
(cond
|
|
(not (boolean? value))
|
|
(u/not-valid plugin-id :showInViewMode value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :showInViewMode "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :showInViewMode "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :hide-in-viewer (not value))))))}
|
|
|
|
{:name "grid"
|
|
:get
|
|
(fn [self]
|
|
(let [layout (-> self u/proxy->shape :layout)
|
|
file-id (obj/get self "$file")
|
|
page-id (obj/get self "$page")
|
|
id (obj/get self "$id")]
|
|
(when (= :grid layout)
|
|
(grid/grid-layout-proxy plugin-id file-id page-id id))))}
|
|
|
|
{:name "flex"
|
|
:get
|
|
(fn [self]
|
|
(let [layout (-> self u/proxy->shape :layout)
|
|
file-id (obj/get self "$file")
|
|
page-id (obj/get self "$page")
|
|
id (obj/get self "$id")]
|
|
(when (= :flex layout)
|
|
(flex/flex-layout-proxy plugin-id file-id page-id id))))}
|
|
|
|
{:name "guides"
|
|
:get #(-> % u/proxy->shape :grids format/format-frame-guides)
|
|
:set (fn [self value]
|
|
(let [id (obj/get self "$id")
|
|
value (parser/parse-frame-guides value)]
|
|
(cond
|
|
(not (sm/validate [:vector ctg/schema:grid] value))
|
|
(u/not-valid plugin-id :guides value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :guides "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :guides "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsh/update-shapes [id] #(assoc % :grids value))))))}
|
|
|
|
{:name "rulerGuides"
|
|
:get
|
|
(fn [_]
|
|
(let [guides (-> (u/locate-page file-id page-id) :guides)]
|
|
(->> guides
|
|
(vals)
|
|
(filter #(= id (:frame-id %)))
|
|
(format/format-array #(rg/ruler-guide-proxy plugin-id file-id page-id (:id %))))))}
|
|
|
|
{:name "horizontalSizing"
|
|
:get #(-> % u/proxy->shape :layout-item-h-sizing (d/nilv :fix) d/name)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")
|
|
value (keyword value)]
|
|
(cond
|
|
(not (contains? #{:fix :auto} value))
|
|
(u/not-valid plugin-id :horizontalSizing value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :horizontalSizing "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :horizontalSizing "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))}
|
|
|
|
{:name "verticalSizing"
|
|
:get #(-> % u/proxy->shape :layout-item-v-sizing (d/nilv :fix) d/name)
|
|
:set
|
|
(fn [self value]
|
|
(let [id (obj/get self "$id")
|
|
value (keyword value)]
|
|
(cond
|
|
(not (contains? #{:fix :auto} value))
|
|
(u/not-valid plugin-id :verticalSizing value)
|
|
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :verticalSizing "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :verticalSizing "Cannot modify a page that is not currently active")
|
|
|
|
:else
|
|
(st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))}
|
|
|
|
{:name "variants"
|
|
:enumerable false
|
|
:get
|
|
(fn [self]
|
|
(let [shape (-> self u/proxy->shape)]
|
|
(when (ctk/is-variant-container? shape)
|
|
(variant-proxy plugin-id file-id (:id shape)))))})))
|
|
|
|
(cond-> (cfh/text-shape? data) (text/add-text-props plugin-id))
|
|
|
|
(cond-> (or (cfh/path-shape? data) (cfh/bool-shape? data))
|
|
(crc/add-properties!
|
|
{:name "commands"
|
|
:get #(-> % u/proxy->shape :content format/format-path-content)
|
|
:set
|
|
(fn [_ value]
|
|
(let [segments (parser/parse-commands value)]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :content "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :content "Cannot modify a page that is not currently active")
|
|
|
|
(not (sm/validate path/schema:segments segments))
|
|
(u/not-valid plugin-id :content segments)
|
|
|
|
:else
|
|
(let [selrect (path/calc-selrect segments)
|
|
content (path/from-plain segments)
|
|
points (grc/rect->points selrect)]
|
|
(st/emit! (dwsh/update-shapes
|
|
[id]
|
|
(fn [shape]
|
|
(-> shape
|
|
(assoc :content content)
|
|
(assoc :selrect selrect)
|
|
(assoc :points points)))))))))}
|
|
{:name "d"
|
|
:get #(-> % u/proxy->shape :content str)
|
|
:set
|
|
(fn [_ value]
|
|
(let [segments
|
|
(if (string? value)
|
|
(svg.path/parse value)
|
|
value)]
|
|
(cond
|
|
(not (r/check-permission plugin-id "content:write"))
|
|
(u/not-valid plugin-id :content "Plugin doesn't have 'content:write' permission")
|
|
|
|
(not (u/page-active? page-id))
|
|
(u/not-valid plugin-id :content "Cannot modify a page that is not currently active")
|
|
|
|
(not (cfh/path-shape? data))
|
|
(u/not-valid plugin-id :content-type type)
|
|
|
|
(not (sm/validate path/schema:segments segments))
|
|
(u/not-valid plugin-id :content segments)
|
|
|
|
:else
|
|
(let [selrect (path/calc-selrect segments)
|
|
content (path/from-plain segments)
|
|
points (grc/rect->points selrect)]
|
|
(st/emit! (dwsh/update-shapes [id]
|
|
(fn [shape]
|
|
(-> shape
|
|
(assoc :content content)
|
|
(assoc :selrect selrect)
|
|
(assoc :points points)))))))))}
|
|
{:name "content"
|
|
:get #(.-d %)
|
|
:set (fn [self value] (set! (.-d self) value))}))))))
|