diff --git a/common/uxbox/common/pages.cljc b/common/uxbox/common/pages.cljc index 1105995391..d244cf2bd4 100644 --- a/common/uxbox/common/pages.cljc +++ b/common/uxbox/common/pages.cljc @@ -215,11 +215,14 @@ (defmethod process-change :del-obj [data {:keys [id] :as change}] - (when-let [{:keys [frame-id] :as obj} (get-in data [:objects id])] - (-> data - (update :objects dissoc id) - (update-in [:objects frame-id :shapes] - (fn [s] (filterv #(not= % id) s)))))) + (when-let [{:keys [frame-id shapes] :as obj} (get-in data [:objects id])] + (let [data (update data :objects dissoc id)] + (cond-> data + (contains? (:objects data) frame-id) + (update-in [:objects frame-id :shapes] (fn [s] (filterv #(not= % id) s))) + + (seq shapes) ; Recursive delete all dependend objects + (as-> $ (reduce #(process-change %1 {:type :del-obj :id %2}) $ shapes)))))) (defmethod process-operation :set [shape op] diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs index 50a39dc877..9df5f6c323 100644 --- a/frontend/src/uxbox/main.cljs +++ b/frontend/src/uxbox/main.cljs @@ -5,7 +5,7 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2015-2020 Andrey Antukh +;; Copyright (c) 2020 UXBOX Labs SL (ns ^:figwheel-hooks uxbox.main (:require diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index b72b87a16c..f1016a9792 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -11,6 +11,8 @@ (:require [clojure.set :as set] [beicon.core :as rx] + [goog.object :as gobj] + [goog.events :as events] [cljs.spec.alpha :as s] [potok.core :as ptk] [uxbox.common.data :as d] @@ -36,7 +38,11 @@ [uxbox.util.time :as dt] [uxbox.util.transit :as t] [uxbox.util.uuid :as uuid] - [vendor.randomcolor])) + [uxbox.util.webapi :as wapi] + [vendor.randomcolor]) + (:import goog.events.EventType + goog.events.KeyCodes + goog.ui.KeyboardShortcutHandler)) ;; TODO: temporal workaround (def clear-ruler nil) @@ -358,6 +364,63 @@ (let [local (:workspace-local state)] (assoc-in state [:workspace-cache page-id] local))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Data Persistence +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare persist-changes) +(declare diff-and-commit-changes) + +(defn initialize-page-persistence + [page-id] + (ptk/reify ::initialize-persistence + ptk/UpdateEvent + (update [_ state] + (assoc state ::page-id page-id)) + + ptk/WatchEvent + (watch [_ state stream] + (let [stoper (rx/filter #(or (ptk/type? ::finalize %) + (ptk/type? ::initialize-page %)) + stream) + notifier (->> stream + (rx/filter (ptk/type? ::commit-changes)) + (rx/debounce 2000) + (rx/merge stoper))] + (rx/merge + (->> stream + (rx/filter (ptk/type? ::commit-changes)) + (rx/map deref) + (rx/buffer-until notifier) + (rx/map vec) + (rx/filter (complement empty?)) + (rx/map #(persist-changes page-id %)) + (rx/take-until (rx/delay 100 stoper))) + (->> stream + (rx/filter #(satisfies? IBatchedChange %)) + (rx/debounce 200) + (rx/map (fn [_] (diff-and-commit-changes page-id))) + (rx/take-until stoper))))))) + +(defn persist-changes + [page-id changes] + (ptk/reify ::persist-changes + ptk/WatchEvent + (watch [_ state stream] + (let [session-id (:session-id state) + page (get-in state [:pages page-id]) + changes (->> changes + (mapcat identity) + (map #(assoc % :session-id session-id)) + (vec)) + params {:id (:id page) + :revn (:revn page) + :changes changes}] + (->> (rp/mutation :update-page params) + (rx/map shapes-changes-commited)))))) + + (defn- generate-operations [ma mb] (let [ma-keys (set (keys ma)) @@ -410,60 +473,6 @@ (when-not (empty? changes) (rx/of (commit-changes changes undo-changes))))))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Data Persistence -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare persist-changes) - -(defn initialize-page-persistence - [page-id] - (ptk/reify ::initialize-persistence - ptk/UpdateEvent - (update [_ state] - (assoc state ::page-id page-id)) - - ptk/WatchEvent - (watch [_ state stream] - (let [stoper (rx/filter #(or (ptk/type? ::finalize %) - (ptk/type? ::initialize-page %)) - stream) - notifier (->> stream - (rx/filter (ptk/type? ::commit-changes)) - (rx/debounce 2000) - (rx/merge stoper))] - (rx/merge - (->> stream - (rx/filter (ptk/type? ::commit-changes)) - (rx/map deref) - (rx/buffer-until notifier) - (rx/map vec) - (rx/filter (complement empty?)) - (rx/map #(persist-changes page-id %)) - (rx/take-until (rx/delay 100 stoper))) - (->> stream - (rx/filter #(satisfies? IBatchedChange %)) - (rx/debounce 200) - (rx/map (fn [_] (diff-and-commit-changes page-id))) - (rx/take-until stoper))))))) - -(defn persist-changes - [page-id changes] - (ptk/reify ::persist-changes - ptk/WatchEvent - (watch [_ state stream] - (let [session-id (:session-id state) - page (get-in state [:pages page-id]) - changes (->> changes - (mapcat identity) - (map #(assoc % :session-id session-id)) - (vec)) - params {:id (:id page) - :revn (:revn page) - :changes changes}] - (->> (rp/mutation :update-page params) - (rx/map shapes-changes-commited)))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Fetching & Uploading ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -896,7 +905,7 @@ in the current workspace page." [state] (let [page-id (::page-id state) - objects (get-in state [:workspace-page page-id :objects])] + objects (get-in state [:workspace-data page-id :objects])] (into #{} (map :name) (vals objects)))) (defn impl-generate-unique-name @@ -1043,14 +1052,12 @@ {:type :add-obj :id (:id obj) :frame-id frame-id - :obj (assoc obj :frame-id frame-id) - :session-id (:session-id state)})) + :obj (assoc obj :frame-id frame-id)})) (:shapes frame)) uchanges (mapv (fn [rch] {:type :del-obj - :id (:id rch) - :session-id (:session-id state)}) + :id (:id rch)}) rchanges) shapes (mapv :id rchanges) @@ -1068,7 +1075,7 @@ :id frame-id :session-id (:session-id state)}] (rx/of (commit-changes (d/concat [rchange] rchanges) - (d/concat [uchange] uchanges) + (d/concat [] uchanges [uchange]) {:commit-local? true})))))) @@ -1096,7 +1103,6 @@ (rx/empty)))))) - ;; --- Toggle shape's selection status (selected or deselected) (defn select-shape @@ -1979,6 +1985,95 @@ (assoc-in state [:workspace-local :context-menu] nil)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Clipboard +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def copy-selected + (letfn [(prepare-selected [state selected] + (let [data (reduce #(prepare %1 state %2) {} selected)] + {:type :copied-shapes + :data (assoc data :selected selected)})) + + (prepare [result state id] + (let [page-id (::page-id state) + objects (get-in state [:workspace-data page-id :objects]) + object (get objects id)] + (cond-> (assoc-in result [:objects id] object) + (= :frame (:type object)) + (as-> $ (reduce #(prepare %1 state %2) $ (:shapes object)))))) + + (on-copy-error [error] + (js/console.error "Clipboard blocked:" error) + (rx/empty))] + + (ptk/reify ::copy-selected + ptk/WatchEvent + (watch [_ state stream] + (let [selected (get-in state [:workspace-local :selected]) + cdata (prepare-selected state selected)] + (->> (rx/from (wapi/write-to-clipboard cdata)) + (rx/catch on-copy-error) + (rx/ignore))))))) + + +(defn- paste-impl + [{:keys [selected objects] :as data}] + (letfn [(prepare-change [id] + (let [obj (get objects id)] + ;; (prn "prepare-change" id obj) + (if (= :frame (:type obj)) + (prepare-frame-change obj) + (prepare-shape-change obj uuid/zero)))) + + (prepare-shape-change [obj frame-id] + (let [id (uuid/next)] + {:type :add-obj + :id id + :frame-id frame-id + :obj (assoc obj :id id :frame-id frame-id)})) + + (prepare-frame-change [obj] + (let [frame-id (uuid/next) + sch (->> (map #(get objects %) (:shapes obj)) + (map #(prepare-shape-change % frame-id))) + fch {:type :add-obj + :id frame-id + :frame-id uuid/zero + :obj (-> obj + (assoc :id frame-id) + (assoc :frame-id uuid/zero) + (assoc :shapes (mapv :id sch)))}] + (d/concat [fch] sch)))] + + (ptk/reify ::paste-impl + ptk/WatchEvent + (watch [_ state stream] + (let [rchanges (->> (map prepare-change selected) + (flatten)) + uchanges (map (fn [ch] + {:type :del-obj + :id (:id ch)}) + rchanges)] + (cljs.pprint/pprint rchanges) + (rx/of (commit-changes (vec rchanges) + (vec (reverse uchanges)) + {:commit-local? true}))))))) + +(def paste + (ptk/reify ::paste + ptk/WatchEvent + (watch [_ state stream] + (->> (rx/from (wapi/read-from-clipboard)) + (rx/filter #(= :copied-shapes (:type %))) + (rx/pr-log "pasting:") + (rx/map :data) + (rx/map paste-impl) + (rx/catch (fn [err] + (js/console.error "Clipboard blocked:" err) + (rx/empty))))))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Page Changes Reactions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1998,3 +2093,63 @@ pages (vec (concat before [id] after))] (assoc-in state [:projects (:project-id page) :pages] pages))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Shortcuts +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def shortcuts + {"ctrl+shift+m" #(rx/of (toggle-layout-flag :sitemap)) + "ctrl+shift+f" #(rx/of (toggle-layout-flag :drawtools)) + "ctrl+shift+i" #(rx/of (toggle-layout-flag :icons)) + "ctrl+shift+l" #(rx/of (toggle-layout-flag :layers)) + "ctrl+0" #(rx/of (reset-zoom)) + "ctrl+d" #(rx/of duplicate-selected) + "ctrl+z" #(rx/of undo) + "ctrl+shift+z" #(rx/of redo) + "ctrl+y" #(rx/of redo) + "ctrl+q" #(rx/of reinitialize-undo) + "ctrl+b" #(rx/of (select-for-drawing :rect)) + "ctrl+e" #(rx/of (select-for-drawing :circle)) + "ctrl+t" #(rx/of (select-for-drawing :text)) + "ctrl+c" #(rx/of copy-selected) + "ctrl+v" #(rx/of paste) + "esc" #(rx/of :interrupt deselect-all) + "delete" #(rx/of delete-selected) + "ctrl+up" #(rx/of (vertical-order-selected :up)) + "ctrl+down" #(rx/of (vertical-order-selected :down)) + "ctrl+shift+up" #(rx/of (vertical-order-selected :top)) + "ctrl+shift+down" #(rx/of (vertical-order-selected :bottom)) + "shift+up" #(rx/of (move-selected :up true)) + "shift+down" #(rx/of (move-selected :down true)) + "shift+right" #(rx/of (move-selected :right true)) + "shift+left" #(rx/of (move-selected :left true)) + "up" #(rx/of (move-selected :up false)) + "down" #(rx/of (move-selected :down false)) + "right" #(rx/of (move-selected :right false)) + "left" #(rx/of (move-selected :left false))}) + +(def initialize-shortcuts + (letfn [(initialize [sink] + (let [handler (KeyboardShortcutHandler. js/document)] + + ;; Register shortcuts. + (run! #(.registerShortcut handler % %) (keys shortcuts)) + + ;; Initialize shortcut listener. + (let [event KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED + callback #(sink (gobj/get % "identifier")) + key (events/listen handler event callback)] + (fn [] + (events/unlistenByKey key) + (.clearKeyListener handler)))))] + (ptk/reify ::initialize-shortcuts + ptk/WatchEvent + (watch [_ state stream] + (let [stoper (rx/filter #(= ::finalize-shortcuts %) stream)] + (->> (rx/create initialize) + (rx/pr-log "[debug]: shortcut:") + (rx/map #(get shortcuts %)) + (rx/filter fn?) + (rx/merge-map (fn [f] (f))) + (rx/take-until stoper))))))) diff --git a/frontend/src/uxbox/main/ui/shapes/frame.cljs b/frontend/src/uxbox/main/ui/shapes/frame.cljs index a1dfad528d..fc235b7476 100644 --- a/frontend/src/uxbox/main/ui/shapes/frame.cljs +++ b/frontend/src/uxbox/main/ui/shapes/frame.cljs @@ -133,7 +133,6 @@ translate #(translate-to-frame % ds-modifier (gpt/point (- x) (- y))) ] - [:svg {:x x :y y :width width :height height} [:& "rect" props] (for [item (reverse childs)] diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index 2278d05c3c..e29cfa1261 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -26,7 +26,6 @@ [uxbox.main.ui.workspace.header :refer [header]] [uxbox.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]] [uxbox.main.ui.workspace.scroll :as scroll] - [uxbox.main.ui.workspace.shortcuts :as shortcuts] [uxbox.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [uxbox.main.ui.workspace.sidebar.history :refer [history-dialog]] [uxbox.main.ui.workspace.left-toolbar :refer [left-toolbar]] @@ -119,10 +118,11 @@ {:fn #(st/emit! dw/initialize-layout)}) (mf/use-effect - {:deps (mf/deps file-id page-id) + {:deps (mf/deps file-id) :fn (fn [] - (let [sub (shortcuts/init)] - #(rx/cancel! sub)))}) + (st/emit! dw/initialize-shortcuts) + #(st/emit! ::dw/finalize-shortcuts))}) + (let [file (mf/deref refs/workspace-file) page (mf/deref refs/workspace-page) layout (mf/deref refs/workspace-layout)] diff --git a/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs b/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs deleted file mode 100644 index 120887265a..0000000000 --- a/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs +++ /dev/null @@ -1,81 +0,0 @@ -;; 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) 2015-2016 Andrey Antukh -;; Copyright (c) 2015-2016 Juan de la Cruz - -(ns uxbox.main.ui.workspace.shortcuts - (:require [goog.events :as events] - [beicon.core :as rx] - [potok.core :as ptk] - [uxbox.main.store :as st] - [uxbox.main.data.lightbox :as dl] - [uxbox.main.data.workspace :as dw]) - (:import goog.events.EventType - goog.events.KeyCodes - goog.ui.KeyboardShortcutHandler)) - -(declare move-selected) - -;; --- Shortcuts - -(defonce +shortcuts+ - { - ;; :shift+g #(st/emit! (dw/toggle-flag :grid)) - :ctrl+shift+m #(st/emit! (dw/toggle-layout-flag :sitemap)) - :ctrl+shift+f #(st/emit! (dw/toggle-layout-flag :drawtools)) - :ctrl+shift+i #(st/emit! (dw/toggle-layout-flag :icons)) - :ctrl+shift+l #(st/emit! (dw/toggle-layout-flag :layers)) - :ctrl+0 #(st/emit! (dw/reset-zoom)) - ;; :ctrl+r #(st/emit! (dw/toggle-flag :ruler)) - :ctrl+d #(st/emit! dw/duplicate-selected) - :ctrl+z #(st/emit! dw/undo) - :ctrl+shift+z #(st/emit! dw/redo) - :ctrl+y #(st/emit! dw/redo) - :ctrl+q #(st/emit! dw/reinitialize-undo) - :ctrl+b #(st/emit! (dw/select-for-drawing :rect)) - :ctrl+e #(st/emit! (dw/select-for-drawing :circle)) - :ctrl+t #(st/emit! (dw/select-for-drawing :text)) - :esc #(st/emit! :interrupt dw/deselect-all) - :delete #(st/emit! dw/delete-selected) - :ctrl+up #(st/emit! (dw/vertical-order-selected :up)) - :ctrl+down #(st/emit! (dw/vertical-order-selected :down)) - :ctrl+shift+up #(st/emit! (dw/vertical-order-selected :top)) - :ctrl+shift+down #(st/emit! (dw/vertical-order-selected :bottom)) - :shift+up #(st/emit! (dw/move-selected :up true)) - :shift+down #(st/emit! (dw/move-selected :down true)) - :shift+right #(st/emit! (dw/move-selected :right true)) - :shift+left #(st/emit! (dw/move-selected :left true)) - :up #(st/emit! (dw/move-selected :up false)) - :down #(st/emit! (dw/move-selected :down false)) - :right #(st/emit! (dw/move-selected :right false)) - :left #(st/emit! (dw/move-selected :left false)) - }) - -;; --- Shortcuts Setup Functions - -(defn- watch-shortcuts - [sink] - (let [handler (KeyboardShortcutHandler. js/document)] - - ;; Register shortcuts. - (doseq [item (keys +shortcuts+)] - (let [identifier (name item)] - (.registerShortcut handler identifier identifier))) - - ;; Initialize shortcut listener. - (let [event KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED - callback #(sink (keyword (.-identifier %))) - key (events/listen handler event callback)] - (fn [] - (events/unlistenByKey key) - (.clearKeyListener handler))))) - -(defn init - [] - (let [stream (->> (rx/create watch-shortcuts) - (rx/pr-log "[debug]: shortcut:"))] - (rx/on-value stream (fn [event] - (when-let [handler (get +shortcuts+ event)] - (handler)))))) diff --git a/frontend/src/uxbox/util/webapi.cljs b/frontend/src/uxbox/util/webapi.cljs index e079e75e6a..29f605e527 100644 --- a/frontend/src/uxbox/util/webapi.cljs +++ b/frontend/src/uxbox/util/webapi.cljs @@ -7,8 +7,10 @@ (ns uxbox.util.webapi "HTML5 web api helpers." (:require + [promesa.core :as p] [beicon.core :as rx] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [uxbox.util.transit :as t])) (defn read-file-as-text [file] @@ -65,4 +67,19 @@ ;; (rx/create on-subscribe))) +(defn write-to-clipboard + [data] + (let [cboard (unchecked-get js/navigator "clipboard")] + (.writeText cboard (uxbox.util.transit/encode data)))) + +(defn- read-from-clipboard + [] + (let [cboard (unchecked-get js/navigator "clipboard")] + (-> (.readText cboard) + (p/then (fn [data] + (try + (t/decode data) + (catch :default e + nil))))))) +