Skip to content
Switch branches/tags
Go to file
Cannot retrieve contributors at this time
(:require [den1k.shortcuts :refer [shortcuts global-shortcuts]]
[examples.util.dom :as ud]
[examples.util.string :as ustr]
[uix.dom.alpha :as uix.dom]
[root.impl.core :as rc]
[root.impl.util :as u]
[xframe.core.alpha :as xf]
[ :as mock-data]
[uix.core.alpha :as uix]))
(def entity-actions
{:undo [[:undo]]
:redo [[:redo]]}
{:remove [[:remove [:<- :content]]]}
{:add [[:add
[:<- :content :items]
{:type :todo-item :active? true :markup ["New Todo"]}]]
:add-after [[:add-after
{:type :todo-item :active? true :markup ["New Todo"]}]]
:remove [[:remove [:<- :content :items]]]
:toggle-checked [[:toggle :checked?]]}})
(defn ent->ref [ent]
(:id ent))
(def projected-data
(u/project (fn [ent] [(ent->ref ent) ent]) mock-data/data))
(reset! rc/state projected-data)
(defn lookup* [x]
(if (coll? x)
(get (xf/<- [::xf/db]) x)))
(xf/reg-sub :get
(fn [k] (lookup* k)))
(def lookup lookup*)
;; see def of <sub which throws in simple compilation
(when-not (.-__REACT_DEVTOOLS_GLOBAL_HOOK__ js/window)
(set! (.-__REACT_DEVTOOLS_GLOBAL_HOOK__ js/window) nil))
(defn lookup-sub [id]
(xf/<sub [:get id]))
(def root
{:->ref ent->ref
:invoke-fn (fn invoke [f x]
;(js/console.log :ent x)
^{:key (ent->ref x)}
[f x])
:lookup lookup
:lookup-sub lookup-sub
:dispatch-fn (fn [x] (or (:view x) (:type x)))
:transact rc/transact
:content-keys [:content]
:content-spec integer?
:entity-actions entity-actions
:add-id rc/add-id}))
(global-shortcuts {"cmd+z" #(root :transact [[:undo]] {:history? false})
"cmd+shift+z" #(root :transact [[:redo]] {:history? false})})
(defn block
[{:as ent
:keys [id show-block-thumb? show-block-menu? path]
{:keys [remove]} :actions}
& children]
(into [:div.flex.items-center
(fn [e]
(root :transact
[[:set (assoc ent :show-block-thumb? true)]]
{:history? false}))
(fn [e]
(root :transact
[[:set (assoc ent :show-block-thumb? false
:show-block-menu? false)]]
{:history? false}))}
{:class (when-not show-block-thumb? "hide-child")}
{:on-click #(root :transact
[[:set (assoc ent :show-block-menu? true)]]
{:history? false})}
(when show-block-menu?
(let [li-tag]
{:style {:top "-0.2rem"}}
{:on-click remove}
"Turn into"
{:value (name ((:dispatch-fn root) ent))
:on-change #(let [opt-kw (-> % .-target .-value keyword)
ent (assoc ent :type opt-kw
:show-block-menu? false)
id (+ 1000 (rand-int 10e4))]
(case opt-kw
[[:set (dissoc ent :view :content)]]
[:<- :content :items]
{:id id :type :todo-item :active? true :markup ["empty"]}]
[:set (-> ent
(assoc :open? true)
(assoc-in [:content :items] [id]))]])))}]
(map (fn [x] [:option {:value x} x]))
["todo-item" "toggle-list"])]]))]]]
(root :view :button
(fn [{:as ent :keys [markup handlers]}]
handlers (first markup)]))
(defn input
[opts {:as ent :keys [markup active?]}]
(fn [node]
(when (and node active?)
(ud/set-cursor node 0 {:unless-active? true})))
#(let [v (-> % .-target .-innerText)]
(root :transact
[[:set (assoc ent :active? false
:markup [v])]]
{:history? (not= v (first markup))}))
#(root :transact [[:set (assoc ent :active? true)]]
{:history? false})}
(first markup)])
(root :view :toggle-list
(fn [{:as ent :keys [id markup content-ui open?]}]
(let [{:keys [items button]} content-ui]
[block ent
(cond-> [:div
{:style (merge
{:font-size 12
:line-height 1
:user-select :none}
(when open?
{:transform "rotate(90deg)"
:transform-origin :center}))
:on-click #(root :transact [[:toggle :open? ent]])}
[input {} ent]]
open? (conj items))])))
(root :view :nav
(fn [{:as ent :keys [content-ui routes]}]
(fn [[k v]]
{;; with history enabled this breaks due to a bug in spec that
;; does not respect a nonconforming during unform
:on-click #(root :transact [[:set (assoc ent :content v)]] {:history? false})}
(name k)]))
(root :view :todo-item
[{:as ent
:keys [path markup checked?]
{:keys [toggle-checked remove]} :actions}]
[block ent
[:input {:type :checkbox
:checked (boolean checked?)
:on-change toggle-checked}]
{"backspace" (fn [e]
(let [v (-> e .-target .-innerText)]
(if (empty? v)
; todo if index at first position concat with previous ent
#(let [v (-> % .-target .-innerText)
[tthis tnext] (ustr/split-at (ud/get-cursor) v)
tnext? (boolean (not-empty tnext))]
(root :transact
[[:set (assoc ent :active? false :markup [tthis])]
[:add-after path ((:add-id root) {:type :todo-item :active? true :markup [(or tnext "")]})]]
{:history? (or tnext? (not= tthis (first markup)))})
(defn ^:export example-root []
"A Poor Person's Notion Clone in 200 LoC"
[:h3 "Baby Steps toward a Rich Document Editor"]
[:b "Current Feature Set:"]
[:li "Routing (click " [:i "home"] " or " [:i "about"] ")"]
[:li "Undo/Redo through shortcuts or rendered buttons"]
[:li "Context menus (hover over the todos or toggle-lists)"]
[:li "Change " [:i.b "block"] " type (through context menu)"]
[:li "Arbitrarily deep nesting of views (make toggle-lists inside toggle-lists)"]]
{:open false}
[:summary.outline-0.pointer "initial app-state"]
[ud/pretty-code-block 120 projected-data]]]
[:div "From the initial app-state root recurses through " [ ":content"]
" keys, looks up the data, resolves components and renders the following UI:"]]
[root :resolve {:root-id 1}]}])
(defn ^:export render-fn [dom-node]
(uix.dom/render [example-root] dom-node))