0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-12 15:51:37 -05:00
penpot/frontend/src/app/main/ui/dashboard/grid.cljs
2021-03-29 10:51:07 +02:00

367 lines
13 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.dashboard.grid
(:require
[app.common.math :as mth]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.file-menu :refer [file-menu]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.icons :as i]
[app.main.worker :as wrk]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [t tr]]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[app.util.time :as dt]
[app.util.timers :as ts]
[beicon.core :as rx]
[cuerdas.core :as str]
[lambdaisland.uri :as uri]
[rumext.alpha :as mf]))
;; --- Grid Item Thumbnail
(mf/defc grid-item-thumbnail
{::mf/wrap [mf/memo]}
[{:keys [file] :as props}]
(let [container (mf/use-ref)]
(mf/use-effect
(mf/deps (:id file))
(fn []
(->> (wrk/ask! {:cmd :thumbnails/generate
:file-id (:id file)
:page-id (get-in file [:data :pages 0])})
(rx/subs (fn [{:keys [svg fonts]}]
(run! fonts/ensure-loaded! fonts)
(when-let [node (mf/ref-val container)]
(set! (.-innerHTML ^js node) svg)))))))
[:div.grid-item-th {:style {:background-color (get-in file [:data :options :background])}
:ref container}]))
;; --- Grid Item
(mf/defc grid-item-metadata
[{:keys [modified-at]}]
(let [locale (mf/deref i18n/locale)
time (dt/timeago modified-at {:locale locale})]
(str (t locale "ds.updated-at" time))))
(mf/defc grid-item
{:wrap [mf/memo]}
[{:keys [id file selected-files navigate?] :as props}]
(let [local (mf/use-state {:menu-open false
:menu-pos nil
:edition false})
locale (mf/deref i18n/locale)
item-ref (mf/use-ref)
menu-ref (mf/use-ref)
selected? (contains? selected-files id)
selected-file-objs
(deref refs/dashboard-selected-file-objs)
;; not needed to subscribe and repaint if changed
on-menu-close
(mf/use-callback
#(swap! local assoc :menu-open false))
on-select
(mf/use-callback
(mf/deps id selected? selected-files @local)
(fn [event]
(when (and (or (not selected?) (> (count selected-files) 1))
(not (:menu-open @local)))
(dom/stop-propagation event)
(let [shift? (kbd/shift? event)]
(when-not shift?
(st/emit! (dd/clear-selected-files)))
(st/emit! (dd/toggle-file-select {:file file}))))))
on-navigate
(mf/use-callback
(mf/deps id)
(fn [event]
(let [menu-icon (mf/ref-val menu-ref)
target (dom/get-target event)]
(when-not (dom/child? target menu-icon)
(let [pparams {:project-id (:project-id file)
:file-id (:id file)}
qparams {:page-id (first (get-in file [:data :pages]))}]
(st/emit! (rt/nav :workspace pparams qparams)))))))
create-counter
(mf/use-callback
(fn [element file-count]
(let [counter-el (dom/create-element "div")]
(dom/set-property! counter-el "class" "drag-counter")
(dom/set-text! counter-el (str file-count))
counter-el)))
on-drag-start
(mf/use-callback
(mf/deps selected-files)
(fn [event]
(let [offset (dom/get-offset-position (.-nativeEvent event))
select-current? (not (contains? selected-files (:id file)))
item-el (mf/ref-val item-ref)
counter-el (create-counter item-el
(if select-current?
1
(count selected-files)))]
(when select-current?
(st/emit! (dd/clear-selected-files))
(st/emit! (dd/toggle-file-select {:file file})))
(dnd/set-data! event "penpot/files" "dummy")
(dnd/set-allowed-effect! event "move")
;; set-drag-image requires that the element is rendered and
;; visible to the user at the moment of creating the ghost
;; image (to make a snapshot), but you may remove it right
;; afterwards, in the next render cycle.
(dom/append-child! item-el counter-el)
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
(ts/raf #(.removeChild item-el counter-el)))))
on-menu-click
(mf/use-callback
(mf/deps file selected?)
(fn [event]
(dom/prevent-default event)
(when-not selected?
(let [shift? (kbd/shift? event)]
(when-not shift?
(st/emit! (dd/clear-selected-files)))
(st/emit! (dd/toggle-file-select {:file file}))))
(let [position (dom/get-client-position event)]
(swap! local assoc :menu-open true
:menu-pos position))))
edit
(mf/use-callback
(mf/deps file)
(fn [name]
(st/emit! (dd/rename-file (assoc file :name name)))
(swap! local assoc :edition false)))
on-edit
(mf/use-callback
(mf/deps file)
(fn [event]
(dom/stop-propagation event)
(swap! local assoc
:edition true
:menu-open false)))]
(mf/use-effect
(mf/deps selected? local)
(fn []
(when (and (not selected?) (:menu-open @local))
(swap! local assoc :menu-open false))))
[:div.grid-item.project-th {:class (dom/classnames
:selected selected?)
:ref item-ref
:draggable true
:on-click on-select
:on-double-click on-navigate
:on-drag-start on-drag-start
:on-context-menu on-menu-click}
[:div.overlay]
[:& grid-item-thumbnail {:file file}]
(when (:is-shared file)
[:div.item-badge i/library])
[:div.item-info
(if (:edition @local)
[:& inline-edition {:content (:name file)
:on-end edit}]
[:h3 (:name file)])
[:& grid-item-metadata {:modified-at (:modified-at file)}]]
[:div.project-th-actions {:class (dom/classnames
:force-display (:menu-open @local))}
[:div.project-th-icon.menu
{:ref menu-ref
:on-click on-menu-click}
i/actions
(when selected?
[:& file-menu {:files selected-file-objs
:show? (:menu-open @local)
:left (:x (:menu-pos @local))
:top (:y (:menu-pos @local))
:navigate? navigate?
:on-edit on-edit
:on-menu-close on-menu-close}])]]]))
(mf/defc empty-placeholder
[{:keys [dragging?] :as props}]
(if-not dragging?
[:div.grid-empty-placeholder
[:div.icon i/file-html]
[:div.text (tr "dashboard.empty-files")]]
[:div.grid-row.no-wrap
[:div.grid-item]]))
(mf/defc loading-placeholder
[]
[:div.grid-empty-placeholder
[:div.icon i/loader]
[:div.text (tr "dashboard.loading-files")]])
(mf/defc grid
[{:keys [id opts files] :as props}]
(let [locale (mf/deref i18n/locale)
selected-files (mf/deref refs/dashboard-selected-files)]
[:section.dashboard-grid
(cond
(nil? files)
[:& loading-placeholder]
(seq files)
[:div.grid-row
(for [item files]
[:& grid-item
{:id (:id item)
:file item
:selected-files selected-files
:key (:id item)
:navigate? true}])]
:else
[:& empty-placeholder])]))
(mf/defc line-grid-row
[{:keys [locale files team-id selected-files on-load-more dragging?] :as props}]
(let [rowref (mf/use-ref)
width (mf/use-state nil)
itemsize 290
ratio (if (some? @width) (/ @width itemsize) 0)
nitems (mth/floor ratio)
limit (min 10 ;; Configuration in backend to return recent files
(if (and (some? @width)
(> (* itemsize (count files)) @width)
(< (- ratio nitems) 0.51))
(dec nitems) ;; Leave space for the "show all" block
nitems))
limit (if dragging?
(dec limit)
limit)
limit (max 1 limit)]
(mf/use-effect
(fn []
(let [node (mf/ref-val rowref)
obs (new js/ResizeObserver
(fn [entries x]
(ts/raf #(let [row (first entries)
row-rect (.-contentRect ^js row)
row-width (.-width ^js row-rect)]
(reset! width row-width)))))]
(.observe ^js obs node)
(fn []
(.disconnect ^js obs)))))
[:div.grid-row.no-wrap {:ref rowref}
(when dragging?
[:div.grid-item])
(for [item (take limit files)]
[:& grid-item
{:id (:id item)
:file item
:selected-files selected-files
:key (:id item)
:navigate? false}])
(when (and (> limit 0)
(> (count files) limit))
[:div.grid-item.placeholder {:on-click on-load-more}
[:div.placeholder-icon i/arrow-down]
[:div.placeholder-label
(t locale "dashboard.show-all-files")]])]))
(mf/defc line-grid
[{:keys [project-id team-id opts files on-load-more] :as props}]
(let [locale (mf/deref i18n/locale)
dragging? (mf/use-state false)
selected-files (mf/deref refs/dashboard-selected-files)
selected-project (mf/deref refs/dashboard-selected-project)
on-drag-enter
(mf/use-callback
(mf/deps selected-project)
(fn [e]
(when (dnd/has-type? e "penpot/files")
(dom/prevent-default e)
(when-not (or (dnd/from-child? e)
(dnd/broken-event? e))
(when (not= selected-project project-id)
(reset! dragging? true))))))
on-drag-over
(mf/use-callback
(fn [e]
(when (dnd/has-type? e "penpot/files")
(dom/prevent-default e))))
on-drag-leave
(mf/use-callback
(fn [e]
(when-not (dnd/from-child? e)
(reset! dragging? false))))
on-drop
(mf/use-callback
(mf/deps files selected-files)
(fn [e]
(reset! dragging? false)
(when (not= selected-project project-id)
(let [data {:ids selected-files
:project-id project-id}
mdata {:on-success
(st/emitf (dm/success (tr "dashboard.success-move-file"))
(dd/fetch-recent-files {:team-id team-id})
(dd/clear-selected-files))}]
(st/emit! (dd/move-files (with-meta data mdata)))))))]
[:section.dashboard-grid {:on-drag-enter on-drag-enter
:on-drag-over on-drag-over
:on-drag-leave on-drag-leave
:on-drop on-drop}
(cond
(nil? files)
[:& loading-placeholder]
(seq files)
[:& line-grid-row {:files files
:team-id team-id
:selected-files selected-files
:on-load-more on-load-more
:dragging? @dragging?
:locale locale}]
:else
[:& empty-placeholder {:dragging? @dragging?}])]))