mirror of
https://github.com/penpot/penpot.git
synced 2025-04-30 05:16:20 +02:00
Add Context Menu for tokens and simple placeholder functions
This commit is contained in:
parent
5efcb0f424
commit
24f1693684
6 changed files with 319 additions and 5 deletions
|
@ -196,6 +196,9 @@
|
||||||
(def context-menu
|
(def context-menu
|
||||||
(l/derived :context-menu workspace-local))
|
(l/derived :context-menu workspace-local))
|
||||||
|
|
||||||
|
(def token-context-menu
|
||||||
|
(l/derived :token-context-menu workspace-local))
|
||||||
|
|
||||||
;; page item that it is being edited
|
;; page item that it is being edited
|
||||||
(def editing-page-item
|
(def editing-page-item
|
||||||
(l/derived :page-item workspace-local))
|
(l/derived :page-item workspace-local))
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
|
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
|
||||||
[app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]]
|
[app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]]
|
||||||
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]
|
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]
|
||||||
|
[app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]]
|
||||||
[app.main.ui.workspace.tokens.modals]
|
[app.main.ui.workspace.tokens.modals]
|
||||||
[app.main.ui.workspace.viewport :refer [viewport]]
|
[app.main.ui.workspace.viewport :refer [viewport]]
|
||||||
[app.util.debug :as dbg]
|
[app.util.debug :as dbg]
|
||||||
|
@ -204,6 +205,7 @@
|
||||||
:style {:background-color background-color
|
:style {:background-color background-color
|
||||||
:touch-action "none"}}
|
:touch-action "none"}}
|
||||||
[:& context-menu]
|
[:& context-menu]
|
||||||
|
[:& token-context-menu]
|
||||||
|
|
||||||
(if ^boolean file-ready?
|
(if ^boolean file-ready?
|
||||||
[:& workspace-page {:page-id page-id
|
[:& workspace-page {:page-id page-id
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
(ns app.main.ui.workspace.tokens.common
|
(ns app.main.ui.workspace.tokens.common
|
||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
|
[potok.v2.core :as ptk]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
;; Helpers ---------------------------------------------------------------------
|
;; Helpers ---------------------------------------------------------------------
|
||||||
|
@ -39,3 +42,19 @@
|
||||||
:default-value default-value
|
:default-value default-value
|
||||||
:autoFocus auto-focus?
|
:autoFocus auto-focus?
|
||||||
:on-change on-change}]])
|
:on-change on-change}]])
|
||||||
|
|
||||||
|
;; Token Context Menu Functions -------------------------------------------------
|
||||||
|
|
||||||
|
(defn show-token-context-menu
|
||||||
|
[{:keys [position token-id] :as params}]
|
||||||
|
(dm/assert! (gpt/point? position))
|
||||||
|
(ptk/reify ::show-token-context-menu
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(assoc-in state [:workspace-local :token-context-menu] params))))
|
||||||
|
|
||||||
|
(def hide-token-context-menu
|
||||||
|
(ptk/reify ::hide-token-context-menu
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(assoc-in state [:workspace-local :token-context-menu] nil))))
|
153
frontend/src/app/main/ui/workspace/tokens/context_menu.cljs
Normal file
153
frontend/src/app/main/ui/workspace/tokens/context_menu.cljs
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
;; 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 INC
|
||||||
|
|
||||||
|
(ns app.main.ui.workspace.tokens.context-menu
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.main.data.events :as ev]
|
||||||
|
[app.main.data.shortcuts :as scd]
|
||||||
|
[app.main.data.workspace :as dw]
|
||||||
|
[app.main.refs :as refs]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||||
|
[app.main.ui.icons :as i]
|
||||||
|
[app.main.ui.workspace.tokens.common :refer [hide-token-context-menu]]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.i18n :refer [tr]]
|
||||||
|
[app.util.timers :as timers]
|
||||||
|
[okulary.core :as l]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(def tokens-menu-ref
|
||||||
|
(l/derived :token-context-menu refs/workspace-local))
|
||||||
|
|
||||||
|
(defn- prevent-default
|
||||||
|
[event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(dom/stop-propagation event))
|
||||||
|
|
||||||
|
(mf/defc token-menu-entry
|
||||||
|
{::mf/props :obj}
|
||||||
|
[{:keys [title shortcut on-click on-pointer-enter on-pointer-leave
|
||||||
|
on-unmount children selected? icon disabled value]}]
|
||||||
|
(let [submenu-ref (mf/use-ref nil)
|
||||||
|
hovering? (mf/use-ref false)
|
||||||
|
on-pointer-enter
|
||||||
|
(mf/use-callback
|
||||||
|
(fn []
|
||||||
|
(mf/set-ref-val! hovering? true)
|
||||||
|
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||||
|
(when (some? submenu-node)
|
||||||
|
(dom/set-css-property! submenu-node "display" "block")))
|
||||||
|
(when on-pointer-enter (on-pointer-enter))))
|
||||||
|
|
||||||
|
on-pointer-leave
|
||||||
|
(mf/use-callback
|
||||||
|
(fn []
|
||||||
|
(mf/set-ref-val! hovering? false)
|
||||||
|
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||||
|
(when (some? submenu-node)
|
||||||
|
(timers/schedule
|
||||||
|
200
|
||||||
|
#(when-not (mf/ref-val hovering?)
|
||||||
|
(dom/set-css-property! submenu-node "display" "none")))))
|
||||||
|
(when on-pointer-leave (on-pointer-leave))))
|
||||||
|
|
||||||
|
set-dom-node
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [dom]
|
||||||
|
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||||
|
(when (and (some? dom) (some? submenu-node))
|
||||||
|
(dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))]
|
||||||
|
|
||||||
|
(mf/use-effect
|
||||||
|
(mf/deps on-unmount)
|
||||||
|
(constantly on-unmount))
|
||||||
|
|
||||||
|
(if icon
|
||||||
|
[:li {:class (stl/css :icon-menu-item)
|
||||||
|
:disabled disabled
|
||||||
|
:data-value value
|
||||||
|
:ref set-dom-node
|
||||||
|
:on-click on-click
|
||||||
|
:on-pointer-enter on-pointer-enter
|
||||||
|
:on-pointer-leave on-pointer-leave}
|
||||||
|
[:span
|
||||||
|
{:class (stl/css :icon-wrapper)}
|
||||||
|
(if selected? [:span {:class (stl/css :selected-icon)}
|
||||||
|
i/tick]
|
||||||
|
[:span {:class (stl/css :selected-icon)}])
|
||||||
|
[:span {:class (stl/css :shape-icon)} icon]]
|
||||||
|
[:span {:class (stl/css :title)} title]]
|
||||||
|
[:li {:class (stl/css :context-menu-item)
|
||||||
|
:disabled disabled
|
||||||
|
:ref set-dom-node
|
||||||
|
:data-value value
|
||||||
|
:on-click on-click
|
||||||
|
:on-pointer-enter on-pointer-enter
|
||||||
|
:on-pointer-leave on-pointer-leave}
|
||||||
|
[:span {:class (stl/css :title)} title]
|
||||||
|
(when shortcut
|
||||||
|
[:span {:class (stl/css :shortcut)}
|
||||||
|
(for [[idx sc] (d/enumerate (scd/split-sc shortcut))]
|
||||||
|
[:span {:key (dm/str shortcut "-" idx)
|
||||||
|
:class (stl/css :shortcut-key)} sc])])
|
||||||
|
|
||||||
|
(when (> (count children) 1)
|
||||||
|
[:span {:class (stl/css :submenu-icon)} i/arrow])
|
||||||
|
|
||||||
|
(when (> (count children) 1)
|
||||||
|
[:ul {:class (stl/css :token-context-submenu)
|
||||||
|
:ref submenu-ref
|
||||||
|
:style {:display "none" :left 250}
|
||||||
|
:on-context-menu prevent-default}
|
||||||
|
children])])))
|
||||||
|
|
||||||
|
(mf/defc menu-separator
|
||||||
|
[]
|
||||||
|
[:li {:class (stl/css :separator)}])
|
||||||
|
|
||||||
|
(mf/defc token-pill-context-menu
|
||||||
|
[{:keys [token-id]}]
|
||||||
|
(let [do-delete #(js/console.log "Deleting")
|
||||||
|
do-duplicate #(js/console.log "Duplicating")
|
||||||
|
do-edit #(js/console.log "Editing")]
|
||||||
|
[:ul.context-list
|
||||||
|
[:> token-menu-entry {:title (tr "Delete Token") :on-click do-delete}]
|
||||||
|
[:> token-menu-entry {:title (tr "Duplicate Token") :on-click do-duplicate}]
|
||||||
|
[:> token-menu-entry {:title (tr "Edit Token") :on-click do-edit}]]))
|
||||||
|
|
||||||
|
(mf/defc token-context-menu
|
||||||
|
[]
|
||||||
|
(let [mdata (mf/deref tokens-menu-ref)
|
||||||
|
top (- (get-in mdata [:position :y]) 20)
|
||||||
|
left (get-in mdata [:position :x])
|
||||||
|
dropdown-ref (mf/use-ref)]
|
||||||
|
|
||||||
|
(mf/use-effect
|
||||||
|
(mf/deps mdata)
|
||||||
|
#(let [dropdown (mf/ref-val dropdown-ref)]
|
||||||
|
(when dropdown
|
||||||
|
(let [bounding-rect (dom/get-bounding-rect dropdown)
|
||||||
|
window-size (dom/get-window-size)
|
||||||
|
delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0)
|
||||||
|
delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0)
|
||||||
|
new-style (str "top: " (- top delta-y) "px; "
|
||||||
|
"left: " (- left delta-x) "px;")]
|
||||||
|
(when (or (> delta-x 0) (> delta-y 0))
|
||||||
|
(.setAttribute ^js dropdown "style" new-style))))))
|
||||||
|
|
||||||
|
[:& dropdown {:show (boolean mdata)
|
||||||
|
:on-close #(st/emit! hide-token-context-menu)}
|
||||||
|
[:div {:class (stl/css :token-context-menu)
|
||||||
|
:ref dropdown-ref
|
||||||
|
:style {:top top :left left}
|
||||||
|
:on-context-menu prevent-default}
|
||||||
|
(when (= :token (:type mdata))
|
||||||
|
[:ul {:class (stl/css :context-list)}
|
||||||
|
[:& token-pill-context-menu {:token-id (:id mdata)}]])]]))
|
126
frontend/src/app/main/ui/workspace/tokens/context_menu.scss
Normal file
126
frontend/src/app/main/ui/workspace/tokens/context_menu.scss
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// 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 INC
|
||||||
|
|
||||||
|
@import "refactor/common-refactor.scss";
|
||||||
|
|
||||||
|
.token-context-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: $s-40;
|
||||||
|
left: $s-736;
|
||||||
|
z-index: $z-index-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-list,
|
||||||
|
.token-context-submenu {
|
||||||
|
@include menuShadow;
|
||||||
|
display: grid;
|
||||||
|
width: $s-240;
|
||||||
|
padding: $s-4;
|
||||||
|
border-radius: $br-8;
|
||||||
|
border: $s-2 solid var(--panel-border-color);
|
||||||
|
background-color: var(--menu-background-color);
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-context-submenu {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
height: $s-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: $s-28;
|
||||||
|
width: 100%;
|
||||||
|
padding: $s-6;
|
||||||
|
border-radius: $br-8;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@include bodySmallTypography;
|
||||||
|
color: var(--menu-foreground-color);
|
||||||
|
}
|
||||||
|
.shortcut {
|
||||||
|
@include flexCenter;
|
||||||
|
gap: $s-2;
|
||||||
|
color: var(--menu-shortcut-foreground-color);
|
||||||
|
.shortcut-key {
|
||||||
|
@include bodySmallTypography;
|
||||||
|
@include flexCenter;
|
||||||
|
height: $s-20;
|
||||||
|
padding: $s-2 $s-6;
|
||||||
|
border-radius: $br-6;
|
||||||
|
background-color: var(--menu-shortcut-background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-icon svg {
|
||||||
|
@extend .button-icon-small;
|
||||||
|
stroke: var(--menu-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--menu-background-color-hover);
|
||||||
|
.title {
|
||||||
|
color: var(--menu-foreground-color-hover);
|
||||||
|
}
|
||||||
|
.shortcut {
|
||||||
|
color: var(--menu-shortcut-foreground-color-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid var(--menu-border-color-focus);
|
||||||
|
background-color: var(--menu-background-color-focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-menu-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
height: $s-28;
|
||||||
|
padding: $s-6;
|
||||||
|
border-radius: $br-8;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--menu-background-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.title {
|
||||||
|
margin-left: $s-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-icon {
|
||||||
|
svg {
|
||||||
|
@extend .button-icon-small;
|
||||||
|
stroke: var(--menu-foreground-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-icon {
|
||||||
|
margin-left: $s-2;
|
||||||
|
svg {
|
||||||
|
@extend .button-icon-small;
|
||||||
|
stroke: var(--menu-foreground-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-menu-item[disabled],
|
||||||
|
.context-menu-item[disabled] {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
[app.main.ui.components.search-bar :refer [search-bar]]
|
[app.main.ui.components.search-bar :refer [search-bar]]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.main.ui.workspace.sidebar.assets.common :as cmm]
|
[app.main.ui.workspace.sidebar.assets.common :as cmm]
|
||||||
[app.main.ui.workspace.tokens.common :refer [workspace-shapes]]
|
[app.main.ui.workspace.tokens.common :as tcm]
|
||||||
[app.main.ui.workspace.tokens.core :refer [tokens-applied?] :as wtc]
|
[app.main.ui.workspace.tokens.core :refer [tokens-applied?] :as wtc]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
@ -36,18 +36,28 @@
|
||||||
|
|
||||||
(mf/defc token-pill
|
(mf/defc token-pill
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[{:keys [on-click token highlighted?]}]
|
[{:keys [on-click token highlighted? on-context-menu]}]
|
||||||
(let [{:keys [name value]} token]
|
(let [{:keys [name value]} token]
|
||||||
[:div {:class (stl/css-case :token-pill true
|
[:div {:class (stl/css-case :token-pill true
|
||||||
:token-pill-highlighted highlighted?)
|
:token-pill-highlighted highlighted?)
|
||||||
:title (str "Token value: " value)
|
:title (str "Token value: " value)
|
||||||
:on-click on-click}
|
:on-click on-click
|
||||||
|
:on-context-menu on-context-menu}
|
||||||
name]))
|
name]))
|
||||||
|
|
||||||
(mf/defc token-component
|
(mf/defc token-component
|
||||||
[{:keys [type file tokens selected-shapes token-type-props]}]
|
[{:keys [type file tokens selected-shapes token-type-props]}]
|
||||||
(let [open? (mf/use-state false)
|
(let [open? (mf/use-state false)
|
||||||
{:keys [modal attributes title]} token-type-props
|
{:keys [modal attributes title]} token-type-props
|
||||||
|
|
||||||
|
on-context-menu (mf/use-fn
|
||||||
|
(fn [event token]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(st/emit! (tcm/show-token-context-menu {:type :token
|
||||||
|
:position (dom/get-client-position event)
|
||||||
|
:token-id (:id token)}))))
|
||||||
|
|
||||||
on-toggle-open-click (mf/use-fn
|
on-toggle-open-click (mf/use-fn
|
||||||
(mf/deps open? tokens)
|
(mf/deps open? tokens)
|
||||||
#(when (seq tokens)
|
#(when (seq tokens)
|
||||||
|
@ -87,7 +97,8 @@
|
||||||
{:key (:id token)
|
{:key (:id token)
|
||||||
:token token
|
:token token
|
||||||
:highlighted? (tokens-applied? token selected-shapes attributes)
|
:highlighted? (tokens-applied? token selected-shapes attributes)
|
||||||
:on-click #(on-token-pill-click % token)}])]])]]))
|
:on-click #(on-token-pill-click % token)
|
||||||
|
:on-context-menu #(on-context-menu % token)}])]])]]))
|
||||||
|
|
||||||
(defn sorted-token-groups
|
(defn sorted-token-groups
|
||||||
"Separate token-types into groups of `:empty` or `:filled` depending if tokens exist for that type.
|
"Separate token-types into groups of `:empty` or `:filled` depending if tokens exist for that type.
|
||||||
|
@ -114,7 +125,7 @@
|
||||||
token-groups (mf/with-memo [tokens]
|
token-groups (mf/with-memo [tokens]
|
||||||
(sorted-token-groups tokens))
|
(sorted-token-groups tokens))
|
||||||
selected-shape-ids (mf/deref refs/selected-shapes)
|
selected-shape-ids (mf/deref refs/selected-shapes)
|
||||||
selected-shapes (workspace-shapes workspace-data current-page-id selected-shape-ids)]
|
selected-shapes (tcm/workspace-shapes workspace-data current-page-id selected-shape-ids)]
|
||||||
[:article
|
[:article
|
||||||
[:div.assets-bar
|
[:div.assets-bar
|
||||||
(for [{:keys [token-key token-type-props tokens]} (concat (:filled token-groups)
|
(for [{:keys [token-key token-type-props tokens]} (concat (:filled token-groups)
|
||||||
|
|
Loading…
Add table
Reference in a new issue