fix: finish removing table v2 from shui dep

Followup to 82a1a18762.
Copied more useful intro from shui-graph into README
This commit is contained in:
Gabriel Horner
2024-05-15 10:08:47 -04:00
parent a70499bd8c
commit ad9dc70b9c
18 changed files with 6 additions and 1505 deletions

1
.gitignore vendored
View File

@@ -62,5 +62,4 @@ packages/ui/.storybook/cljs
deps/shui/.lsp
deps/shui/.lsp-cache
deps/shui/.clj-kondo
deps/shui/shui-graph/logseq/bak
tx-log*

30
deps/shui/README.md vendored
View File

@@ -1,29 +1,11 @@
## Description
This library provides a set of UI components for use within logseq.
Shui is the component library for logseq. It has 3 main goals:
1. Provide an abstraction for specific components, separate from the main codebase
2. Provide a consistent look and feel for the future of logseq
3. Provide ready to use components to plugin authors to allow for a more consistent better user experience of plugin authors and users
## API
This library is under the parent namespace `logseq.shui`. This library provides
several namespaces, all of which will be versioned, with the exception of `logseq.shui.context`.
An example of a versioned namespace is the table namespace:
`logseq.shui.table.v2`
`root` components are exported from each versioned file to indicate the root component to be rendered:
`logseq.shui.table.v2/root`
Each root component should expect two arguments, `props` and `context`.
## `props`
Ultimately, components in shui will need to be used by JavaScript. While it is idiomatic in clojure to
use a list of properties, it is idiomatic in react to use a single props map. Shui components should therefore
stick to this convention when possible to ease the conversion between the two languages.
## `context`
Context is a set of functions that call back to the main application. These are abstracted out into a context
object to make it clear what is used internally, and what is used externally.
This library is under the parent namespace `logseq.shui`.

View File

@@ -1,2 +0,0 @@
-
-

View File

@@ -1,341 +0,0 @@
{:meta/version 1
;; Currently, we support either "Markdown" or "Org".
;; This can overwrite your global preference so that
;; maybe your personal preferred format is Org but you'd
;; need to use Markdown for some projects.
;; :preferred-format ""
;; Preferred workflow style.
;; Value is either ":now" for NOW/LATER style,
;; or ":todo" for TODO/DOING style.
:preferred-workflow :now
;; The app will ignore those directories or files.
;; E.g. :hidden ["/archived" "/test.md" "../assets/archived"]
:hidden []
;; When creating the new journal page, the app will use your template if there is one.
;; You only need to input your template name here.
:default-templates
{:journals ""}
;; Set a custom date format for journal page title
;; Example:
;; :journal/page-title-format "EEE, do MMM yyyy"
;; Whether to enable hover on tooltip preview feature
;; Default is true, you can also toggle this via setting page
:ui/enable-tooltip? true
;; Show brackets around page references
;; :ui/show-brackets? true
;; Enable showing the body of blocks when referencing them.
:ui/show-full-blocks? false
;; Enable remove accents when searching.
;; After toggle this option, please remember to rebuild your search index by press (cmd+c cmd+s).
:feature/enable-search-remove-accents? true
;; Enable journals
;; :feature/enable-journals? true
;; Enable flashcards
;; :feature/enable-flashcards? true
;; Enable Whiteboards
;; :feature/enable-whiteboards? true
;; Disable the built-in Scheduled tasks and deadlines query
;; :feature/disable-scheduled-and-deadline-query? true
;; Specify the number of days in the future to display in the
;; scheduled tasks and deadlines query, with a default value of 0 which
;; only displays tasks for today.
;; Example usage:
;; Display all scheduled tasks and deadlines in the next 7 days
;; :scheduled/future-days 7
;; Specify the date on which the week starts.
;; Goes from 0 to 6 (Monday to Sunday), default to 6
:start-of-week 6
;; Specify a custom CSS import
;; This option take precedence over your local `logseq/custom.css` file
;; You may find a list of awesome logseq themes here:
;; https://github.com/logseq/awesome-logseq#css-themes
;; Example:
;; :custom-css-url "@import url('https://cdn.jsdelivr.net/gh/dracula/logseq@master/custom.css');"
;; Specify a custom js import
;; This option take precedence over your local `logseq/custom.js` file
;; :custom-js-url ""
;; Set a custom Arweave gateway
;; Default gateway: https://arweave.net
;; :arweave/gateway ""
;; Set Bullet indentation when exporting
;; default option: tab
;; Possible options are for `:sidebar` are
;; 1. `:eight-spaces` as eight spaces
;; 2. `:four-spaces` as four spaces
;; 3. `:two-spaces` as two spaces
;; :export/bullet-indentation :tab
;; When :all-pages-public? true, export repo would export all pages within that repo.
;; Regardless of whether you've set any page to public or not.
;; Example:
;; :publishing/all-pages-public? true
;; Specify default home page and sidebar status for Logseq
;; If not specified, Logseq default opens journals page on startup
;; value for `:page` is name of page
;; Possible options for `:sidebar` are
;; 1. `"Contents"` to open up `Contents` in sidebar by default
;; 2. `page name` to open up some page in sidebar
;; 3. Or multiple pages in an array ["Contents" "Page A" "Page B"]
;; If `:sidebar` is not set, sidebar will be hidden
;; Example:
;; 1. Setup page "Changelog" as home page and "Contents" in sidebar
;; :default-home {:page "Changelog", :sidebar "Contents"}
;; 2. Setup page "Jun 3rd, 2021" as home page without sidebar
;; :default-home {:page "Jun 3rd, 2021"}
;; 3. Setup page "home" as home page with multiple pages in sidebar
;; :default-home {:page "home" :sidebar ["page a" "page b"]}
:default-home {:page "Contents"}
;; Tell logseq to use a specific folder in the repo as a default location for notes
;; if not specified, notes are stored in `pages` directory
;; :pages-directory "your-directory"
;; Tell logseq to use a specific folder in the repo as a default location for journals
;; if not specified, journals are stored in `journals` directory
;; :journals-directory "your-directory"
;; Set this to true will convert
;; `[[Grant Ideas]]` to `[[file:./grant_ideas.org][Grant Ideas]]` for org-mode
;; For more, see https://github.com/logseq/logseq/issues/672
;; :org-mode/insert-file-link? true
;; Setup custom shortcuts under `:shortcuts` key
;; Syntax:
;; 1. `+` means keys pressing simultaneously. eg: `ctrl+shift+a`
;; 2. ` ` empty space between keys represents key chords. eg: `t s` means press `t` followed by `s`
;; 3. `mod` means `Ctrl` for Windows/Linux and `Command` for Mac
;; 4. use `false` to disable particular shortcut
;; 5. you can define multiple bindings for one action, eg `["ctrl+j" "down"]`
;; full list of configurable shortcuts are available below:
;; https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/config.cljs
;; Example:
;; :shortcuts
;; {:editor/new-block "enter"
;; :editor/new-line "shift+enter"
;; :editor/insert-link "mod+shift+k"
;; :editor/highlight false
;; :ui/toggle-settings "t s"
;; :editor/up ["ctrl+k" "up"]
;; :editor/down ["ctrl+j" "down"]
;; :editor/left ["ctrl+h" "left"]
;; :editor/right ["ctrl+l" "right"]}
:shortcuts {}
;; By default, pressing `Enter` in the document mode will create a new line.
;; Set this to `true` so that it's the same behaviour as the usual outliner mode.
:shortcut/doc-mode-enter-for-new-block? false
;; Block content larger than `block/content-max-length` will not be searchable
;; or editable for performance.
:block/content-max-length 10000
;; Whether to show command doc on hover
:ui/show-command-doc? true
;; Whether to show empty bullets for non-document mode (the default mode)
:ui/show-empty-bullets? false
;; Pre-defined :view function to use with advanced queries
:query/views
{:pprint
(fn [r] [:pre.code (pprint r)])}
;; Pre-defined :result-transform function for use with advanced queries
:query/result-transforms
{:sort-by-priority
(fn [result] (sort-by (fn [h] (get h :block/priority "Z")) result))}
;; The app will show those queries in today's journal page,
;; the "NOW" query asks the tasks which need to be finished "now",
;; the "NEXT" query asks the future tasks.
:default-queries
{:journals
[{:title "🔨 NOW"
:query [:find (pull ?h [*])
:in $ ?start ?today
:where
[?h :block/marker ?marker]
[(contains? #{"NOW" "DOING"} ?marker)]
[?h :block/page ?p]
[?p :block/journal-day ?d]
[(>= ?d ?start)]
[(<= ?d ?today)]]
:inputs [:14d :today]
:result-transform (fn [result]
(sort-by (fn [h]
(get h :block/priority "Z")) result))
:collapsed? false}
{:title "📅 NEXT"
:query [:find (pull ?h [*])
:in $ ?start ?next
:where
[?h :block/marker ?marker]
[(contains? #{"NOW" "LATER" "TODO"} ?marker)]
[?h :block/page ?p]
[?p :block/journal-day ?d]
[(> ?d ?start)]
[(< ?d ?next)]]
:inputs [:today :7d-after]
:collapsed? false}]}
;; Add your own commands to slash menu to speedup.
;; E.g.
;; :commands
;; [
;; ["js" "Javascript"]
;; ["md" "Markdown"]
;; ]
:commands
[]
;; By default, a block can only be collapsed if it has some children.
;; `:outliner/block-title-collapse-enabled? true` enables a block with a title
;; (multiple lines) can be collapsed too. For example:
;; - block title
;; block content
:outliner/block-title-collapse-enabled? false
;; Macros replace texts and will make you more productive.
;; For example:
;; Change the :macros value below to:
;; {"poem" "Rose is $1, violet's $2. Life's ordered: Org assists you."}
;; input "{{poem red,blue}}"
;; becomes
;; Rose is red, violet's blue. Life's ordered: Org assists you.
:macros {}
;; The default level to be opened for the linked references.
;; For example, if we have some example blocks like this:
;; - a [[page]] (level 1)
;; - b (level 2)
;; - c (level 3)
;; - d (level 4)
;;
;; With the default value of level 2, `b` will be collapsed.
;; If we set the level's value to 3, `b` will be opened and `c` will be collapsed.
:ref/default-open-blocks-level 2
:ref/linked-references-collapsed-threshold 50
;; Favorites to list on the left sidebar
:favorites []
;; any number between 0 and 1 (the greater it is the faster the changes of the next-interval of card reviews) (default 0.5)
;; :srs/learning-fraction 0.5
;; the initial interval after the first successful review of a card (default 4)
;; :srs/initial-interval 4
;; hide specific properties for blocks
;; E.g. :block-hidden-properties #{:created-at :updated-at}
;; :block-hidden-properties #{}
;; Enable all your properties to have corresponding pages
:property-pages/enabled? true
;; Properties to exclude from having property pages
;; E.g.:property-pages/excludelist #{:duration :author}
;; :property-pages/excludelist
;; By default, property value separated by commas will not be treated as
;; page references. You can add properties to enable it.
;; E.g. :property/separated-by-commas #{:alias :tags}
;; :property/separated-by-commas #{}
;; Properties that are ignored when parsing property values for references
;; :ignored-page-references-keywords #{"author" "startup"}
;; logbook setup
;; :logbook/settings
;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated
;; :enabled-in-all-blocks true ;display logbook in all blocks after timetracking
;; :enabled-in-timestamped-blocks false ;don't display logbook at all
;; }
;; Mobile photo uploading setup
;; :mobile/photo
;; {:allow-editing? true
;; :quality 80}
;; Mobile features options
;; Gestures
;; :mobile
;; {:gestures/disabled-in-block-with-tags ["kanban"]}
;; Extra CodeMirror options
;; See https://codemirror.net/5/doc/manual.html#config for possible options
;; :editor/extra-codemirror-options {:keyMap "emacs" :lineWrapping true}
;; Enable logical outdenting
;; :editor/logical-outdenting? true
;; When both text and a file are in the clipboard, paste the file
;; :editor/preferred-pasting-file? true
;; Quick capture templates for receiving contents from other apps.
;; Each template contains three elements {time}, {text} and {url}, which can be auto-expanded
;; by received contents from other apps. Note: the {} cannot be omitted.
;; - {time}: capture time
;; - {date}: capture date using current date format, use `[[{date}]]` to get a page reference
;; - {text}: text that users selected before sharing.
;; - {url}: url or assets path for media files stored in Logseq.
;; You can also reorder them, or even only use one or two of them in the template.
;; You can also insert or format any text in the template as shown in the following examples.
;; :quick-capture-templates
;; {:text "[[quick capture]] **{time}**: {text} from {url}"
;; :media "[[quick capture]] **{time}**: {url}"}
;; Quick capture options
;; :quick-capture-options {:insert-today? false :redirect-page? false :default-page nil}
;; File sync options
;; Ignore these files when syncing, regexp is supported.
;; :file-sync/ignore-files []
;; dwim (do what I mean) for Enter key when editing.
;; Context-awareness of Enter key makes editing more easily
; :dwim/settings {
; :admonition&src? true
; :markup? false
; :block-ref? true
; :page-ref? true
; :properties? true
; :list? true
; }
;; Decide the way to escape the special characters in the page title.
;; Warning:
;; This is a dangerous operation. If you want to change the setting,
;; should access the setting `Filename format` and follow the instructions.
;; Or you have to rename all the affected files manually then re-index on all
;; clients after the files are synced. Wrong handling may cause page titles
;; containing special characters to be messy.
;; Available values:
;; :file/name-format :triple-lowbar
;; ;use triple underscore `___` for slash `/` in page title
;; ;use Percent-encoding for other invalid characters
:file/name-format :triple-lowbar
:feature/enable-whiteboards? true}
;; specify the format of the filename for journal files
;; :journal/file-name-format "yyyy_MM_dd"

View File

@@ -1,22 +0,0 @@
- ## What is shui?
- Shui is the component library for logseq. It has 3 main goals:
- 1. Provide an abstraction for specific components, separate from the main codebase
2. Provide a consistent look and feel for the future of logseq
3. Provide ready to use components to plugin authors to allow for a more consistent better user experience of plugin authors and users
-
- ## What are the general concepts of shui?
- Shui has a few core principles:
- ### Focus on a native core experience
- We want to provide a smooth, consistent, and native feel for all logseq features, first and foremost
- ### Specific output, general input
- Components should be generally reusable by their props, however should have the user experience themselves
- Eventually, getting to a highly composable components is a great goal, but we should start small and focused first
- ### UI is a marathon, not a sprint
- Components in shui should be versioned, and should expect to evolve over time
- We need to go from highly coupled, low reused components to a loosely coupled, highly reusable library. This will take time, and means components have to be adaptable over time
- Versioning is at the core of shui
-
- ## How to contribute to shui?
- In the logseq repo, there is a directory at `deps/shui`. Here you can find all of the shui components
- In the logseq repo, you can find a copy of this graph at `deps/shui/shui-graph`. Here you can find and add all the test cases needed for different `shui` components
- In the logseq repo, you can find tests under the `e2e-tests/shui`. To keep our infra streamlined, `shui` is bundled with and tested with the current CI for logseq

View File

@@ -1,3 +0,0 @@
table-example:: true
-

View File

@@ -1 +0,0 @@
table-example:: true

View File

@@ -1 +0,0 @@
table-example:: true

View File

@@ -1,208 +0,0 @@
- [[About Shui]]
- [[shui/components]] if there was text here
- beta
- [[shui/components/table]]
- up next
- [[shui/components/button]]
- [[shui/components/input]]
- [[shui/components/tooltip]]
- [[shui/components/text]]
- future
- [[shui/components/icon]]
- [[shui/components/tag]]
- [[shui/components/toggle]]
- [[shui/components/context-menu]]
- [[shui/components/right-sidebar]]
- [[shui/components/modal]]
- [[shui/components/properties]]
- [[shui/components/code]]
collapsed:: true
- ```css
:root {
--lx-blue-1: #123456;
}
```
- ```clojurescript
(js/document.style.setProperty "--lx-blue-1" ""#abcdef")
```
- ```python
# This is a single-line comment
"""
This is a
multi-line comment (docstring)
"""
# Import statement
import math
# Constant
CONSTANT = 3.14159
# Function definition, decorators and function call
@staticmethod
def add_numbers(x, y):
"""This function adds two numbers"""
return x + y
result = add_numbers(5, 7)
# Built-in functions
print(f"Sum is: {result}")
# Class definition and object creation
class MyClass:
# Class variable
class_var = "I'm a class variable"
def __init__(self, instance_var):
# Instance variable
self.instance_var = instance_var
def method(self):
return self.instance_var
# Creating object of the class
obj = MyClass("I'm an instance variable")
print(obj.method())
# Control flow - if, elif, else
num = 10
if num > 0:
print("Positive number")
elif num == 0:
print("Zero")
else:
print("Negative number")
# For loop and range function
for i in range(5):
print(i)
# List comprehension
squares = [x**2 for x in range(10)]
# Generator expression
gen = (x**2 for x in range(10))
# While loop
count = 0
while count < 5:
print(count)
count += 1
# Exception handling
try:
# Division by zero
x = 1 / 0
except ZeroDivisionError as e:
print("Handling run-time error:", e)
# Lambda function
double = lambda x: x * 2
print(double(5))
# File I/O
with open('test.txt', 'r') as file:
content = file.read()
# Assert
assert num > 0, "Number is not positive"
```
- ```clojure
;; This is a comment
;; Numbers
42
2.71828
;; Strings
"Hello, world!"
;; Characters
\a
;; Booleans
true
false
;; Lists
'(1 2 3 4 5)
;; Vectors
[1 2 3 4 5]
;; Maps
{:name "John Doe" :age 30 :email "john.doe@example.com"}
;; Sets
#{1 2 3 4 5}
;; Functions
(defn add-numbers [x y]
"This function adds two numbers."
(+ x y))
(def result (add-numbers 5 7))
(println "Sum is: " result)
;; Anonymous function
(#(+ %1 %2) 5 7)
;; Conditionals
(if (> result 0)
(println "Positive number")
(println "Zero or negative number"))
;; Loops
(loop [x 0]
(when (< x 5)
(println x)
(recur (+ x 1))))
;; For
(for [x (range 5)] (println x))
;; Map over a list
(map inc '(1 2 3))
;; Exception handling
(try
(/ 1 0)
(catch ArithmeticException e
(println "Caught an exception: " (.getMessage e))))
;; Macros
(defmacro unless [pred a b]
`(if (not ~pred) ~a ~b))
(unless true
(println "This will not print")
(println "This will print"))
;; Keywords
:foo
:bar/baz
```
- ```css
.example {
something: "#abc123"
}
```
- [[shui/colors]]
- We want to switch to radix variables
- We want to make it easy to customize with themes
- We want to support as much old themes as possible
- var(--ui-button-color,
collapsed:: true
- var(--logseq-button-primary-color,
collapsed:: true
- var(--lx-color-6)))
- light and dark variants
- [[shui/inline]]
-
- /
-
-

View File

@@ -1,4 +0,0 @@
- Below is a list of components that can be found in the shui library
- [[shui/components/table]]
- The table component is used to render tabular data.
-

View File

@@ -1,2 +0,0 @@
-
-

View File

@@ -1 +0,0 @@
- support hidden properties

View File

@@ -1,62 +0,0 @@
- ### Props
- logseq.table.version:: 2
logseq.table.hover:: row
logseq.table.stripes:: true
logseq.table.borders:: false
| Prop Name | Description | Values |
| --- | --- | --- |
| `logseq.table.version` | The version of the table | 1, 2 |
| `logseq.table.hover` | The hover effect of the table | cell (default), row, col, both, none |
| `logseq.table.compact` | Whether to show a compact version of the data | false (default), true |
| `logseq.table.headers` | The casing that should be applied to the header cols | none (default), uppercase, capitalize, capitalize-first, lowercase |
| `logseq.table.borders` | Whether or not the table should have borders between all cells and rows | true (default), false |
| `logseq.table.stripes` | Whether or not the table should have alternately colored table rows | false (default), true |
| `logseq.table.max-width` | The maximum width (in rems) that should be applied to each column | <any number> (default 30) |
| `logseq.color` | The color accent of the table | red, orange, yellow, green, blue, purple |
- ### Examples
- #### Simplest possible markdown table
collapsed:: true
- logseq.table.version:: 1
| Fruit | Color |
| Apples | Red |
| Bananas | Yellow |
- #### Longer more complicated markdown table, with various widths and input types
collapsed:: true
- logseq.table.version:: 2
| Length | Text | EN | ZH |
| --- | --- | --- | --- |
| 70 | Logseq is a new note-taking app that has been making waves in the productivity community. | x | |
| 138 | With its unique approach to linking and organizing information, Logseq allows users to create a highly interconnected and personalized knowledge base. | x | |
| 194 | Unlike traditional note-taking apps, Logseq encourages users to embrace the power of plain text and markdown formatting, enabling them to easily manipulate and query their notes. | x | |
| 246 | From students to researchers, Logseq's flexible and intuitive interface makes it an ideal tool for anyone looking to optimize their note-taking and knowledge management workflow. | x | |
| 312 | Whether you're looking to organize your thoughts, collaborate with others, or simply streamline your note-taking process, Logseq offers a revolutionary approach that is sure to revolutionize the way you work. | x | |
| 35 | Logseq 是一款在生产力社群中备受瞩目的新型笔记应用。| | x |
| 59 | Logseq 以其独特的链接和组织信息方式,使用户能够创建高度互联且个性化的知识库。 | | x | 86 | 不同于传统笔记应用Logseq 鼓励用户采用纯文本和 Markdown 格式,使其能够轻松地操作和查询笔记。 | | x |
| 123 | 从学生到研究人员Logseq 灵活直观的界面使其成为任何想要优化笔记和知识管理工作流程的人的理想工具。| | x |
| 152 | 无论您是想整理自己的思路、与他人合作还是简化笔记流程Logseq 提供的革命性方法肯定会改变您的工作方式。| | x |
- #### Query table for blocks
- logseq.table.version:: 2
query-table:: true
query-properties:: [:block]
logseq.table.borders:: false
{{query #table-example/block}}
-
- #### data
- Block 1 #table-example/block
table-example:: true
- Block 2 #table-example/block
table-example:: true
- Block 3 #table-example/block
table-example:: true
- #### Query table for pages
- {{query (page-property "table-example" "true")}}
logseq.table.version:: 2
- [[Page 1]]
- [[Page 2]]
- [[Page 3]]
- #### Query table for mixed pages and blocks
- {{query (property "table-example" true)}}
query-table:: true
logseq.table.version:: 2
-
- {{query }}

View File

@@ -1 +0,0 @@
-

View File

@@ -1,473 +0,0 @@
(ns logseq.shui.table.v2
(:require
[clojure.string :as str]
[logseq.shui.util :refer [use-ref-bounding-client-rect use-dom-bounding-client-rect $main-content] :as util]
[rum.core :as rum]))
(declare table-cell)
(def COLORS #{"tomato" "red" "crimson" "pink" "plum" "purple" "violet" "indigo" "blue" "sky" "cyan" "teal" "mint" "green" "grass" "lime" "yellow" "amber" "orange" "brown"})
(def MAX_WIDTH 30 #_rem) ;; Max width in rem for a single column
(def MIN_WIDTH 4 #_rem) ;; Min width in rem for a single column
;; in order to make sure the tailwind classes are included,
;; the values are pulled from the classes via regex.
;; the return values are simply the numbers in the classes.
(def CELL_PADDING (->> "px-[0.75rem]" (re-find #"\d+\.?\d*") js/parseFloat))
(def CELL_PADDING_COMPACT (->> "px-[0.25rem]" (re-find #"\d+\.?\d*") js/parseFloat))
(def BORDER_WIDTH (->> "border-[1px]" (re-find #"\d+\.?\d*") js/parseFloat))
;; -- Helpers ------------------------------------------------------------------
(defn get-in-first
([obj path] (get-in obj path))
([obj path & more] (get-in obj path (apply get-in-first obj more))))
(defn get-in-first-fallback
([obj path] (get-in obj path))
([obj path fallback] (get-in obj path fallback))
([obj path path-b & more] (get-in obj path (apply get-in-first-fallback obj path-b more))))
(defn read-prop [value]
(case value
"false" false
"true" true
value))
(defn get-view-prop
"Get the config for a specified item. Can be overridden in blocks, specified in config,
fallback to default config, or fallback to the provided parameters"
([context kw]
(read-prop
(get-in-first context [:block :properties kw]
[:block :block/properties kw]
[:config kw])))
([context kw fallback]
(read-prop
(get-in-first-fallback context [:block :properties kw]
[:block :block/properties kw]
[:config kw]
fallback))))
(defn color->gray [color]
(case color
("tomato" "red" "crimson" "pink" "plum" "purple" "violet") "mauve"
("indigo" "blue" "sky" "cyan") "slate"
("teal" "mint" "green") "sage"
("grass" "lime") "olive"
("yellow" "amber" "orange" "brown") "sand"
nil))
(defn rdx
([color step] (str "bg-" color "-" step))
([param color step] (str (name param) "-" color "-" step)))
; ([color step] (str "bg-" color "dark-" step))
; ([param color step] (str param "-" color "dark-" step))))
; --ls-primary-background-color: #fff;
; --ls-secondary-background-color: #f8f8f8;
; --ls-tertiary-background-color: #f2f2f3;
; --ls-quaternary-background-color: #ebeaea));
(defn lsx
"This is a temporary bridge between the radix color grading and the
current logseq theming variables. Should set the prop to the given css variable"
([step] (lsx :bg step))
([param step]
(case step
1 ({"bg" "bg-[color:var(--ls-primary-background-color)]"} (name param))
2 ({"bg" "bg-[color:var(--ls-secondary-background-color)]"} (name param))
3 ({"bg" "bg-[color:var(--ls-tertiary-background-color)]"} (name param))
4 ({"bg" "bg-[color:var(--ls-quaternary-background-color)]"} (name param))
5 ({"bg" "bg-[color:var(--ls-quinary-background-color)]"} (name param))
6 ({"bg" "bg-[color:var(--ls-senary-background-color)]"} (name param))
7 ({"bg" "bg-[color:var(--ls-border-color)]"
"border" "border-[color:var(--ls-border-color)]"} (name param))
11 ({"text" "text-[color:var(--ls-secondary-text-color)]"} (name param))
12 ({"text" "text-[color:var(--ls-primary-text-color)]"} (name param)))))
(defn varc [color step]
(str "var(--color-" color "-" step ")"))
(defn last-str
"Given an inline AST, return the last string element you can walk to"
[inline]
(cond
(keyword? inline) (name inline)
(string? inline) inline
(coll? inline) (last-str (last inline))
:else (pr-str inline)))
(comment
(last-str "A")
(last-str ["Plain" "A"])
(last-str [["Plain" "A"]])
(last-str [["Plain" "A"]
[["Emphasis" [["Italic"] [["Plain" "B"]]]]]]))
(defn render-cell-in-context
"Some instances of the table provide us with raw data, others provide us with
inline ASTs. This function renders the content appropriately, passing the AST along
to map-inline if necessary."
[{:keys [map-inline-block int->local-time-2]} cell-data]
(cond
(sequential? cell-data) (map-inline-block [:table :v2] cell-data)
(string? cell-data) cell-data
(keyword? cell-data) (name cell-data)
(boolean? cell-data) (pr-str cell-data)
(number? cell-data) (if-let [date (int->local-time-2 cell-data)]
date cell-data)))
(defn map-with-all-indices [data]
(let [!row-index (volatile! -1)]
(for [[group-index group] (map-indexed vector data)
[group-row-index row] (map-indexed vector group)
:let [row-index (vswap! !row-index inc)]]
[group-index group-row-index row-index group row])))
(defn get-columns [block data]
(->> (or (some-> (get-in block [:block/properties :logseq.table.cols])
(str/split #", ?"))
(map last-str (ffirst data)))
(map (comp str/lower-case str/trim))))
(defn cell-bg-classes
"We track the cell the cursor last entered and update the cells according to the configured
hover preference: cell, row, col, both, or none.
We also have to account for the header cells and stripes cells"
[{:keys [row-index col-index hover header? gray color stripes? cursor]}]
(let [;; check how the cursor position overlaps with the current cell
row-highlighted? (= row-index (second cursor))
col-highlighted? (= col-index (first cursor))
cell-highlighted? (and row-highlighted? col-highlighted?)
;; check how the cell needs to be highlighted
highlight-row? (and row-highlighted? (#{"row" "both"} hover))
highlight-col? (and col-highlighted? (#{"col" "both"} hover))
highlight-cell? (and cell-highlighted? (#{"cell" "row" "col" "both"} hover))]
(cond
highlight-cell? (if header? (lsx 6) (lsx 4))
highlight-row? (if header? (lsx 5) (lsx 3))
highlight-col? (if header? (lsx 5) (lsx 3))
header? (lsx 4)
(and stripes? (even? row-index)) (lsx 2)
:else (lsx 1))))
(defn cell-rounded-classes
"Depending on where the cell is, and whether there is a gradient accent, we need to round specific corners
The cond-> is used to account for single row or single column talbes that may have multiple rounded corners."
[{:keys [color row-index col-index total-rows total-cols]}]
(let [no-gradient-accent? (nil? color)]
(cond-> ""
(and no-gradient-accent? (= [row-index col-index] [0 0])) (str " rounded-tl")
(and no-gradient-accent? (= [row-index col-index] [0 (dec total-cols)])) (str " rounded-tr")
(= [row-index col-index] [(dec total-rows) 0]) (str " rounded-bl")
(= [row-index col-index] [(dec total-rows) (dec total-cols)]) (str " rounded-br"))))
(defn cell-text-transform-classes [{:keys [headers header?]}]
(when header?
(cond-> (get #{"uppercase" "capitalize" "lowercase" "none" "capitalize-first"} headers "none")
(= headers "capitalize-first") (str " lowercase"))))
(defn cell-padding-classes [{:keys [compact? header?]}]
(cond
#_compact_th (and compact? header?) (str "px-[" CELL_PADDING_COMPACT "rem] py-0.5")
#_compact_td compact? (str "px-[" CELL_PADDING_COMPACT "rem] py-0.5")
#_padded_th header? (str "px-[" CELL_PADDING "rem] py-1.5")
#_padded_td :else (str "px-[" CELL_PADDING "rem] py-2")))
(defn cell-text-classes [{:keys [header?]}]
(if header?
(str (lsx :text 11) " text-sm tracking-wide font-bold")
(str (lsx :text 12) " text-base")))
(defn cell-classes [table-opts]
(str/join " "
[(cell-bg-classes table-opts)
(cell-rounded-classes table-opts)
(cell-text-classes table-opts)
(cell-text-transform-classes table-opts)
(cell-padding-classes table-opts)]))
;; -- Handlers -----------------------------------------------------------------
(defn handle-cell-pointer-down [e {:keys [cell-focus col-index row-index]}]
(when (not= cell-focus [col-index row-index])
(.stopPropagation e)
(.preventDefault e)))
(defn handle-cell-click
"When a cell is clicked, we need to update the cursor position and the selected cells"
[e {:keys [cell-focus set-cell-focus header? col-index row-index]} cell-ref]
; (.stopPropagation e)
(.preventDefault e)
(when-not (= cell-focus [col-index row-index])
(set-cell-focus [col-index row-index])))
(defn handle-cell-keydown
"When a cell is focused, we need to update the cursor position and the selected cells"
[e {:keys [cell-focus set-cell-focus header? col-index row-index total-rows total-cols]}]
(when (= cell-focus [col-index row-index])
(and (case (.-key e)
"ArrowUp" (if (= row-index 0)
(set-cell-focus [col-index row-index])
(set-cell-focus [col-index (dec row-index)]))
"ArrowDown" (if (= row-index (dec total-rows))
(set-cell-focus [col-index row-index])
(set-cell-focus [col-index (inc row-index)]))
"ArrowLeft" (cond
;; if we are in the top left, then do not move the focus
(and (= col-index 0) (= row-index 0))
(set-cell-focus [col-index row-index])
;; if we are in the first column, then move to the last column of the previous row
(= col-index 0)
(set-cell-focus [(dec total-cols) (dec row-index)])
;; otherwise, move to the previous column
:else
(set-cell-focus [(dec col-index) row-index]))
"ArrowRight" (cond
;; if we are in the bottom right, then do not move the focus
(and (= col-index (dec total-cols)) (= row-index (dec total-rows)))
(set-cell-focus [col-index row-index])
;; if we are in the last column, then move to the first column of the next row
(= col-index (dec total-cols))
(set-cell-focus [0 (inc row-index)])
;; otherwise, move to the next column
:else
(set-cell-focus [(inc col-index) row-index]))
nil)
;; Prevent default actions when the table handles it itself
(.preventDefault e)
(.stopPropagation e))))
;; -- Hooks --------------------------------------------------------------------
(defn use-atom
"A hook that wraps use-state to allow for interaction with
the state as if it were an atom"
[initial-value]
(let [atom-ref (rum/use-ref (atom initial-value))
atom-current (.. atom-ref -current)
[state set-state] (rum/use-state initial-value)]
(rum/use-effect! (fn []
(set-state @atom-current)
identity)
[atom-current])
[state atom-current]))
(defn use-dynamic-widths [data]
(let [[static atomic] (use-atom {})
add-column-width (fn [col-index width]
(when (< (get @atomic col-index 0) (min MAX_WIDTH width))
(swap! atomic assoc col-index (min MAX_WIDTH width))
;; rum is complaining that we can only return teardown functions
identity))]
;; Reset the minimum widths when the data changes
(rum/use-effect! (fn [] (reset! atomic {}) identity)
[data])
[static add-column-width]))
(defn use-table-flow-at-width [table-px max-cols-px]
(let [[overflow set-overflow] (rum/use-state false)
[underflow set-underflow] (rum/use-state false)
handle-container-width (fn [container-px]
(set-underflow (< max-cols-px container-px))
(set-overflow (< container-px table-px)))]
[overflow underflow handle-container-width]))
;; -- Components (V2) -----------------------------------------------------------
(rum/defc table-scrollable-overflow [handle-root-width-change child]
(let [[set-root-ref root-rect root-ref] (use-ref-bounding-client-rect)
main-content-rect (use-dom-bounding-client-rect ($main-content))
left-adjustment (- (:left root-rect) (:left main-content-rect))
right-adjustment (- (:width main-content-rect)
(- (:right root-rect) (:left main-content-rect)))
;; Because in a scrollable container, we need to account for the scrollbar being clicked,
;; we add a handler to prevent the table from switching to the input on click.
;; This also prevents the table from switching to eiditng mode when the left or right area
;; of the table is clicked, but that feels natural to me.
handle-pointer-down (fn [e]
(when (= root-ref (.. e -target -parentElement))
(.preventDefault e)))]
(rum/use-effect! #(handle-root-width-change (:width root-rect)) [(:width root-rect)])
[:div {:ref set-root-ref}
[:div {:style {:width (:width main-content-rect)
:margin-left (- (:left main-content-rect) (:left root-rect))
:padding-left left-adjustment
:padding-right right-adjustment
:overflow-x "scroll"}
:class "mt-2"
:on-pointer-down handle-pointer-down}
child]]))
(rum/defc table-gradient-accent [{:keys [color color-gradient linear-gradient]}]
[:div.rounded-t.h-2.-ml-px.-mt-px.-mr-px
{:style {:grid-column "1 / -1" :order -999
:background (linear-gradient color :09 color-gradient)}
:data-testid "v2-table-gradient-accent"}])
(rum/defc table-header-row [handle-cell-width-change cells {:keys [cell-col-map] :as opts}]
[:<>
(for [[cell-index cell] (map-indexed vector cells)
:let [col-index (get cell-col-map cell-index)]
:when col-index]
^{:key cell-index}
(table-cell handle-cell-width-change cell (assoc opts :cell-index cell-index :col-index col-index :header? true)))])
(rum/defc table-data-row [handle-cell-width-change cells {:keys [cell-col-map] :as opts}]
[:<>
(for [[cell-index cell] (map-indexed vector cells)
:let [col-index (get cell-col-map cell-index)]
:when col-index]
^{:key cell-index}
(table-cell handle-cell-width-change cell (assoc opts :cell-index cell-index :col-index col-index)))])
(rum/defc table-cell [handle-cell-width-change cell {:keys [row-index col-index render-cell show-separator? total-cols set-cell-hover cell-focus table-underflow?] :as opts}]
(let [cell-ref (rum/use-ref nil)
cell-order (+ (* row-index total-cols) col-index)
static-width (get-in opts [:static-widths col-index])
dynamic-width (when-not static-width
(get-in opts [:dynamic-widths col-index]))]
;; Whenever the cell changes, we need to calculate new bounds for the given content
;; -innerText is used here to strip out formatting, this may turn out to not work for all given block types
(rum/use-layout-effect! #(->> (.. cell-ref -current -innerText)
(count)
(handle-cell-width-change col-index))
[cell])
;; Whenever the cell becomes focused, we set it's tabIndex. When the tabIndex is set, call focus on the element
(rum/use-layout-effect! #(when (= cell-focus [col-index row-index])
; (.. cell-ref -current -tabIndex 0)
(some-> cell-ref .-current .focus))
; (.execCommand js/document "selectAll"))
[cell-focus])
[:div {:ref cell-ref
:class (cell-classes opts)
:style (cond-> {:box-sizing :border-box}
(not table-underflow?) (assoc :max-width (str MAX_WIDTH "rem"))
static-width (assoc :width (str static-width "rem"))
dynamic-width (assoc :min-width (str (max MIN_WIDTH dynamic-width) "rem"))
cell-order (assoc :order cell-order)
show-separator? (assoc :margin-top 3))
:tab-index (when (= cell-focus [col-index row-index]) "-1")
:on-pointer-enter #(set-cell-hover [col-index row-index])
:on-click #(handle-cell-click % opts cell-ref)
:on-pointer-down #(handle-cell-pointer-down % opts)
; :on-pointer-up handle-cell-interrupt
:on-key-down #(handle-cell-keydown % opts)}
(render-cell cell)]))
(rum/defc table-container [{:keys [columns borders? table-overflow? total-table-width gray set-cell-hover] :as opts} & children]
(let [grid-template-columns (str "repeat(" (count columns) ", minmax(max-content, 1fr))")]
[:div.grid.border.rounded {:style {:grid-template-columns grid-template-columns
:gap (when borders? BORDER_WIDTH)
:width (when table-overflow? total-table-width)}
:class (str (lsx 7) " " (lsx :border 7))
:data-testid "v2-table-container"
:on-pointer-leave #(set-cell-hover [])}
children]))
(rum/defc root
[{:keys [data] :as _props} {:keys [block color-accent color-gradient linear-gradient] :as context}]
(let [;; In order to highlight cells in the same row or column of the hovered cell,
;; we need to know the row and column that the cursor is in
[[_cell-hover-x _cell-hover-y :as cell-hover] set-cell-hover] (rum/use-state [])
[[_cell-focus-x _cell-focus-y :as cell-focus] set-cell-focus] (rum/use-state [])
;; Depending on the content of the table, we roughly adjust the width of the column
;; to do this we need to keep track of the .innerText.length of each cell and update
;; it whenever it changes
[dynamic-widths handle-cell-width-change] (use-dynamic-widths data)
;; We need to call into the view config several times, so we can memoize it
;; TODO: insert global config here
get-view-prop* (partial get-view-prop context)
;; Most of the config options will be repeated and reused throughout the table, so store
;; all of it's state in a single map for consistency
table-opts {; user configurable properties (sometimes with defaults)
:color (get-view-prop* :logseq.color color-accent)
:headers (get-view-prop* :logseq.table.headers "none")
:borders? (get-view-prop* :logseq.table.borders true)
:compact? (get-view-prop* :logseq.table.compact false)
:hover (get-view-prop* :logseq.table.hover "cell")
:stripes? (get-view-prop* :logseq.table.stripes false)
:gray (color->gray (get-in context [:config :logseq.color]))
:columns (get-columns block data)
; non configurable properties
:color-gradient color-gradient
:linear-gradient linear-gradient
:cell-hover cell-hover
:cell-focus cell-focus
:cursor (or (not-empty cell-focus) (not-empty cell-hover))
:dynamic-widths dynamic-widths
:render-cell (partial render-cell-in-context context)
:set-cell-hover set-cell-hover
:set-cell-focus set-cell-focus
:total-rows (reduce + 0 (map count data))}
;; The total table width has to account for the borders and the padding
;; everything is tracked in rems, except for the border, since it's so small
cell-padding-width (* 2 (if (:compact? table-opts) CELL_PADDING_COMPACT CELL_PADDING))
total-border-width (* (count (:columns table-opts)) BORDER_WIDTH)
total-table-width (->> (vals dynamic-widths)
(map (partial + cell-padding-width))
(reduce + 0)
(util/rem->px)
(+ total-border-width))
total-max-col-width (-> (count (:columns table-opts))
(* MAX_WIDTH)
(util/rem->px)
(+ total-border-width))
;; The table is actually rendered differently when it needs to be scrollable.
;; Keep track of whether the ideal table size overflows it's container size,
;; and provide a handler to be called whenever the container width changes
[table-overflow? table-underflow? handle-root-width-change] (use-table-flow-at-width total-table-width total-max-col-width)
;; Because the data may come in a different order than it should be presented,
;; we need to distinguish between these and provide a conversion.
;; The order the data is stored in is referred to as the cell order.
;; The order the data is displayed as is referred to as the col order.
;; Since these are called on every render of every cell, and are not dynamic, they are computed up front
cell-col-map (->> (ffirst data)
(map-indexed (juxt #(identity %1)
#(.indexOf (:columns table-opts) (.toLowerCase (last-str %2)))))
(remove (comp #{-1} second))
(into {}))
;; There are a couple more computed table properties that are best calculated
;; after the initial object is creaated
table-opts (assoc table-opts :total-cols (count (:columns table-opts))
:total-table-width total-table-width
:table-overflow? table-overflow?
:table-underflow? table-underflow?
:cell-col-map cell-col-map)]
; (js/console.log "shui table opts context" (clj->js context))
;; (js/console.log "shui table opts" (clj->js table-opts))
; (js/console.log "shui table opts" (pr-str table-opts))
;; Scrollable Container: if the table is larger than the container, manage the scrolling effects here
(table-scrollable-overflow handle-root-width-change
;; Grid Container: control the outermost table related element (border radius, grid, etc)
(table-container table-opts
;; Gradient Accent: the accent color at the top of the application
(when (:color table-opts)
(table-gradient-accent table-opts))
;; Rows: the actual table rows
(for [[group-index group-row-index row-index _group row] (map-with-all-indices data)
:let [show-separator? (and (= 0 group-row-index) (< 1 group-index))
opts (assoc table-opts :group-index group-index
:group-row-index group-row-index
:row-index row-index
:show-separator? show-separator?)]]
(if (= 0 group-index)
;; Table Header: Rows in the first section are rendered as headers
^{:key row-index} (table-header-row handle-cell-width-change row opts)
;; Table Body: The rest of the data is rendered as cells
^{:key row-index} (table-data-row handle-cell-width-change row opts)))))))

View File

@@ -11,61 +11,6 @@
(goog-define NODETEST false)
(defn el->clj-rect [el]
(let [rect (.getBoundingClientRect el)]
{:top (.-top rect)
:left (.-left rect)
:bottom (.-bottom rect)
:right (.-right rect)
:width (.-width rect)
:height (.-height rect)
:x (.-x rect)
:y (.-y rect)}))
(defn clj-rect-observer [update!]
(js/ResizeObserver.
(fn [entries]
(when (.-contentRect (first (js->clj entries)))
(update!)))))
(defn use-dom-bounding-client-rect
([el] (use-dom-bounding-client-rect el nil))
([el tick]
(let [[rect set-rect] (rum/use-state nil)]
(rum/use-effect!
(if el
(fn []
(let [update! #(set-rect (el->clj-rect el))
observer (clj-rect-observer update!)]
(update!)
(.observe observer el)
#(.disconnect observer)))
#())
[el tick])
rect)))
(defn use-ref-bounding-client-rect
([] (use-ref-bounding-client-rect nil))
([tick]
(let [[ref set-ref] (rum/use-state nil)
rect (use-dom-bounding-client-rect ref tick)]
[set-ref rect ref]))
([ref tick] [nil (use-dom-bounding-client-rect ref tick)]))
(defn rem->px [rem]
(-> js/document.documentElement
js/getComputedStyle
(.-fontSize)
(js/parseFloat)
(* rem)))
(defn px->rem [px]
(->> js/document.documentElement
js/getComputedStyle
(.-fontSize)
(js/parseFloat)
(/ px)))
(defn kebab-case->camel-case
"Converts from kebab case to camel case, eg: on-click to onClick"
[input]

View File

@@ -1,304 +0,0 @@
import { expect } from '@playwright/test'
import fs from 'fs/promises'
import path from 'path'
import { test } from '../fixtures'
import { randomString, editFirstBlock, navigateToStartOfBlock, createRandomPage } from '../utils'
test.setTimeout(60000)
const KEY_DELAY = 100
// The following function assumes that the block is currently in edit mode,
// and it just enters a simple table
const inputSimpleTable = async (page) => {
await page.keyboard.type('| Header A | Header B |')
await page.keyboard.press('Shift+Enter')
await page.keyboard.type('| A1 | B1 |')
await page.keyboard.press('Shift+Enter')
await page.keyboard.type('| A2 | B2 |')
await page.keyboard.press('Escape')
await page.waitForTimeout(KEY_DELAY)
}
// The following function does not assume any state, and will prepend the provided lines to the
// first block of the document
const prependPropsToFirstBlock = async (page, block, ...props) => {
await editFirstBlock(page)
await page.waitForTimeout(KEY_DELAY)
await navigateToStartOfBlock(page, block)
await page.waitForTimeout(KEY_DELAY)
for (const prop of props) {
await page.keyboard.type(prop)
await page.waitForTimeout(KEY_DELAY)
await page.keyboard.press('Shift+Enter')
await page.waitForTimeout(KEY_DELAY)
}
await page.keyboard.press('Escape')
await page.waitForTimeout(KEY_DELAY)
}
const setPropInFirstBlock = async (page, block, prop, value) => {
await editFirstBlock(page)
await page.waitForTimeout(KEY_DELAY)
await navigateToStartOfBlock(page, block)
await page.waitForTimeout(KEY_DELAY)
const inputValue = await page.inputValue('textarea >> nth=0')
const match = inputValue.match(new RegExp(`${prop}::(.*)(\n|$)`))
if (!match) {
await page.keyboard.press('Shift+Enter')
await page.waitForTimeout(KEY_DELAY)
await page.keyboard.press('ArrowUp')
await page.waitForTimeout(KEY_DELAY)
await page.keyboard.type(`${prop}:: ${value}`)
// await page.waitForTimeout(1000)
// await page.waitForTimeout(KEY_DELAY)
// await page.keyboard.type(prop + ':: ' + value)
// await page.waitForTimeout(1000)
// await page.keyboard.press('Shift+Enter')
await page.waitForTimeout(KEY_DELAY)
await page.keyboard.press('Escape')
return await page.waitForTimeout(KEY_DELAY)
}
const [propLine, propValue, propTernary] = match
const startIndex = match.index
const endIndex = startIndex + propLine.length - propTernary.length
// Go to the of the prop
for (let i = 0; i < endIndex; i++) {
await page.keyboard.press('ArrowRight')
}
// Delete the value of the prop
for (let i = 0; i < propValue.length; i++) {
await page.keyboard.press('Backspace')
}
// Input the new value of the prop
await page.keyboard.type(" " + value.trim())
await page.waitForTimeout(KEY_DELAY)
await page.keyboard.press('Escape')
return await page.waitForTimeout(KEY_DELAY)
}
test('table can have it\'s version changed via props', async ({ page, block, graphDir }) => {
const pageTitle = await createRandomPage(page)
// create a v1 table
inputSimpleTable(page)
// find and confirm existence of first data cell
await expect(await page.locator('table tbody tr >> nth=0').innerHTML()).toContain('A1</td>')
// change to a version 2 table
await setPropInFirstBlock(page, block, 'logseq.table.version', '2')
// find and confirm existence of first data cell in new format
await expect(await page.getByTestId('v2-table-container').innerHTML()).toContain('A1</div>')
})
test('table can configure logseq.color::', async ({ page, block, graphDir }) => {
const pageTitle = await createRandomPage(page)
// create a v1 table
await page.keyboard.type('logseq.table.version:: 2')
await page.keyboard.press('Shift+Enter')
await inputSimpleTable(page)
// check for default general config
await expect(await page.getByTestId('v2-table-gradient-accent')).not.toBeVisible()
await setPropInFirstBlock(page, block, 'logseq.color', 'red')
// check for gradient accent
await expect(await page.getByTestId('v2-table-gradient-accent')).toBeVisible()
})
test('table can configure logseq.table.hover::', async ({ page, block, graphDir }) => {
const pageTitle = await createRandomPage(page)
// create a v1 table
await page.keyboard.type('logseq.table.version:: 2')
await page.keyboard.press('Shift+Enter')
await inputSimpleTable(page)
await page.waitForTimeout(KEY_DELAY)
await page.getByText('A1', { exact: true }).hover()
await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]')
await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
await setPropInFirstBlock(page, block, 'logseq.table.hover', 'row')
await page.waitForTimeout(KEY_DELAY)
await page.getByText('A1', { exact: true }).hover()
await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]')
await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]')
await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
await setPropInFirstBlock(page, block, 'logseq.table.hover', 'col')
await page.waitForTimeout(KEY_DELAY)
await page.getByText('A1', { exact: true }).hover()
await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]')
await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]')
await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
await setPropInFirstBlock(page, block, 'logseq.table.hover', 'both')
await page.waitForTimeout(KEY_DELAY)
await page.getByText('A1', { exact: true }).hover()
await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]')
await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]')
await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]')
await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
await setPropInFirstBlock(page, block, 'logseq.table.hover', 'none')
await page.waitForTimeout(KEY_DELAY)
await page.getByText('A1', { exact: true }).hover()
await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-quaternary-background-color)]')
await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
})
test('table can configure logseq.table.headers', async ({ page, block, graphDir }) => {
const pageTitle = await createRandomPage(page)
// create a table
await page.keyboard.type('logseq.table.version:: 2')
await page.keyboard.press('Shift+Enter')
await inputSimpleTable(page)
// Check none (default)
await expect(await page.getByText('Header A', { exact: true })).toBeVisible()
await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header A")
// Check none (explicit)
await setPropInFirstBlock(page, block, 'logseq.table.headers', 'none')
await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header A")
// Check uppercase
await setPropInFirstBlock(page, block, 'logseq.table.headers', 'uppercase')
await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("HEADER A")
// Check lowercase
await setPropInFirstBlock(page, block, 'logseq.table.headers', 'lowercase')
await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("header a")
// Check capitalize
await setPropInFirstBlock(page, block, 'logseq.table.headers', 'capitalize')
await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header A")
// Check capitalize-first
await setPropInFirstBlock(page, block, 'logseq.table.headers', 'capitalize-first')
await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header a")
})
test('table can configure logseq.table.borders', async ({ page, block, graphDir }) => {
const pageTitle = await createRandomPage(page)
// create a table
await page.keyboard.type('logseq.table.version:: 2')
await page.keyboard.press('Shift+Enter')
await inputSimpleTable(page)
// Check true (default)
await expect(await page.getByTestId('v2-table-container')).toHaveCSS("gap", /^[1-9].*/)
// Check true (explicit)
await setPropInFirstBlock(page, block, 'logseq.table.borders', 'true')
await expect(await page.getByTestId('v2-table-container')).toHaveCSS("gap", /^[1-9].*/)
// Check false
await setPropInFirstBlock(page, block, 'logseq.table.borders', 'false')
await expect(await page.getByTestId('v2-table-container')).not.toHaveCSS("gap", /^[1-9].*/)
})
test('table can configure logseq.table.stripes', async ({ page, block, graphDir }) => {
const pageTitle = await createRandomPage(page)
// create a table
await page.keyboard.type('logseq.table.version:: 2')
await page.keyboard.press('Shift+Enter')
await inputSimpleTable(page)
await page.waitForTimeout(KEY_DELAY)
// Check false (default)
await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]")
await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]")
// Check false (explicit)
await setPropInFirstBlock(page, block, 'logseq.table.stripes', 'false')
await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]")
await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]")
// Check false
await setPropInFirstBlock(page, block, 'logseq.table.stripes', 'true')
await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]")
await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-secondary-background-color)]")
})
test('table can configure logseq.table.compact', async ({ page, block, graphDir }) => {
const pageTitle = await createRandomPage(page)
// create a table
await page.keyboard.type('logseq.table.version:: 2')
await page.keyboard.press('Shift+Enter')
await inputSimpleTable(page)
await page.waitForTimeout(KEY_DELAY)
// Check false (default)
const defaultClasses = await page.getByText('A1', { exact: true }).getAttribute('class')
// Check false (explicit)
await setPropInFirstBlock(page, block, 'logseq.table.compact', 'false')
const falseClasses = await page.getByText('A1', { exact: true }).getAttribute('class')
// Check false
await setPropInFirstBlock(page, block, 'logseq.table.compact', 'true')
const trueClasses = await page.getByText('A1', { exact: true }).getAttribute('class')
const getPX = (str) => {
const match = str.match(/px-\[([0-9\.]*)[a-z]*\]/)
return match ? parseFloat(match[1]) : null
}
await expect(getPX(defaultClasses)).toEqual(getPX(falseClasses))
await expect(getPX(defaultClasses)).toBeGreaterThan(getPX(trueClasses))
})
test('table can configure logseq.table.cols::', async ({ page, block, graphDir }) => {
const pageTitle = await createRandomPage(page)
// create a v1 table
await page.keyboard.type('logseq.table.version:: 2')
await page.keyboard.press('Shift+Enter')
await inputSimpleTable(page)
// check for default general config
await expect(await page.getByText('A1', { exact: true })).toBeVisible()
await expect(await page.getByText('B1', { exact: true })).toBeVisible()
await setPropInFirstBlock(page, block, 'logseq.table.cols', 'Header A, Header B')
await expect(await page.getByText('A1', { exact: true })).toBeVisible()
await expect(await page.getByText('B1', { exact: true })).toBeVisible()
await setPropInFirstBlock(page, block, 'logseq.table.cols', 'Header A')
await expect(await page.getByText('A1', { exact: true })).toBeVisible()
await expect(await page.getByText('B1', { exact: true })).not.toBeVisible()
await setPropInFirstBlock(page, block, 'logseq.table.cols', 'Header B')
await expect(await page.getByText('A1', { exact: true })).not.toBeVisible()
await expect(await page.getByText('B1', { exact: true })).toBeVisible()
})