(ns weathergen.ui.grids (:require [garden.core :refer [css]] [garden.selectors :as css-sel] [hoplon.svg :as svg] [taoensso.timbre :as log :refer-macros ^{:line 7, :column 28} (log trace debug info warn error fatal report logf tracef debugf infof warnf errorf fatalf reportf spy get-env log-env)] [weathergen.help :as help] [weathergen.ui.buttons :as buttons] [weathergen.ui.common :as comm :refer [inl px register-styles! triangle]] [javelin.core :refer [->Cell input? cell cell? destroy-cell! lift lens? set-formula! constant? cell-doseq* deref* set-cell! lens formula? alts! dosync* cell-map formula]] [hoplon.jquery :refer [set-attributes! check-val! set-styles! text-val!]] [hoplon.core :refer [form audio input menuitem hgroup do! timeout $text base h1 set-attributes! embed shadow h3 body keygen progress main cite on-page-load i p nav ruby a menu blockquote img $comment span track data u dl select html thead del fieldset aside figure figcaption q on! bdi append-child! video address caption dd rp hr tbody table html-var add-initfn! pre ul replace-child! html-time html-map sup dfn sub mark script button wbr insert-before! strong normalize-class li dt rtc td tr section th optgroup bust-cache iframe remove-child! legend em kbd spliced html-object article abbr template multicol prerendering? <!-- source output set-styles! route-cell header datalist tfoot s ins footer title h5 canvas param div option summary samp small style textarea loop-tpl* h4 head ol details col label picture rt when-dom h6 link page-load colgroup meter html-meta static-elements bdo --> b attribute? code dialog noframes do-watch noscript safe-nth h2 area br]]) (:require-macros [weathergen.cljs.macros :refer [with-time formula-of with-attr-bindings]] [javelin.core :refer [with-let mx2 dosync cell= set-cell!= prop-cell cell-doseq formulet defc defc= macroexpand-all mx formula-of cell-let]] [hoplon.core :refer [definterval text elem+ cache-key with-timeout defelem+ when-tpl static sexp defelem elem def-values if-tpl cond-tpl with-page-load for-tpl with-dom case-tpl loop-tpl with-interval with-init!]]))














;; (set! *warn-on-infer* true)

(defelem master-detail
  "Creates a master-detail grid view. TODO: explain options."
  [attrs _]
  (let [{:keys [data detail columns formatter options]} attrs]
    (div
     (dissoc attrs :data :detail :columns :formatter :options)
     (let [data* (cell= data)
           columns* (cell= columns)
           options* (cell= options)
           ;; TODO: Should detail be permitted to be a cell?
           td* (fn [& args]
                 (apply td :css {:border "solid 1px #eee"
                                 :padding "0 2px 0 2px"} args))]
       (table
        :css {:background "white"
              :border-collapse "collapse"
              :outline "solid 1px #ccc"}
        (thead
         (tr
          :css {:background "linear-gradient(to bottom, white, white 33%, #eee 66%)"}
          (formula-of [columns*]
            (for [column columns*]
              (td (:name column))))
          #_(for-tpl [column columns*]
              (td (-> column :name cell=)))))
        (formula-of [data* columns*]
          (for [item data*]
            (let [expanded? (cell true)
                  subitems (detail item)
                  spacer-width "12px"
                  spacer-margin "3px"
                  spacing {:width spacer-width
                           :margin-right spacer-margin
                           :display "inline-block"}
                  spacer (fn [] (span :css spacing))]
              [(tbody
                (tr
                 (td*
                  :colspan (count columns*)
                  (if (-> subitems count zero?)
                    (spacer)
                    (div :css {:width spacer-width
                               :margin-right spacer-margin
                               :display "inline-block"
                               :text-align "center"
                               :vertical-align "middle"
                               :height "12px"
                               :border-radius "50%"
                               :border-color "black"
                               :border-style "solid"
                               :border-width "1px"
                               :text-anchor "6px"
                               :font-size "120%"
                               :line-height "12px"
                               :font-weight "bold"}
                         :click #(swap! expanded? not)
                         (if-tpl expanded? "-" "+")))
                  (formatter item))))
               (tbody
                (for [subitem subitems]
                  (tr
                   :fade-toggle expanded?
                   (let [formatter (:formatter (first columns*))]
                     (td*
                      (spacer)
                      (spacer)
                      (formatter subitem)))
                   (for [column (drop 1 columns*)]
                     (td*
                      (let [formatter (:formatter column)]
                        (formatter subitem)))))))])))
        #_(for-tpl [item data*]
            (let [expanded? (cell true)
                  subitems (formula-of [item] (detail item))]
              [(tbody
                (tr
                 (for-tpl [column columns*]
                   [(if-tpl (-> subitems count zero? cell=)
                      ""
                      (button :click #(swap! expanded? not) (if-tpl expanded? "-" "+")))
                    (cell=
                     (td* (formula-of [column item]
                            (let [fmt (:formatter column)]
                              (fmt :master item)))))])))
               (tbody
                :fade-toggle expanded?
                (for-tpl [subitem subitems]
                  (tr
                   (for-tpl [column columns*]
                     (td* (formula-of [column subitem]
                            (let [fmt (:formatter column)]
                              (fmt :detail subitem))))))))])))))))

(defn- sorters
  "Returns UI for the sort handles at the top of a column.
  `sort-state` is a tuple of column index and direction for the
  current sort. `column-id` is a cell the index of the column we're
  rendering."
  [sort-criteria column-id]
  (inl
   :class "sorters"
   (svg/svg
    :width "20px"
    :viewBox "-100 -100 200 200"
    (comm/triangle :transform "rotate(180) translate(0 -50)"
                   :r 50
                   :stroke "black"
                   :stroke-width "2"
                   :fill (formula-of [sort-criteria column-id]
                           (if (= sort-criteria [column-id :ascending])
                             "black"
                             "none")))
    (comm/triangle :transform "translate(0 -50)"
                   :r 50
                   :stroke "black"
                   :stroke-width "2"
                   :fill (formula-of [sort-criteria column-id]
                           (if (= sort-criteria [column-id :descending])
                                  "black"
                                  "none"))))))

(defn- nudge
  "Moves item `item` in `coll` by one position earlier or later,
  depending on the sign of `n`.

  E.g. `(reorder [:a :b :c] :b -1)` returns `[:b :a :c]`"
  [coll item n]
  (->> coll
       (map-indexed (fn [i v]
                      [(if (= v item)
                         (if (pos? n)
                           (+ i 1.5)
                           (- i 1.5))
                         i)
                       v]))
       (sort-by first)
       (mapv second)))

;; TODO: Don't bonk when row-attrs is nil
(defelem table-grid
  "Returns a table grid control. Attributes

:data - A cell containing a seq of maps
:row-attrs - A function from a cell containing a row to an Hoplon
             attrs map
:when-empty - UI to show when there are no rows in the data
:columns - A map from column IDs to column descriptions
:fixed-columns - A seq of IDs of unmovable columns
:movable-columns - [cell] A seq of IDs of movable columns
:hidden-columns - [cell] A set of IDs of hidden columns

Column description:
:title - Shows up in the header row
:sort-key - Function from a row value (not cell) to a sort value
:formatter - Function from a row cell and row number cell to UI
:css - Function from cell of row item and cell of row number to
         map of css values."
  [attrs _]
  (register-styles!
   ::table-grid
   [[:table.table-grid {:border-collapse "collapse"}
     [:tr.table-grid-row (css-sel/& (css-sel/nth-child :even))
      {:background "#eee"}]
     [:tr.table-grid-row (css-sel/& (css-sel/nth-child :odd))
      {:background "white"}]
     [:thead.table-grid-header
      [:td.table-grid-cell {:background  "#ddd"
                            :border      "solid 1px grey"
                            :padding     "2px 4px 4px 4px"
                            :white-space "nowrap"}]
      [:.sorters { ;; :float "right"
                  :display "inline-block"
                  }
       [:svg {:vertical-align "bottom"}]]]
     [:tbody.table-grid-body
      [:td.table-grid-cell
       {:padding "2px 3px 2px 3px"
        :border  "solid 1px lightgray"
        ;;:border-bottom "none"
        ;;:border-top "none"
        }]]]
    [:td.table-grid-cell.hidden-column {:display "none"}]])
  (with-attr-bindings attrs [data columns row-attrs when-empty fixed-columns movable-columns hidden-columns]
    (let [sort-criteria    (cell nil)
          sorted-items     (formula-of [data sort-criteria]
                             (if-not sort-criteria
                               data
                               (let [[sort-column sort-direction] sort-criteria
                                     {:keys [sort-key]}           (get columns sort-column)]
                                 (sort-by sort-key
                                          (if (= sort-direction :ascending)
                                            compare
                                            (fn [a b]
                                              (- (compare a b))))
                                          data))))
          num-columns      (count columns)
          column-order     (formula-of [fixed-columns movable-columns]
                             (into (vec fixed-columns)
                                   (vec movable-columns)))
          editing-columns? (cell false)
          classify (fn [elem class]
                     (fn [& args]
                       (apply elem :class class args)))
          table (classify hoplon.core/table "table-grid")
          thead (classify hoplon.core/thead "table-grid-header")
          tbody (classify hoplon.core/tbody "table-grid-body")
          tr    (classify hoplon.core/tr "table-grid-row")
          td    (classify hoplon.core/td "table-grid-cell")]
      (table
       (thead
        (tr
         (td
          :class "table-grid-cell"
          :colspan num-columns
          (buttons/image-button
           :src "images/settings.svg"
           :width (px 16)
           :title "Settings"
           :latched? editing-columns?
           :click #(swap! editing-columns? not))
          (help/help-icon [:table-grid :settings])))
        (tr (for [n (range num-columns)]
              (let [column-id    (formula-of [column-order]
                                   (nth column-order n))
                    hidden?      (formula-of [hidden-columns column-id]
                                   (hidden-columns column-id))
                    disable-sort (formula-of [column-id]
                                   (-> columns (get column-id) :disable-sort))]
                (td
                 :class (formula-of [editing-columns? hidden?]
                          {:hidden-column (and (not editing-columns?) hidden?)
                           :table-grid-cell true})
                 :click #(when-not @disable-sort
                           (swap! sort-criteria
                                  (fn [[old-index direction]]
                                    [@column-id
                                     (cond
                                       (not= old-index @column-id)
                                       :ascending

                                       (= direction :ascending)
                                       :descending

                                       :else
                                       :ascending)]))
                           true)
                 (when-tpl (cell= (and (< (count fixed-columns) n)
                                       editing-columns?))
                   (buttons/a-button
                    :title "Move column left"
                    :css {:background "linear-gradient(white, white 50%, rgb(195, 194, 194) 100%)"}
                    :click #(do
                              (swap! movable-columns nudge @column-id -1)
                              false)
                    "<"))
                 (when-tpl editing-columns?
                   (inl
                    :css (formula-of [hidden?]
                           {:border-width  (px 2)
                            :border-style  "inset"
                            :border-radius (px 3)
                            :color         (if hidden? "red" "green")
                            :background    "white"
                            :line-height   "90%"
                            :padding       (px 1)
                            :cursor        "pointer"
                            :width         (px 10)})
                    :click (fn [_]
                             (swap! hidden-columns (if @hidden? disj conj) @column-id)
                             false)
                    (if-tpl hidden? "✗" "✓")))
                 (formula-of [column-id]
                   (-> columns (get column-id) :title))
                 (if-tpl disable-sort
                   []
                   (sorters sort-criteria column-id))
                 (when-tpl (cell= (and (<= (count fixed-columns) n)
                                       (< n (dec num-columns))
                                       editing-columns?))
                   (buttons/a-button
                    :title "Move column right"
                    :css {:background "linear-gradient(white, white 50%, rgb(195, 194, 194) 100%)"}
                    :click #(do
                              (swap! movable-columns nudge @column-id 1)
                              false)
                    ">")))))))
       (if-tpl (-> sorted-items count zero? (and when-empty) cell=)
         (tbody
          (tr
           (td
            :colspan num-columns
            when-empty)))
         (tbody
          (for-tpl [indexed (cell= (map-indexed vector sorted-items))]
            (cell-let [[rownum item] indexed]
              (tr
               (row-attrs item rownum)
               ;; Here we have a map with functions that produce
               ;; cells, and we sort of invert it into cells that
               ;; contain maps from the keys to the value resulting
               ;; from calling the function. Hard to explain, and so
               ;; probably not a very good way to do this.
               (let [formatteds (apply
                                 (javelin.core/formula
                                  (fn [& formatteds]
                                    (zipmap (map key columns)
                                            formatteds)))
                                 (->> columns
                                      vals
                                      (map :formatter)
                                      (map (fn [formatter]
                                             (formatter item)))))
                     csses      (apply
                                 (javelin.core/formula
                                  (fn [& csses]
                                    (zipmap (map key columns)
                                            csses)))
                                 (->> columns
                                      vals
                                      (map :css)
                                      (map (fn [css]
                                             (when css
                                               (css item rownum))))))]
                 (for [n (range num-columns)]
                   (let [column-id (formula-of [column-order]
                                     (nth column-order n))
                         hidden?   (formula-of [hidden-columns column-id]
                                     (hidden-columns column-id))]
                     (with-let [^js/HTMLTableDataCellElement c (td)]
                       (c :class (formula-of [editing-columns? hidden?]
                                   {:hidden-column (and (not editing-columns?) hidden?)
                                    :table-grid-cell true})
                          :css (formula-of [column-id csses]
                                 ;; This is horrible, but it appears
                                 ;; that Hoplon won't actually remove
                                 ;; the style attribute it's already
                                 ;; set if the value is nil. Or an
                                 ;; empty map, or anything else I
                                 ;; could figure out to provide.
                                 (or (get csses column-id)
                                     (do
                                       (.removeAttribute c "style")
                                       nil))
                                 (get csses column-id))
                          (formula-of [column-id formatteds]
                            (get formatteds column-id))))))))))))))))
