mirror of
https://github.com/penpot/penpot.git
synced 2025-05-23 21:26:13 +02:00
✨ Add new accessibility functionalities to dashboard
This commit is contained in:
parent
9e190d9810
commit
fcb8b15ef2
15 changed files with 625 additions and 145 deletions
|
@ -64,6 +64,10 @@ a {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: "worksans", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: $fs12;
|
font-size: $fs12;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
|
@ -73,6 +73,40 @@
|
||||||
|
|
||||||
form {
|
form {
|
||||||
margin: 2rem 0 0.5rem 0;
|
margin: 2rem 0 0.5rem 0;
|
||||||
|
.accept-terms-and-privacy-wrapper {
|
||||||
|
position: relative;
|
||||||
|
.input-checkbox {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.input-checkbox input[type="checkbox"] {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
opacity: 0;
|
||||||
|
top: 22px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
label:before {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: -36px;
|
||||||
|
}
|
||||||
|
label:after {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: -33px;
|
||||||
|
}
|
||||||
|
.input-checkbox input[type="checkbox"]:focus {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
.auth-links {
|
||||||
|
margin-left: 40px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,15 @@
|
||||||
margin: $size-3 $size-4 $size-4 $size-2;
|
margin: $size-3 $size-4 $size-4 $size-2;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
a {
|
a,
|
||||||
|
button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
@media #{$bp-max-1366} {
|
@media #{$bp-max-1366} {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
flex: 1 0 230px;
|
flex: 1 0 230px;
|
||||||
|
|
|
@ -183,6 +183,7 @@
|
||||||
|
|
||||||
.dashboard-project-row {
|
.dashboard-project-row {
|
||||||
margin-bottom: $size-5;
|
margin-bottom: $size-5;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.project {
|
.project {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -211,6 +212,8 @@
|
||||||
font-size: $fs14;
|
font-size: $fs14;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
.placeholder-icon {
|
.placeholder-icon {
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
@ -297,6 +300,35 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.show-more {
|
||||||
|
align-items: center;
|
||||||
|
color: $color-gray-30;
|
||||||
|
display: flex;
|
||||||
|
font-size: $fs14;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 9px;
|
||||||
|
right: 53px;
|
||||||
|
.placeholder-icon {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
margin-left: 10px;
|
||||||
|
svg {
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
fill: $color-gray-30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: $color-primary-dark;
|
||||||
|
svg {
|
||||||
|
fill: $color-primary-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-files-row-title-info {
|
.recent-files-row-title-info {
|
||||||
|
|
|
@ -245,16 +245,16 @@
|
||||||
:type "text"}]]
|
:type "text"}]]
|
||||||
|
|
||||||
(when (contains? @cf/flags :terms-and-privacy-checkbox)
|
(when (contains? @cf/flags :terms-and-privacy-checkbox)
|
||||||
[:div.fields-row
|
[:div.fields-row.input-visible.accept-terms-and-privacy-wrapper
|
||||||
[:& fm/input {:name :accept-terms-and-privacy
|
[:& fm/input {:name :accept-terms-and-privacy
|
||||||
:class "check-primary"
|
:class "check-primary"
|
||||||
:type "checkbox"}
|
:type "checkbox"}
|
||||||
[:span
|
[:span
|
||||||
(tr "auth.terms-privacy-agreement")
|
(tr "auth.terms-privacy-agreement")]]
|
||||||
[:div
|
[:div.auth-links
|
||||||
[:a {:href "https://penpot.app/terms" :target "_blank"} (tr "auth.terms-of-service")]
|
[:a {:href "https://penpot.app/terms" :target "_blank"} (tr "auth.terms-of-service")]
|
||||||
[:span ",\u00A0"]
|
[:span ",\u00A0"]
|
||||||
[:a {:href "https://penpot.app/privacy" :target "_blank"} (tr "auth.privacy-policy")]]]]])
|
[:a {:href "https://penpot.app/privacy" :target "_blank"} (tr "auth.privacy-policy")]]])
|
||||||
|
|
||||||
[:& fm/submit-button
|
[:& fm/submit-button
|
||||||
{:label (tr "auth.register-submit")
|
{:label (tr "auth.register-submit")
|
||||||
|
|
260
frontend/src/app/main/ui/components/context_menu_a11y.cljs
Normal file
260
frontend/src/app/main/ui/components/context_menu_a11y.cljs
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
;; 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.components.context-menu-a11y
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.main.refs :as refs]
|
||||||
|
[app.main.ui.components.dropdown :refer [dropdown']]
|
||||||
|
[app.main.ui.icons :as i]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
[app.util.keyboard :as kbd]
|
||||||
|
[app.util.object :as obj]
|
||||||
|
[app.util.timers :as tm]
|
||||||
|
[goog.object :as gobj]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(defn generate-ids-group
|
||||||
|
[options parent-name]
|
||||||
|
(let [ids (->> options
|
||||||
|
(map :id)
|
||||||
|
(filter some?))]
|
||||||
|
(if parent-name
|
||||||
|
(cons "go-back-sub-option" ids)
|
||||||
|
ids)))
|
||||||
|
|
||||||
|
(mf/defc context-menu-a11y-item
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
|
||||||
|
(let [children (gobj/get props "children")
|
||||||
|
on-click (gobj/get props "on-click")
|
||||||
|
on-key-down (gobj/get props "on-key-down")
|
||||||
|
id (gobj/get props "id")
|
||||||
|
klass (gobj/get props "klass")
|
||||||
|
key (gobj/get props "key")
|
||||||
|
data-test (gobj/get props "data-test")]
|
||||||
|
[:li {:id id
|
||||||
|
:class klass
|
||||||
|
:tab-index "0"
|
||||||
|
:on-key-down on-key-down
|
||||||
|
:on-click on-click
|
||||||
|
:key key
|
||||||
|
:role "menuitem"
|
||||||
|
:data-test data-test}
|
||||||
|
children]))
|
||||||
|
|
||||||
|
(mf/defc context-menu-a11y'
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(assert (fn? (gobj/get props "on-close")) "missing `on-close` prop")
|
||||||
|
(assert (boolean? (gobj/get props "show")) "missing `show` prop")
|
||||||
|
(assert (vector? (gobj/get props "options")) "missing `options` prop")
|
||||||
|
(let [open? (gobj/get props "show")
|
||||||
|
on-close (gobj/get props "on-close")
|
||||||
|
options (gobj/get props "options")
|
||||||
|
is-selectable (gobj/get props "selectable")
|
||||||
|
selected (gobj/get props "selected")
|
||||||
|
top (gobj/get props "top" 0)
|
||||||
|
left (gobj/get props "left" 0)
|
||||||
|
fixed? (gobj/get props "fixed?" false)
|
||||||
|
min-width? (gobj/get props "min-width?" false)
|
||||||
|
origin (gobj/get props "origin")
|
||||||
|
route (mf/deref refs/route)
|
||||||
|
in-dashboard? (= :dashboard-projects (:name (:data route)))
|
||||||
|
local (mf/use-state {:offset-y 0
|
||||||
|
:offset-x 0
|
||||||
|
:levels nil})
|
||||||
|
|
||||||
|
on-local-close
|
||||||
|
(mf/use-callback
|
||||||
|
(fn []
|
||||||
|
(swap! local assoc :levels [{:parent-option nil
|
||||||
|
:options options}])
|
||||||
|
(on-close)))
|
||||||
|
|
||||||
|
props (obj/merge props #js {:on-close on-local-close})
|
||||||
|
|
||||||
|
ids (generate-ids-group (:options (last (:levels @local))) (:parent-option (last (:levels @local))))
|
||||||
|
check-menu-offscreen
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps top (:offset-y @local) left (:offset-x @local))
|
||||||
|
(fn [node]
|
||||||
|
(when (some? node)
|
||||||
|
(let [bounding_rect (dom/get-bounding-rect node)
|
||||||
|
window_size (dom/get-window-size)
|
||||||
|
{node-height :height node-width :width} bounding_rect
|
||||||
|
{window-height :height window-width :width} window_size
|
||||||
|
target-offset-y (if (> (+ top node-height) window-height)
|
||||||
|
(- node-height)
|
||||||
|
0)
|
||||||
|
target-offset-x (if (> (+ left node-width) window-width)
|
||||||
|
(- node-width)
|
||||||
|
0)]
|
||||||
|
|
||||||
|
(when (or (not= target-offset-y (:offset-y @local)) (not= target-offset-x (:offset-x @local)))
|
||||||
|
(swap! local assoc :offset-y target-offset-y :offset-x target-offset-x))))))
|
||||||
|
|
||||||
|
enter-submenu
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps options)
|
||||||
|
(fn [option-name sub-options]
|
||||||
|
(fn [event]
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(swap! local update :levels
|
||||||
|
conj {:parent-option option-name
|
||||||
|
:options sub-options}))))
|
||||||
|
|
||||||
|
exit-submenu
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [event]
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(swap! local update :levels pop)))
|
||||||
|
|
||||||
|
on-key-down
|
||||||
|
(fn [options-original parent-original]
|
||||||
|
(fn [event]
|
||||||
|
(let [ids (generate-ids-group options-original parent-original)
|
||||||
|
first-id (dom/get-element (first ids))
|
||||||
|
first-element (dom/get-element first-id)
|
||||||
|
len (count ids)
|
||||||
|
parent (dom/get-target event)
|
||||||
|
parent-id (dom/get-attribute parent "id")
|
||||||
|
option (first (filter #(= parent-id (:id %)) options-original))
|
||||||
|
sub-options (:sub-options option)
|
||||||
|
has-suboptions? (some? (:sub-options option))
|
||||||
|
option-handler (:option-handler option)
|
||||||
|
is-back-option (= "go-back-sub-option" parent-id)]
|
||||||
|
(when (kbd/home? event)
|
||||||
|
(when first-element
|
||||||
|
(dom/focus! first-element)))
|
||||||
|
|
||||||
|
(when (kbd/enter? event)
|
||||||
|
(if is-back-option
|
||||||
|
(exit-submenu event)
|
||||||
|
|
||||||
|
(if has-suboptions?
|
||||||
|
(do
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(swap! local update :levels
|
||||||
|
conj {:parent-option (:option-name option)
|
||||||
|
:options sub-options}))
|
||||||
|
|
||||||
|
(do
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(option-handler event)))))
|
||||||
|
|
||||||
|
(when (and is-back-option
|
||||||
|
(kbd/left-arrow? event))
|
||||||
|
(exit-submenu event))
|
||||||
|
|
||||||
|
(when (and has-suboptions? (kbd/right-arrow? event))
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(swap! local update :levels
|
||||||
|
conj {:parent-option (:option-name option)
|
||||||
|
:options sub-options}))
|
||||||
|
(when (kbd/up-arrow? event)
|
||||||
|
(let [actual-selected (dom/get-active)
|
||||||
|
actual-id (dom/get-attribute actual-selected "id")
|
||||||
|
actual-index (d/index-of ids actual-id)
|
||||||
|
previous-id (if (= 0 actual-index)
|
||||||
|
(last ids)
|
||||||
|
(nth ids (- actual-index 1)))]
|
||||||
|
(dom/focus! (dom/get-element previous-id))))
|
||||||
|
|
||||||
|
(when (kbd/down-arrow? event)
|
||||||
|
(let [actual-selected (dom/get-active)
|
||||||
|
actual-id (dom/get-attribute actual-selected "id")
|
||||||
|
actual-index (d/index-of ids actual-id)
|
||||||
|
next-id (if (= (- len 1) actual-index)
|
||||||
|
(first ids)
|
||||||
|
(nth ids (+ 1 actual-index)))]
|
||||||
|
(dom/focus! (dom/get-element next-id))))
|
||||||
|
|
||||||
|
(when (or (kbd/esc? event) (kbd/tab? event))
|
||||||
|
(on-close)
|
||||||
|
(dom/focus! (dom/get-element origin))))))]
|
||||||
|
|
||||||
|
(mf/with-effect [options]
|
||||||
|
(swap! local assoc :levels [{:parent-option nil
|
||||||
|
:options options}]))
|
||||||
|
|
||||||
|
(mf/with-effect [ids]
|
||||||
|
(tm/schedule-on-idle
|
||||||
|
(dom/focus! (dom/get-element (first ids)))))
|
||||||
|
|
||||||
|
(when (and open? (some? (:levels @local)))
|
||||||
|
[:> dropdown' props
|
||||||
|
|
||||||
|
(let [level (-> @local :levels peek)
|
||||||
|
original-options (:options level)
|
||||||
|
parent-original (:parent-option level)]
|
||||||
|
[:div.context-menu {:class (dom/classnames :is-open open?
|
||||||
|
:fixed fixed?
|
||||||
|
:is-selectable is-selectable)
|
||||||
|
:style {:top (+ top (:offset-y @local))
|
||||||
|
:left (+ left (:offset-x @local))}
|
||||||
|
:on-key-down (on-key-down original-options parent-original)}
|
||||||
|
(let [level (-> @local :levels peek)]
|
||||||
|
[:ul.context-menu-items {:class (dom/classnames :min-width min-width?)
|
||||||
|
:role "menu"
|
||||||
|
:ref check-menu-offscreen}
|
||||||
|
(when-let [parent-option (:parent-option level)]
|
||||||
|
[:*
|
||||||
|
[:& context-menu-a11y-item
|
||||||
|
{:id "go-back-sub-option"
|
||||||
|
:tab-index "0"
|
||||||
|
:on-key-down (fn [event]
|
||||||
|
(dom/prevent-default event))}
|
||||||
|
[:div.context-menu-action.submenu-back
|
||||||
|
{:data-no-close true
|
||||||
|
:on-click exit-submenu}
|
||||||
|
[:span i/arrow-slide]
|
||||||
|
parent-option]]
|
||||||
|
[:li.separator]])
|
||||||
|
(for [[index option] (d/enumerate (:options level))]
|
||||||
|
|
||||||
|
(let [option-name (:option-name option)
|
||||||
|
id (:id option)
|
||||||
|
sub-options (:sub-options option)
|
||||||
|
option-handler (:option-handler option)
|
||||||
|
data-test (:data-test option)]
|
||||||
|
(when option-name
|
||||||
|
(if (= option-name :separator)
|
||||||
|
[:li.separator {:key (dm/str "context-item-" index)}]
|
||||||
|
[:& context-menu-a11y-item
|
||||||
|
{:id id
|
||||||
|
:class (dom/classnames :is-selected (and selected (= option-name selected)))
|
||||||
|
:key (dm/str "context-item-" index)
|
||||||
|
:tab-index "0"
|
||||||
|
:on-key-down (fn [event]
|
||||||
|
(dom/prevent-default event))}
|
||||||
|
(if-not sub-options
|
||||||
|
[:a.context-menu-action {:on-click #(do (dom/stop-propagation %)
|
||||||
|
(on-close)
|
||||||
|
(option-handler %))
|
||||||
|
:data-test data-test}
|
||||||
|
(if (and in-dashboard? (= option-name "Default"))
|
||||||
|
(tr "dashboard.default-team-name")
|
||||||
|
option-name)]
|
||||||
|
[:a.context-menu-action.submenu
|
||||||
|
{:data-no-close true
|
||||||
|
:on-click (enter-submenu option-name sub-options)
|
||||||
|
:data-test data-test}
|
||||||
|
option-name
|
||||||
|
[:span i/arrow-slide]])]))))])])])))
|
||||||
|
|
||||||
|
(mf/defc context-menu-a11y
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(assert (fn? (gobj/get props "on-close")) "missing `on-close` prop")
|
||||||
|
(assert (boolean? (gobj/get props "show")) "missing `show` prop")
|
||||||
|
(assert (vector? (gobj/get props "options")) "missing `options` prop")
|
||||||
|
|
||||||
|
(when (gobj/get props "show")
|
||||||
|
(mf/element context-menu-a11y' props)))
|
|
@ -25,7 +25,7 @@
|
||||||
on-key-down (gobj/get props "on-key-down")
|
on-key-down (gobj/get props "on-key-down")
|
||||||
id (gobj/get props "id")
|
id (gobj/get props "id")
|
||||||
klass (gobj/get props "klass")
|
klass (gobj/get props "klass")
|
||||||
key (gobj/get props "klass")
|
key (gobj/get props "key")
|
||||||
data-test (gobj/get props "data-test")]
|
data-test (gobj/get props "data-test")]
|
||||||
[:li {:id id
|
[:li {:id id
|
||||||
:class klass
|
:class klass
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
ref (gobj/get props "container")
|
ref (gobj/get props "container")
|
||||||
ids (gobj/get props "ids")
|
ids (gobj/get props "ids")
|
||||||
list-class (gobj/get props "list-class")
|
list-class (gobj/get props "list-class")
|
||||||
|
ids (filter some? ids)
|
||||||
on-click
|
on-click
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [target (dom/get-target event)
|
(let [target (dom/get-target event)
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.colors :as clr]
|
[app.common.colors :as clr]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.math :as mth]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.main.data.dashboard :as dd]
|
[app.main.data.dashboard :as dd]
|
||||||
[app.main.data.dashboard.shortcuts :as sc]
|
[app.main.data.dashboard.shortcuts :as sc]
|
||||||
|
@ -83,6 +84,10 @@
|
||||||
container-size (* (+ 2 num-cards) card-width)
|
container-size (* (+ 2 num-cards) card-width)
|
||||||
;; We need space for num-cards plus the libraries&templates link
|
;; We need space for num-cards plus the libraries&templates link
|
||||||
more-cards (> (+ @card-offset (* (+ 1 num-cards) card-width)) content-width)
|
more-cards (> (+ @card-offset (* (+ 1 num-cards) card-width)) content-width)
|
||||||
|
visible-card-count (mth/floor (/ content-width 275))
|
||||||
|
left-moves (/ @card-offset -275)
|
||||||
|
first-visible-card left-moves
|
||||||
|
last-visible-card (+ (- visible-card-count 1) left-moves)
|
||||||
content-ref (mf/use-ref)
|
content-ref (mf/use-ref)
|
||||||
|
|
||||||
toggle-collapse
|
toggle-collapse
|
||||||
|
@ -146,38 +151,78 @@
|
||||||
[:div.dashboard-templates-section {:class (when collapsed "collapsed")}
|
[:div.dashboard-templates-section {:class (when collapsed "collapsed")}
|
||||||
[:div.title
|
[:div.title
|
||||||
[:button {:tab-index "0"
|
[:button {:tab-index "0"
|
||||||
:on-click toggle-collapse}
|
:on-click toggle-collapse
|
||||||
|
:on-key-down (fn [event]
|
||||||
|
(when (kbd/enter? event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(toggle-collapse)))}
|
||||||
[:span (tr "dashboard.libraries-and-templates")]
|
[:span (tr "dashboard.libraries-and-templates")]
|
||||||
[:span.icon (if collapsed i/arrow-up i/arrow-down)]]]
|
[:span.icon (if collapsed i/arrow-up i/arrow-down)]]]
|
||||||
[:div.content {:ref content-ref
|
[:div.content {:ref content-ref
|
||||||
:style {:left @card-offset :width (str container-size "px")}}
|
:style {:left @card-offset :width (str container-size "px")}}
|
||||||
|
|
||||||
(for [num-item (range (count templates)) :let [item (nth templates num-item)]]
|
(for [num-item (range (count templates)) :let [item (nth templates num-item)]]
|
||||||
[:a.card-container {:tab-index "0"
|
(let [is-visible? (and (>= num-item first-visible-card) (<= num-item last-visible-card))]
|
||||||
|
[:a.card-container {:tab-index (if (or (not is-visible?) collapsed)
|
||||||
|
"-1"
|
||||||
|
"0")
|
||||||
:id (str/concat "card-container-" num-item)
|
:id (str/concat "card-container-" num-item)
|
||||||
:key (:id item)
|
:key (:id item)
|
||||||
:on-click #(import-template item)
|
:on-click #(import-template item)
|
||||||
:on-key-down (fn [event]
|
:on-key-down (fn [event]
|
||||||
(when (kbd/enter? event)
|
(when (kbd/enter? event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
(import-template item)))}
|
(import-template item)))}
|
||||||
[:div.template-card
|
[:div.template-card
|
||||||
[:div.img-container
|
[:div.img-container
|
||||||
[:img {:src (:thumbnail-uri item)
|
[:img {:src (:thumbnail-uri item)
|
||||||
:alt (:name item)}]]
|
:alt (:name item)}]]
|
||||||
[:div.card-name [:span (:name item)] [:span.icon i/download]]]])
|
[:div.card-name [:span (:name item)] [:span.icon i/download]]]]))
|
||||||
|
|
||||||
|
(let [is-visible? (and (>= num-cards first-visible-card) (<= num-cards last-visible-card))]
|
||||||
[:div.card-container
|
[:div.card-container
|
||||||
[:div.template-card
|
[:div.template-card
|
||||||
[:div.img-container
|
[:div.img-container
|
||||||
[:a {:tab-index "0"
|
[:a {:id (str/concat "card-container-" num-cards)
|
||||||
:href "https://penpot.app/libraries-templates" :target "_blank" :on-click handle-template-link}
|
:tab-index (if (or (not is-visible?) collapsed)
|
||||||
|
"-1"
|
||||||
|
"0")
|
||||||
|
:href "https://penpot.app/libraries-templates.html"
|
||||||
|
:target "_blank"
|
||||||
|
:on-click handle-template-link
|
||||||
|
:on-key-down (fn [event]
|
||||||
|
(when (kbd/enter? event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(handle-template-link)))}
|
||||||
[:div.template-link
|
[:div.template-link
|
||||||
[:div.template-link-title (tr "dashboard.libraries-and-templates")]
|
[:div.template-link-title (tr "dashboard.libraries-and-templates")]
|
||||||
[:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]]]
|
[:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]])]
|
||||||
(when (< @card-offset 0)
|
(when (< @card-offset 0)
|
||||||
[:button.button.left {:on-click move-left} i/go-prev])
|
[:button.button.left {:tab-index (if collapsed
|
||||||
|
"-1"
|
||||||
|
"0")
|
||||||
|
:on-click move-left
|
||||||
|
:on-key-down (fn [event]
|
||||||
|
(when (kbd/enter? event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(move-left)
|
||||||
|
(let [first-element (dom/get-element (str/concat "card-container-" first-visible-card))]
|
||||||
|
(when first-element
|
||||||
|
(dom/focus! first-element)))))} i/go-prev])
|
||||||
(when more-cards
|
(when more-cards
|
||||||
[:button.button.right {:on-click move-right
|
[:button.button.right {:tab-index (if collapsed
|
||||||
:aria-label (tr "labels.next")} i/go-next])]))
|
"-1"
|
||||||
|
"0")
|
||||||
|
:on-click move-right
|
||||||
|
:aria-label (tr "labels.next")
|
||||||
|
:on-key-down (fn [event]
|
||||||
|
(when (kbd/enter? event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(move-right)
|
||||||
|
(let [last-element (dom/get-element (str/concat "card-container-" last-visible-card))]
|
||||||
|
(when last-element
|
||||||
|
(dom/focus! last-element)))))} i/go-next])]))
|
||||||
|
|
||||||
(mf/defc dashboard-content
|
(mf/defc dashboard-content
|
||||||
[{:keys [team projects project section search-term profile] :as props}]
|
[{:keys [team projects project section search-term profile] :as props}]
|
||||||
|
@ -277,6 +322,7 @@
|
||||||
(let [events [(events/listen goog/global EventType.KEYDOWN
|
(let [events [(events/listen goog/global EventType.KEYDOWN
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(when (kbd/enter? event)
|
(when (kbd/enter? event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
(st/emit! (dd/open-selected-file)))))]]
|
(st/emit! (dd/open-selected-file)))))]]
|
||||||
(fn []
|
(fn []
|
||||||
(doseq [key events]
|
(doseq [key events]
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.context-menu :refer [context-menu]]
|
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]]
|
||||||
[app.main.ui.context :as ctx]
|
[app.main.ui.context :as ctx]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
@ -27,6 +27,10 @@
|
||||||
(tr "labels.drafts")
|
(tr "labels.drafts")
|
||||||
(:name project)))
|
(:name project)))
|
||||||
|
|
||||||
|
(defn get-project-id
|
||||||
|
[project]
|
||||||
|
(str (:id project)))
|
||||||
|
|
||||||
(defn get-team-name
|
(defn get-team-name
|
||||||
[team]
|
[team]
|
||||||
(if (:is-default team)
|
(if (:is-default team)
|
||||||
|
@ -49,7 +53,7 @@
|
||||||
projects))
|
projects))
|
||||||
|
|
||||||
(mf/defc file-menu
|
(mf/defc file-menu
|
||||||
[{:keys [files show? on-edit on-menu-close top left navigate? origin] :as props}]
|
[{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id] :as props}]
|
||||||
(assert (seq files) "missing `files` prop")
|
(assert (seq files) "missing `files` prop")
|
||||||
(assert (boolean? show?) "missing `show?` prop")
|
(assert (boolean? show?) "missing `show?` prop")
|
||||||
(assert (fn? on-edit) "missing `on-edit` prop")
|
(assert (fn? on-edit) "missing `on-edit` prop")
|
||||||
|
@ -65,13 +69,10 @@
|
||||||
|
|
||||||
current-team-id (mf/use-ctx ctx/current-team-id)
|
current-team-id (mf/use-ctx ctx/current-team-id)
|
||||||
teams (mf/use-state nil)
|
teams (mf/use-state nil)
|
||||||
|
|
||||||
current-team (get @teams current-team-id)
|
current-team (get @teams current-team-id)
|
||||||
other-teams (remove #(= (:id %) current-team-id) (vals @teams))
|
other-teams (remove #(= (:id %) current-team-id) (vals @teams))
|
||||||
|
|
||||||
current-projects (remove #(= (:id %) (:project-id file))
|
current-projects (remove #(= (:id %) (:project-id file))
|
||||||
(:projects current-team))
|
(:projects current-team))
|
||||||
|
|
||||||
on-new-tab
|
on-new-tab
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(let [path-params {:project-id (:project-id file)
|
(let [path-params {:project-id (:project-id file)
|
||||||
|
@ -213,51 +214,99 @@
|
||||||
(reset! teams %)))))))
|
(reset! teams %)))))))
|
||||||
|
|
||||||
(when current-team
|
(when current-team
|
||||||
(let [sub-options (conj (vec (for [project current-projects]
|
(let [sub-options (concat (vec (for [project current-projects]
|
||||||
[(get-project-name project)
|
{:option-name (get-project-name project)
|
||||||
(on-move (:id current-team)
|
:id (get-project-id project)
|
||||||
(:id project))]))
|
:option-handler (on-move (:id current-team)
|
||||||
|
(:id project))}))
|
||||||
(when (seq other-teams)
|
(when (seq other-teams)
|
||||||
[(tr "dashboard.move-to-other-team") nil
|
[{:option-name (tr "dashboard.move-to-other-team")
|
||||||
|
:id "move-to-other-team"
|
||||||
|
:sub-options
|
||||||
(for [team other-teams]
|
(for [team other-teams]
|
||||||
[(get-team-name team) nil
|
{:option-name (get-team-name team)
|
||||||
|
:id (get-project-id team)
|
||||||
|
:sub-options
|
||||||
(for [sub-project (:projects team)]
|
(for [sub-project (:projects team)]
|
||||||
[(get-project-name sub-project)
|
{:option-name (get-project-name sub-project)
|
||||||
(on-move (:id team)
|
:id (get-project-id sub-project)
|
||||||
(:id sub-project))])])
|
:option-handler (on-move (:id team)
|
||||||
"move-to-other-team"]))
|
(:id sub-project))})})}]))
|
||||||
|
|
||||||
options (if multi?
|
options (if multi?
|
||||||
[[(tr "dashboard.duplicate-multi" file-count) on-duplicate nil "duplicate-multi"]
|
[{:option-name (tr "dashboard.duplicate-multi" file-count)
|
||||||
|
:id "file-duplicate-multi"
|
||||||
|
:option-handler on-duplicate
|
||||||
|
:data-test "duplicate-multi"}
|
||||||
(when (or (seq current-projects) (seq other-teams))
|
(when (or (seq current-projects) (seq other-teams))
|
||||||
[(tr "dashboard.move-to-multi" file-count) nil sub-options "move-to-multi"])
|
{:option-name (tr "dashboard.move-to-multi" file-count)
|
||||||
[(tr "dashboard.export-binary-multi" file-count) on-export-binary-files]
|
:id "file-move-multi"
|
||||||
[(tr "dashboard.export-standard-multi" file-count) on-export-standard-files]
|
:sub-options sub-options
|
||||||
|
:data-test "move-to-multi"})
|
||||||
|
{:option-name (tr "dashboard.export-binary-multi" file-count)
|
||||||
|
:id "file-binari-export-multi"
|
||||||
|
:option-handler on-export-binary-files}
|
||||||
|
{:option-name (tr "dashboard.export-standard-multi" file-count)
|
||||||
|
:id "file-standard-export-multi"
|
||||||
|
:option-handler on-export-standard-files}
|
||||||
(when (:is-shared file)
|
(when (:is-shared file)
|
||||||
[(tr "labels.unpublish-multi-files" file-count) on-del-shared nil "file-del-shared"])
|
{:option-name (tr "labels.unpublish-multi-files" file-count)
|
||||||
|
:id "file-unpublish-multi"
|
||||||
|
:option-handler on-del-shared
|
||||||
|
:data-test "file-del-shared"})
|
||||||
(when (not is-lib-page?)
|
(when (not is-lib-page?)
|
||||||
[:separator]
|
{:option-name :separator}
|
||||||
[(tr "labels.delete-multi-files" file-count) on-delete nil "delete-multi-files"])]
|
{:option-name (tr "labels.delete-multi-files" file-count)
|
||||||
|
:id "file-delete-multi"
|
||||||
|
:option-handler on-delete
|
||||||
|
:data-test "delete-multi-files"})]
|
||||||
|
|
||||||
[[(tr "dashboard.open-in-new-tab") on-new-tab]
|
[{:option-name (tr "dashboard.open-in-new-tab")
|
||||||
[(tr "labels.rename") on-edit nil "file-rename"]
|
:id "file-open-new-tab"
|
||||||
[(tr "dashboard.duplicate") on-duplicate nil "file-duplicate"]
|
:option-handler on-new-tab}
|
||||||
|
{:option-name (tr "labels.rename")
|
||||||
|
:id "file-rename"
|
||||||
|
:option-handler on-edit
|
||||||
|
:data-test "file-rename"}
|
||||||
|
{:option-name (tr "dashboard.duplicate")
|
||||||
|
:id "file-duplicate"
|
||||||
|
:option-handler on-duplicate
|
||||||
|
:data-test "file-duplicate"}
|
||||||
(when (and (not is-lib-page?) (or (seq current-projects) (seq other-teams)))
|
(when (and (not is-lib-page?) (or (seq current-projects) (seq other-teams)))
|
||||||
[(tr "dashboard.move-to") nil sub-options "file-move-to"])
|
{:option-name (tr "dashboard.move-to")
|
||||||
|
:id "file-move-to"
|
||||||
|
:sub-options sub-options
|
||||||
|
:data-test "file-move-to"})
|
||||||
(if (:is-shared file)
|
(if (:is-shared file)
|
||||||
[(tr "dashboard.unpublish-shared") on-del-shared nil "file-del-shared"]
|
{:option-name (tr "dashboard.unpublish-shared")
|
||||||
[(tr "dashboard.add-shared") on-add-shared nil "file-add-shared"])
|
:id "file-del-shared"
|
||||||
[:separator]
|
:option-handler on-del-shared
|
||||||
[(tr "dashboard.download-binary-file") on-export-binary-files nil "download-binary-file"]
|
:data-test "file-del-shared"}
|
||||||
[(tr "dashboard.download-standard-file") on-export-standard-files nil "download-standard-file"]
|
{:option-name (tr "dashboard.add-shared")
|
||||||
|
:id "file-add-shared"
|
||||||
|
:option-handler on-add-shared
|
||||||
|
:data-test "file-add-shared"})
|
||||||
|
{:option-name :separator}
|
||||||
|
{:option-name (tr "dashboard.download-binary-file")
|
||||||
|
:id "file-download-binary"
|
||||||
|
:option-handler on-export-binary-files
|
||||||
|
:data-test "download-binary-file"}
|
||||||
|
{:option-name (tr "dashboard.download-standard-file")
|
||||||
|
:id "file-download-standard"
|
||||||
|
:option-handler on-export-standard-files
|
||||||
|
:data-test "download-standard-file"}
|
||||||
(when (not is-lib-page?)
|
(when (not is-lib-page?)
|
||||||
[:separator]
|
{:option-name :separator}
|
||||||
[(tr "labels.delete") on-delete nil "file-delete"])])]
|
{:option-name (tr "labels.delete")
|
||||||
|
:id "file-delete"
|
||||||
|
:option-handler on-delete
|
||||||
|
:data-test "file-delete"})])]
|
||||||
|
|
||||||
[:& context-menu {:on-close on-menu-close
|
[:& context-menu-a11y {:on-close on-menu-close
|
||||||
:show show?
|
:show show?
|
||||||
:fixed? (or (not= top 0) (not= left 0))
|
:fixed? (or (not= top 0) (not= left 0))
|
||||||
:min-width? true
|
:min-width? true
|
||||||
:top top
|
:top top
|
||||||
:left left
|
:left left
|
||||||
:options options}]))))
|
:options options
|
||||||
|
:origin parent-id}]))))
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.files.features :as ffeat]
|
[app.common.files.features :as ffeat]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.logging :as log]
|
[app.common.logging :as log]
|
||||||
[app.main.data.dashboard :as dd]
|
[app.main.data.dashboard :as dd]
|
||||||
[app.main.data.messages :as msg]
|
[app.main.data.messages :as msg]
|
||||||
|
@ -251,7 +252,14 @@
|
||||||
(st/emit! (dd/clear-selected-files)))
|
(st/emit! (dd/clear-selected-files)))
|
||||||
(st/emit! (dd/toggle-file-select file)))
|
(st/emit! (dd/toggle-file-select file)))
|
||||||
|
|
||||||
(let [position (dom/get-client-position event)]
|
(let [client-position (dom/get-client-position event)
|
||||||
|
position (if (and (nil? (:y client-position)) (nil? (:x client-position)))
|
||||||
|
(let [target-element (dom/get-target event)
|
||||||
|
points (dom/get-bounding-rect target-element)
|
||||||
|
y (:top points)
|
||||||
|
x (:left points)]
|
||||||
|
(gpt/point x y))
|
||||||
|
client-position)]
|
||||||
(swap! local assoc
|
(swap! local assoc
|
||||||
:menu-open true
|
:menu-open true
|
||||||
:menu-pos position))))
|
:menu-pos position))))
|
||||||
|
@ -277,7 +285,7 @@
|
||||||
(swap! local assoc :menu-open false)))
|
(swap! local assoc :menu-open false)))
|
||||||
|
|
||||||
[:li.grid-item.project-th
|
[:li.grid-item.project-th
|
||||||
[:a
|
[:button
|
||||||
{:tab-index "0"
|
{:tab-index "0"
|
||||||
:class (dom/classnames :selected selected?
|
:class (dom/classnames :selected selected?
|
||||||
:library library-view?)
|
:library library-view?)
|
||||||
|
@ -314,9 +322,11 @@
|
||||||
[:div.project-th-icon.menu
|
[:div.project-th-icon.menu
|
||||||
{:tab-index "0"
|
{:tab-index "0"
|
||||||
:ref menu-ref
|
:ref menu-ref
|
||||||
|
:id (str file-id "-action-menu")
|
||||||
:on-click on-menu-click
|
:on-click on-menu-click
|
||||||
:on-key-down (fn [event]
|
:on-key-down (fn [event]
|
||||||
(when (kbd/enter? event)
|
(when (kbd/enter? event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
(on-menu-click event)))}
|
(on-menu-click event)))}
|
||||||
i/actions
|
i/actions
|
||||||
(when selected?
|
(when selected?
|
||||||
|
@ -328,8 +338,8 @@
|
||||||
:on-edit on-edit
|
:on-edit on-edit
|
||||||
:on-menu-close on-menu-close
|
:on-menu-close on-menu-close
|
||||||
:origin origin
|
:origin origin
|
||||||
:dashboard-local dashboard-local}])]]]]]))
|
:dashboard-local dashboard-local
|
||||||
|
:parent-id (str file-id "-action-menu")}])]]]]]))
|
||||||
|
|
||||||
(mf/defc grid
|
(mf/defc grid
|
||||||
[{:keys [files project origin limit library-view? create-fn] :as props}]
|
[{:keys [files project origin limit library-view? create-fn] :as props}]
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.context-menu :refer [context-menu]]
|
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]]
|
||||||
[app.main.ui.context :as ctx]
|
[app.main.ui.context :as ctx]
|
||||||
[app.main.ui.dashboard.import :as udi]
|
[app.main.ui.dashboard.import :as udi]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
|
@ -94,34 +94,54 @@
|
||||||
on-finish-import
|
on-finish-import
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn []
|
(fn []
|
||||||
(when (fn? on-import) (on-import))))]
|
(when (fn? on-import) (on-import))))
|
||||||
|
|
||||||
|
options [(when-not (:is-default project)
|
||||||
|
{:option-name (tr "labels.rename")
|
||||||
|
:id "project-menu-rename"
|
||||||
|
:option-handler on-edit
|
||||||
|
:data-test "project-rename"})
|
||||||
|
(when-not (:is-default project)
|
||||||
|
{:option-name (tr "dashboard.duplicate")
|
||||||
|
:id "project-menu-duplicated"
|
||||||
|
:option-handler on-duplicate
|
||||||
|
:data-test "project-duplicate"})
|
||||||
|
(when-not (:is-default project)
|
||||||
|
{:option-name (tr "dashboard.pin-unpin")
|
||||||
|
:id "project-menu-pin"
|
||||||
|
:option-handler toggle-pin})
|
||||||
|
|
||||||
|
(when (and (seq teams) (not (:is-default project)))
|
||||||
|
{:option-name (tr "dashboard.move-to")
|
||||||
|
:id "project-menu-move-to"
|
||||||
|
:sub-options (for [team teams]
|
||||||
|
{:option-name (:name team)
|
||||||
|
:id (:name team)
|
||||||
|
:option-handler (on-move (:id team))})
|
||||||
|
:data-test "project-move-to"})
|
||||||
|
(when (some? on-import)
|
||||||
|
{:option-name (tr "dashboard.import")
|
||||||
|
:id "project-menu-import"
|
||||||
|
:option-handler on-import-files
|
||||||
|
:data-test "file-import"})
|
||||||
|
(when-not (:is-default project)
|
||||||
|
{:option-name :separator})
|
||||||
|
(when-not (:is-default project)
|
||||||
|
{:option-name (tr "labels.delete")
|
||||||
|
:id "project-menu-delete"
|
||||||
|
:option-handler on-delete
|
||||||
|
:data-test "project-delete"})]]
|
||||||
|
|
||||||
[:*
|
[:*
|
||||||
[:& udi/import-form {:ref file-input
|
[:& udi/import-form {:ref file-input
|
||||||
:project-id (:id project)
|
:project-id (:id project)
|
||||||
:on-finish-import on-finish-import}]
|
:on-finish-import on-finish-import}]
|
||||||
[:& context-menu
|
[:& context-menu-a11y
|
||||||
{:on-close on-menu-close
|
{:on-close on-menu-close
|
||||||
:show show?
|
:show show?
|
||||||
:fixed? (or (not= top 0) (not= left 0))
|
:fixed? (or (not= top 0) (not= left 0))
|
||||||
:min-width? true
|
:min-width? true
|
||||||
:top top
|
:top top
|
||||||
:left left
|
:left left
|
||||||
:options [(when-not (:is-default project)
|
:options options}]]))
|
||||||
[(tr "labels.rename") on-edit nil "project-rename"])
|
|
||||||
(when-not (:is-default project)
|
|
||||||
[(tr "dashboard.duplicate") on-duplicate nil "project-duplicate"])
|
|
||||||
(when-not (:is-default project)
|
|
||||||
[(tr "dashboard.pin-unpin") toggle-pin])
|
|
||||||
(when (and (seq teams) (not (:is-default project)))
|
|
||||||
[(tr "dashboard.move-to") nil
|
|
||||||
(for [team teams]
|
|
||||||
[(:name team) (on-move (:id team))])
|
|
||||||
"project-move-to"])
|
|
||||||
(when (some? on-import)
|
|
||||||
[(tr "dashboard.import") on-import-files nil "file-import"])
|
|
||||||
(when-not (:is-default project)
|
|
||||||
[:separator])
|
|
||||||
(when-not (:is-default project)
|
|
||||||
[(tr "labels.delete") on-delete nil "project-delete"])]}]]))
|
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.ui.dashboard.projects
|
(ns app.main.ui.dashboard.projects
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
[app.main.data.dashboard :as dd]
|
[app.main.data.dashboard :as dd]
|
||||||
[app.main.data.events :as ev]
|
[app.main.data.events :as ev]
|
||||||
|
@ -187,13 +188,23 @@
|
||||||
toggle-pin
|
toggle-pin
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps project)
|
(mf/deps project)
|
||||||
#(st/emit! (dd/toggle-project-pin project)))
|
(fn [event]
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(st/emit! (dd/toggle-project-pin project))))
|
||||||
|
|
||||||
on-menu-click
|
on-menu-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [position (dom/get-client-position event)]
|
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
|
|
||||||
|
(let [client-position (dom/get-client-position event)
|
||||||
|
position (if (and (nil? (:y client-position)) (nil? (:x client-position)))
|
||||||
|
(let [target-element (dom/get-target event)
|
||||||
|
points (dom/get-bounding-rect target-element)
|
||||||
|
y (:top points)
|
||||||
|
x (:left points)]
|
||||||
|
(gpt/point x y))
|
||||||
|
client-position)]
|
||||||
(swap! local assoc
|
(swap! local assoc
|
||||||
:menu-open true
|
:menu-open true
|
||||||
:menu-pos position))))
|
:menu-pos position))))
|
||||||
|
@ -276,7 +287,7 @@
|
||||||
[:& project-menu
|
[:& project-menu
|
||||||
{:project project
|
{:project project
|
||||||
:show? (:menu-open @local)
|
:show? (:menu-open @local)
|
||||||
:left (:x (:menu-pos @local))
|
:left (+ 24 (:x (:menu-pos @local)))
|
||||||
:top (:y (:menu-pos @local))
|
:top (:y (:menu-pos @local))
|
||||||
:on-edit on-edit-open
|
:on-edit on-edit-open
|
||||||
:on-menu-close on-menu-close
|
:on-menu-close on-menu-close
|
||||||
|
@ -294,9 +305,6 @@
|
||||||
:on-click toggle-pin
|
:on-click toggle-pin
|
||||||
:alt (tr "dashboard.pin-unpin")
|
:alt (tr "dashboard.pin-unpin")
|
||||||
:aria-label (tr "dashboard.pin-unpin")
|
:aria-label (tr "dashboard.pin-unpin")
|
||||||
:on-key-down (fn [event]
|
|
||||||
(when (kbd/enter? event)
|
|
||||||
(toggle-pin event)))
|
|
||||||
:tab-index "0"}
|
:tab-index "0"}
|
||||||
(if (:is-pinned project)
|
(if (:is-pinned project)
|
||||||
i/pin-fill
|
i/pin-fill
|
||||||
|
@ -321,22 +329,27 @@
|
||||||
:tab-index "0"
|
:tab-index "0"
|
||||||
:on-key-down (fn [event]
|
:on-key-down (fn [event]
|
||||||
(when (kbd/enter? event)
|
(when (kbd/enter? event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
(on-menu-click event)))}
|
(on-menu-click event)))}
|
||||||
i/actions]]]
|
i/actions]]]]
|
||||||
|
|
||||||
(when (and (> limit 0)
|
|
||||||
(> file-count limit))
|
|
||||||
[:div.show-more {:on-click on-nav}
|
|
||||||
[:div.placeholder-label
|
|
||||||
(tr "dashboard.show-all-files")]
|
|
||||||
[:div.placeholder-icon i/arrow-down]])]
|
|
||||||
|
|
||||||
[:& line-grid
|
[:& line-grid
|
||||||
{:project project
|
{:project project
|
||||||
:team team
|
:team team
|
||||||
:files files
|
:files files
|
||||||
:create-fn create-file
|
:create-fn create-file
|
||||||
:limit limit}]]))
|
:limit limit}]
|
||||||
|
|
||||||
|
(when (and (> limit 0)
|
||||||
|
(> file-count limit))
|
||||||
|
[:button.show-more {:on-click on-nav
|
||||||
|
:tab-index "0"
|
||||||
|
:on-key-down (fn [event]
|
||||||
|
(when (kbd/enter? event)
|
||||||
|
(on-nav)))}
|
||||||
|
[:div.placeholder-label
|
||||||
|
(tr "dashboard.show-all-files")]
|
||||||
|
[:div.placeholder-icon i/arrow-down]])]))
|
||||||
|
|
||||||
|
|
||||||
(def recent-files-ref
|
(def recent-files-ref
|
||||||
|
|
|
@ -147,7 +147,7 @@
|
||||||
[^js node selector]
|
[^js node selector]
|
||||||
|
|
||||||
(loop [current node]
|
(loop [current node]
|
||||||
(if (or (nil? current) (.matches current selector) )
|
(if (or (nil? current) (.matches current selector))
|
||||||
current
|
current
|
||||||
(recur (.-parentElement current)))))
|
(recur (.-parentElement current)))))
|
||||||
|
|
||||||
|
@ -309,6 +309,13 @@
|
||||||
(when (some? el)
|
(when (some? el)
|
||||||
(.querySelectorAll el selector))))
|
(.querySelectorAll el selector))))
|
||||||
|
|
||||||
|
(defn get-element-offset-position
|
||||||
|
[^js node]
|
||||||
|
(when (some? node)
|
||||||
|
(let [x (.-offsetTop node)
|
||||||
|
y (.-offsetLeft node)]
|
||||||
|
(gpt/point x y))))
|
||||||
|
|
||||||
(defn get-client-position
|
(defn get-client-position
|
||||||
[^js event]
|
[^js event]
|
||||||
(let [x (.-clientX event)
|
(let [x (.-clientX event)
|
||||||
|
@ -567,7 +574,7 @@
|
||||||
(let [extension (cm/mtype->extension mtype)
|
(let [extension (cm/mtype->extension mtype)
|
||||||
opts {:suggestedName (str filename "." extension)
|
opts {:suggestedName (str filename "." extension)
|
||||||
:types [{:description description
|
:types [{:description description
|
||||||
:accept { mtype [(str "." extension)]}}]}]
|
:accept {mtype [(str "." extension)]}}]}]
|
||||||
|
|
||||||
(-> (p/let [file-system (.showSaveFilePicker globals/window (clj->js opts))
|
(-> (p/let [file-system (.showSaveFilePicker globals/window (clj->js opts))
|
||||||
writable (.createWritable file-system)
|
writable (.createWritable file-system)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue