* Fix extended task rendering in widgets
This patch addresses several issues.
1. Extended tasks like `[PLANNED]` or `[TODO]` were not rendering
correctly when generated via widgets. The HTML renderer was producing
plain text without brackets or proper styling, while simple `[x]`
tasks worked fine. The fix produces the same DOM structure as
CodeMirror producing coherent styling everywhere.
2. Widget checkboxes are not interactive now. Since widget tasks have no
backing state handler clicking them only caused a misleading visual
toggle with no effect. The `disabled` attribute prevents this
confusion and the dimmed appearanceit signals to the user that the
checkbox is read-only in this context which is the truth.
To see it in action try the following Markdown snippet in SilverBullet:
```
- [x] #foo [att: i]
- [PLANNED] bar #bar [att: i]
${'- [x] #foo [att: i]\n- [PLANNED] bar #bar [att: i]'}
```
Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
* Revert and just prevent widget checkboxes from toggling visually
Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
* Support custom task states in widgets and indexer
Custom task states like `[PLANNED]` or `[IN PROGRESS]` now render
correctly in *widgets*, matching the same DOM structure as CodeMirror
uses in the editor. Widget-rendered tasks that reference a real task
(via `page@pos`) are fully interactive — clicking an extended state
cycles it through the configured states and updates the source task.
Tasks without a reference are disabled to prevent misleading visual
feedback.
The task indexer now respects the `done` flag on custom states defined
via `taskState.define`, so queries like `where not t.done` correctly
exclude tasks in states marked as done. Previously only the built-in
simple tasks (`- [x]` or `- [X]`) states were treated as completed. The
`Task: Remove Completed` command already handled custom done states and
continues to work unchanged.
State cycling now preserves the definition order from `taskState.define`
calls instead of sorting alphabetically. A new `order` field allows
explicit control over the cycle sequence. Without it, states cycle in
definition order. With it, states are sorted by the `order` field value.
This gives users full control over the progression, for example:
PLANNED --> IN PROGRESS --> FINISHED
The `taskState.define` API now validates its input against a schema:
`name` (string, required), `done` (boolean, optional), and `order`
(number, optional).
The `expandMarkdown` no longer injects "fake" `page@pos` references into
tasks rendered via `widget.markdownBlock` or `widget.new` which was
causing broken links and unwanted interactivity on static task content.
The `task:stateChange` event is dispatched when toggling tasks from
widgets, ensuring Lua listeners (such as some custom completed timestamp
management) fire correctly regardless of where the toggle originates.
Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
---------
Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
11 KiB
11 KiB
description, tags
| description | tags |
|---|---|
| Defines the schemas and built-in values of built-in configuration options | meta |
SilverBullet uses the config APIs to manage its runtime configuration. You can set most of these anywhere in your space, but the convention is to use your CONFIG page for this purpose.
This meta page defines the JSON Schema of all built-in configuration settings, as well as their default values. You can override them by e.g. putting the following in your CONFIG page (use space-lua instead of lua in your actual page):
config.set("sync.documents", true)
Built-in options (schema)
This defines the JSON schema for built-in configuration.
-- priority: 100
config.define("sync", {
description = "Configure sync",
type = "object",
properties = {
-- Defaults to false
documents = schema.boolean(),
-- In .gitignore format, either in a single string, or as a list of strings
ignore = {
oneOf = {
schema.array "string",
schema.string()
}
}
},
additionalProperties = false
})
config.define("index", {
description = "Configure individual indexers",
type = "object",
properties = {
paragraph = {
type = "object",
properties = {
-- Index paragraphs without a hashtag
all = schema.boolean(),
},
},
item = {
type = "object",
properties = {
-- Index items without a hashtag
all = schema.boolean(),
},
},
task = {
type = "object",
properties = {
-- Index tasks without a hashtag
all = schema.boolean(),
},
},
},
additionalProperties = true
})
config.define("plugs", {
description = "List of plugs to enable",
oneOf = {
schema.array "string",
{ -- only as a fallback for an empty table in Lua
type = "object",
}
},
})
-- Editor configuration options
config.define("autoCloseBrackets", {
description = "List of brackets to auto close",
type = "string",
})
config.define("shortWikiLinks", {
description = "Render wiki links to just the last segment, e.g. Person/John becomes John",
type = "boolean"
})
config.define("emoji", {
description = "Additional emoji aliases",
type = "object",
properties = {
aliases = {
type = "object",
additionalProperties = schema.string(),
}
}
})
config.define("smartQuotes", {
description = "Configure smart quotes",
type = "object",
properties = {
enabled = {
type = "boolean",
description = "Indicates whether smart quotes are enabled"
},
double = {
type = "object",
properties = {
left = {
type = "string",
description = "Character for the left double quote"
},
right = {
type = "string",
description = "Character for the right double quote"
}
},
additionalProperties = false
},
single = {
type = "object",
properties = {
left = {
type = "string",
description = "Character for the left single quote"
},
right = {
type = "string",
description = "Character for the right single quote"
}
},
additionalProperties = false
},
},
additionalProperties = false
})
config.define("mobileMenuStyle", {
type = "string",
})
config.define("vim", {
description = "Vim mode configuration",
type = "object",
properties = {
unmap = {
description = "Keys to unmap",
type = "array",
items = {
oneOf = {
{ type = "string" },
{
type = "object",
properties = {
key = { type = "string" },
mode = {
type = "string",
enum = {"normal", "insert", "visual"}
}
},
required = { "key" },
additionalProperties = false
}
}
}
},
map = {
description = "Custom mappings",
type = "array",
items = {
type = "object",
properties = {
map = { type = "string" },
to = { type = "string" },
mode = {
type = "string",
enum = {"normal", "insert", "visual"}
}
},
required = { "map", "to" },
additionalProperties = false
}
},
noremap = {
description = "Non-recursive custom mappings",
type = "array",
items = {
type = "object",
properties = {
map = { type = "string" },
to = { type = "string" },
mode = {
type = "string",
enum = {"normal", "insert", "visual"}
}
},
required = { "map", "to" },
additionalProperties = false
}
},
commands = {
description = "Custom Ex commands",
type = "array",
items = {
type = "object",
properties = {
command = { type = "string" },
ex = { type = "string" }
},
required = { "command", "ex" },
additionalProperties = false
}
},
},
additionalProperties = false
})
-- Query specific configuration options
config.define("queryCollation", {
description = "Configure string ordering in queries",
type = "object",
properties = {
enabled = {
type = "boolean",
description = "Indicates whether string collation should be used instead of simple codepoint ordering"
},
locale = {
type = "string",
description = "Language tag to specify sorting rules (from BCP 47)"
},
options = {
type = "object",
description = "Additional options passed to Intl.Collator constructor"
-- See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#options
},
},
additionalProperties = false
})
-- Configuration for internal use mostly
-- Don't use directly, use command.define instead
config.define("commands", {
type = "object",
additionalProperties = {
type = "object",
properties = {
name = schema.string(),
contexts = schema.nullableArray "string",
priority = schema.nullable "number",
key = {
anyOf = {
schema.string(),
schema.array("string"),
schema.null()
}
},
mac = {
anyOf = {
schema.string(),
schema.array("string"),
schema.null()
}
},
hide = schema.nullable "boolean",
requireMode = schema.nullable {
type = "string",
enum = {"rw", "ro"},
},
requireEditor = schema.nullable("string"),
run = schema.func(),
},
},
})
-- Don't use directly, use slashCommands.define instead
config.define("slashCommands", {
type = "object",
additionalProperties = {
type = "object",
properties = {
name = schema.string(),
description = schema.nullable "string",
priority = schema.nullable "number",
onlyContexts = schema.nullableArray "string",
exceptContexts = schema.nullableArray "string",
run = schema.func(),
},
},
})
-- Don't use directly, use event.listen instead
config.define("eventHandlers", {
type = "object",
additionalProperties = schema.array(schema.func()),
})
-- Don't use directly, use mq.listen instead
config.define("mqSubscriptions", {
type = "object",
additionalProperties = {
type = "array",
items = {
type = "object",
properties = {
batchSize = schema.number(),
run = schema.func(),
}
}
},
})
-- Task states
config.define("taskStates", {
type = "object",
additionalProperties = {
type = "object",
properties = {
name = schema.string(),
done = schema.nullable "boolean",
order = schema.nullable "number",
},
required = {"name"},
},
})
-- Don't use directly, WIP
config.define("tags", {
type = "object",
additionalProperties = {
type = "object",
properties = {
name = schema.string(),
schema = schema.schema(),
-- Whether or not an object HAS to validate to be indexed (defaults to false), has a performance penalty
mustValidate = schema.boolean(),
-- Additional custom validation logic
validate = schema.func(),
-- Invoked by the object indexer, takes a proposed object as input, returns an array of objects (can be empty table to skip indexing altogether)
transform = schema.func(),
},
},
})
-- Don't use directly, use actionButton.define instead
config.define("actionButtons", {
type = "array",
items = {
type = "object",
properties = {
icon = {
type = "string",
description = "Icon for the action button, from https://feathericons.com"
},
description = {
type = "string",
description = "Optional description of the action button"
},
priority = {
type = "number",
description = "Optional priority: the higher the earlier the button will appear in the list"
},
mobile = {
type = "boolean",
description = "Optional boolean indicating if the action button is applicable for mobile"
},
standalone = {
type = "boolean",
description = "Optional: when set to true, button only appears in standalone/PWA mode; when false, only in browser mode"
},
dropdown = {
type = "boolean",
description = "Optional: set to false to keep this button outside the dropdown menu on mobile (default: true)"
},
run = schema.func(),
},
required = {"icon", "run"},
additionalProperties = false
}
})
Default values
Default values for built-in configuration options.
-- priority: 99
config.set {
index = {
paragraph = { all = false },
item = { all = true },
task = { all = true },
},
taskStates = {},
shortWikiLinks = true,
actionButtons = {
{
icon = "home",
description = "Go to the index page",
priority = 3,
run = function()
editor.invokeCommand "Navigate: Home"
end
},
{
icon = "book",
description = "Open page",
priority = 2,
dropdown = false,
run = function()
editor.invokeCommand "Navigate: Page Picker"
end
},
{
icon = "terminal",
description = "Run command",
priority = 1,
run = function()
editor.invokeCommand "Open Command Palette"
end,
},
{
icon = "chevron-left",
description = "Go back",
standalone = true,
priority = 0,
run = function()
editor.goHistory(-1)
end,
},
{
icon = "chevron-right",
description = "Go forward",
standalone = true,
priority = -1,
run = function()
editor.goHistory(1)
end,
}
},
}