Compare commits

..

21 Commits

Author SHA1 Message Date
JYC333
a6e0df70d7 refactor: address gemini review 2026-03-09 16:17:45 +00:00
JYC333
0ef7dd8fc2 refactor: minor cleanup 2026-03-09 15:57:47 +00:00
JYC333
bd1f6b7a0f refactor: fix full search 2026-03-09 15:44:55 +00:00
JYC333
cc24c2a4dc refactor: minor fix 2026-03-09 15:28:37 +00:00
JYC333
eaa6da73ca refactor: fix attribute detail autocomplete doesn't catch default value 2026-03-09 15:11:27 +00:00
JYC333
84ea5d3401 refactor: restore behaviors 2026-03-09 15:07:17 +00:00
JYC333
65095713cc refactor: fix behaviour difference 2026-03-09 14:54:25 +00:00
JYC333
8f6b565673 refactor: add back UI for note_autocomplete 2026-03-09 13:28:27 +00:00
JYC333
c0dd59458b refactor: migrate note_autocomplete core function 2026-03-09 12:39:51 +00:00
JYC333
d363d2016e refactor: address gemini code review 2026-03-09 12:21:26 +00:00
JYC333
702df56a8a refactor: migrate label autocomplete 2026-03-09 12:21:26 +00:00
JYC333
91677eb7da refactor: limit ctrl+enter action only at when creating reation on relation map 2026-03-09 12:21:26 +00:00
JYC333
e74f619d06 refactor: fix cleanup to avoid DOM leaks 2026-03-09 12:21:26 +00:00
JYC333
1c79beaa57 refactor: use ctrl+enter to confirm in relation creation at relation map page 2026-03-09 12:21:26 +00:00
JYC333
42a2b1bc13 fix: relation definition is not included when create relation in relation map 2026-03-09 12:21:26 +00:00
JYC333
8393b916c1 refactor: clean up old autocomplete implementation 2026-03-09 12:21:26 +00:00
JYC333
656796d18d refactor: migrate relation map 2026-03-09 12:21:26 +00:00
JYC333
0fe00b77cf fix: dropdown menu not follow the input when attribute detail dialog height changed 2026-03-09 12:21:26 +00:00
JYC333
150f88cc56 refactor: use headless autocomplete, migrate attribute deatil 2026-03-09 12:21:26 +00:00
JYC333
1b013e6888 refactor: add new autocomplete registry 2026-03-09 12:21:26 +00:00
JYC333
2dfc4514b0 refactor: add plan and package 2026-03-09 12:21:26 +00:00
10 changed files with 1780 additions and 377 deletions

View File

@@ -0,0 +1,272 @@
# Migration Plan: `autocomplete.js` → `@algolia/autocomplete-js`
> Issue: https://github.com/TriliumNext/Trilium/issues/5134
>
> 目标:将旧的 `autocomplete.js@0.38.1`jQuery 插件)迁移到 `@algolia/autocomplete-js`(独立组件)
---
## 当前状态总览
### 两个库的架构差异
| | 旧 `autocomplete.js` | 新 `@algolia/autocomplete-js` |
|---|---|---|
| 模式 | jQuery 插件,**增强已有 `<input>`** | 独立组件,**传入容器 `<div>`,自己创建 `<input>`** |
| 初始化 | `$el.autocomplete(config, [datasets])` | `autocomplete({ container, getSources, ... })` 返回 `api` |
| 操作 | `$el.autocomplete("open"/"close"/"val")` | `api.setIsOpen()`, `api.setQuery()`, `api.refresh()` |
| 销毁 | `$el.autocomplete("destroy")` | `api.destroy()` |
| 事件 | jQuery 事件 `autocomplete:selected` | `onSelect` 回调、`onStateChange` 回调 |
| DOM | 增强已有 input添加 `.aa-input` 类 | 替换容器内容为自己的 DOM`.aa-Form``.aa-Panel` 等) |
### 关键迁移原则
1. **不使用 wrapper/适配层**,直接在各 service 中调用 `autocomplete()` API
2. 消费者代码需要适配:传入容器 `<div>` 而非 `<input>`,通过 API/回调读写值
3. 增量迁移:每个使用点独立迁移,逐一验证
4. **优先保留旧版业务逻辑与交互语义**:迁移时默认以旧版 `autocomplete.js` 行为为准,不主动重设计状态流或交互。
5. **只有在新旧包能力或生命周期模型存在冲突、无法直接一一映射时,才允许添加补丁逻辑**;这类补丁的目标不是“接近”,而是尽可能恢复与旧版完全相同的 behavior。
### 涉及的功能区域
1. **属性名称自动补全**`attribute_autocomplete.ts``attribute_detail.ts``RelationMap.tsx`
2. **标签值自动补全**`attribute_autocomplete.ts``attribute_detail.ts`
3. **笔记搜索自动补全**`note_autocomplete.ts``NoteAutocomplete.tsx``attribute_detail.ts`
4. **关闭弹出窗口**`dialog.ts``entrypoints.ts``tab_manager.ts`
5. **CKEditor 提及** — 不使用 autocomplete.js**无需迁移**
---
## 迁移步骤
### Step 0: 安装新依赖 ✅ 完成
**文件变更:**
- `apps/client/package.json` — 添加 `@algolia/autocomplete-js@1.19.6`,暂时保留 `autocomplete.js`
**验证方式:**
- ✅ 新依赖安装成功
---
### Step 1: 迁移属性名称自动补全 ✅ 完成
**文件变更:**
- `apps/client/src/services/attribute_autocomplete.ts` — 将 `initAttributeNameAutocomplete()` 完全使用 **Headless API (`@algolia/autocomplete-core`)** 重写,移除遗留的 jQuery autocomplete 调用。
- `apps/client/src/widgets/attribute_widgets/attribute_detail.ts` — 维持原有 `<input>` 模型不变,仅需增加 `onValueChange` 处理回调。
- `apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx` — 维持原有回调逻辑,新旧无感替换。
- `apps/client/src/stylesheets/style.css` — 增加自定义 Headless 渲染面板样式 (`.aa-core-panel``.aa-core-list` 等)。
**架构说明:**
由于 Trilium 依赖同一页面同时运行多个 autocomplete 生命周期(边栏属性列表,底部编辑器等),原生 `@algolia/autocomplete-js` 会因为单例 DOM 冲突强行报错 "doesn't support multiple instances running at the same time"。
解决方案是退化使用纯状态机的 `@algolia/autocomplete-core`,自己进行 DOM 劫持与面板渲染。
- `requestAnimationFrame`:针对下拉层自动跟踪光标位置,适配面板的高频大小变化
- 事件阻断:拦截了选择时候的 `Enter` 返回键事件气泡,避免误触外层 Dialog 销毁。
**验证方式:**
- ✅ 打开一个笔记 → 点击属性面板弹出 "Label detail" → 输入属性名称时正常显示下拉自动补全框
- ✅ 放大/缩小/变形整个面板,下拉菜单粘连位置准确
- ✅ 键盘上下方向键可高亮,按 Enter 可选中当前项填充,且对话框不关闭
- ✅ 关系图 (Relation map) 创建关系时,关系名输入框的自动补全同样工作正常
---
### Step 2: 迁移标签值自动补全 ✅ 完成
**文件变更:**
- `apps/client/src/services/attribute_autocomplete.ts` — 移除旧有的 jQuery `$el.autocomplete` 初始化,整体复用封装的 `@algolia/autocomplete-core` Headless 架构流。在内部设计了一套针对 Label Name 值更变时的 `cachedAttributeName` 以及 `getItems` 数据惰性更新机制。
- `apps/client/src/widgets/attribute_widgets/attribute_detail.ts` — 取消监听不标准的 jQuery 强盗冒泡事件 `autocomplete:closed`,改为直接在配置中传入清晰的 `onValueChange` 回调函数。同时解决了所有输入遗留 Bug。
**说明与优化点:**
与 Step 1 类似,同样完全剔除了所有的残旧依赖与 jQuery 控制流,在此基础上还针对值类型的特异性做了几个高级改动:
1. **取消内存破坏型重建 (Fix Memory Leak)**:旧版本在每次触发聚焦 (Focus) 时都会发送摧毁指令强扫 DOM。新架构下只要容器保持存活就仅仅使用 `.refresh()` 接口来控制界面弹出与数据隐式获取。
2. **惰性与本地缓存 (Local Fast CACHE)**:如果关联的属性名 (Attribute Name) 没有被更改,再次打开提示面板时将以 0ms 的延迟抛出旧缓存 `cachedAttributeValues`。一旦属性名被修改,则重新发起服务端网络请求。
3. **彻底分离逻辑**:删除了文件中的 `still using old autocomplete.js` 遗留注释,此时 `attribute_autocomplete.ts` 文件内已经 100% 运行在崭新的 Autocomplete 体系上。
**验证方式:**
- ✅ 打开属性面板 → 点击或输入任意已有 Label 类型的 Name → 切换到值输入框 → 能瞬间弹出相应的旧值提示列表。
- ✅ 在旧值提示列表中用上下方向键选取并回车 → 能实现无缝填充并将更变保存回右侧详细侧边栏。
- ✅ 解决回车冲突:确认选择时系统发出的事件能干净落回所属宿主 DOM 且并不抢占外层组件快捷键。
---
### Step 3: 迁移笔记搜索自动补全核心 (拆分为 4 个增量阶段)
由于搜索自动补全模块(`note_autocomplete.ts`)承载了系统最为复杂的交互、多态分发与 UI我们将其拆分为 4 个逐步可验证的子阶段:
#### Step 3.1: 基础骨架与核心接口联通 (Headless 骨架) ✅ 完成
**目标:** 用 `@algolia/autocomplete-core` 完全接管旧版的 `$el.autocomplete` 初始化,打通搜索接口。
**工作内容:**
-`initNoteAutocomplete()` 中引入基于 `instanceMap` 的单例验证逻辑与 DOM 隔离。
- 建立 `getSources`,实现调用 `server.get("api/search/autocomplete", ...)`
- 只做极其简单的 UI比如简单的 `ul > li` text将获取到的 `title` 渲染出来,确保网络流程畅通。
**完成情况与验证 (**`apps/client/src/services/note_autocomplete.ts`**)**
- ✅ 彻底移除了原依赖于 jQuery `autocomplete.js` 的各种初始化配置与繁复的字符串 DOM 拼接节点。
- ✅ 实现了对 `Jump to Note (Ctrl+J)` 等真实组件的向下兼容事件 (`autocomplete:noteselected`) 无缝派发反馈。
- ✅ 在跳往某个具体笔记或在新建 Relation 面板选用特定目标笔记时,基础请求和简装提示版均工作正常。
#### Step 3.2: 复杂 UI 渲染重构与匹配高亮 (模板渲染) ✅ 基本完成
**目标:** 实现与原版相同级别(甚至更好)的视觉体验(例如笔记图标、上级路径显示、搜索词高亮标红等)。
**工作内容:**
- 重写原有的基于字符串或 jQuery 的构建 DOM 模板代码(专门处理带 `notePath` `icon` `isSearch` 判断等数据)。
- 将 DOM 构建系统集成到 `onStateChange` 的渲染函数里,通过 `innerHTML` 拼装或 DOM 手工建立实现原生高性能面板。
- 引入对应的样式 (`style.css`) 补全排版缺漏。
**验证方式:** 下拉出的搜索面板变得非常美观,与系统的 Dark/Light 色调融合;笔记标题对应的图标出现,匹配的字样高亮突出。
**当前验证结果:**
-`Ctrl+J / Jump to Note`UI 渲染、recent notes、键盘/鼠标高亮联动、删空回 recent notes 等核心交互已基本恢复。
-`attribute_detail.ts` 等依赖 jQuery 事件的目标笔记选择入口,抽查结果正常。
- ⚠️ React 侧消费者尚未完成迁移验收。抽查 `Move to` 时发现功能不正常,这部分应归入 **Step 5** 继续处理,而不是视为 Step 3.2 已全链路完成。
#### Step 3.3: 差异化分发逻辑与对外事件抛出 (交互改造) ✅ 基本完成
**目标:** 支持该组件的多态性。它能在搜笔记之外搜命令(`>` 起手)、甚至是外部链接。同时能够被外部组件监听到选择动作。
**工作内容:**
- 在选择项(`onSelect`)的回调中,根据用户选的是“系统命令”、“外部链接”还是“普通笔记”走截然不同的行为逻辑。
- 对外派发事件:原本通过 `$el.trigger("autocomplete:noteselected")` 的逻辑需要保留,以保证那些使用了搜索框的组件(例如右侧关系面板)依然能顺利收到选中反馈。
**验证方式:** 选中某个建议项时能够真正实现页面的调转/关系绑定;输入 `>` 开头能够列举出所有快捷命令(如 Toggle Dark mode
**当前验证结果:**
- ✅ 选择分发已按旧版语义迁移:`command``external-link``create-note``search-notes` 与普通 note 走独立分支。
-`autocomplete:noteselected``autocomplete:externallinkselected``autocomplete:commandselected` 三类对外事件均已保留。
- ✅ 鼠标点击和键盘回车现在统一走同一套 `handleSuggestionSelection()` 分发逻辑,不再额外误抛 `autocomplete:noteselected`
-`Ctrl+J / Jump to Note``attribute_detail.ts` 的普通 note 选择链路已抽查通过。
- ⚠️ React 消费方整体仍应放在 **Step 5** 继续验收;`Move to` 等问题不属于 Step 3.3 本身已完成的范围。
#### Step 3.4: 特殊键盘事件拦截与附带按钮包容 (终极打磨) ✅ 基本完成
**目标:** 解决在旧 jQuery 中强绑定的 IME中日韩等输入法防抖问题并恢复如 `Shift+Enter`、周边附加按钮(清除等)的正常运作。
**工作内容:**
- 将旧的输入法合成事件 (`compositionstart` / `compositionend`) 判断逻辑迁移到新的 `onInput` / `onKeyDown` 外围保护之中。
- 重构对 `Shift+Enter` (唤起全文搜索)、`Ctrl+Enter` 等组合快捷键的劫持处理。
- 修正周边辅助控件(例如搜索栏自带的 “最近笔记(钟表)”、“清除栏(X)” 按钮)因为 DOM 结构调整可能引发的影响。
**验证方式:** 中文拼音输入法敲打途中不会错误地发起网络搜索;各种组合回车热键重新生效,整个搜索系统重回巅峰状态。
**当前验证结果:**
-`compositionstart` / `compositionend` 已恢复旧版保护逻辑:合成期间不发起搜索,结束后按“清空再恢复 query”的语义重新跑一次。
-`Shift+Enter``Ctrl+Enter` 的快捷分发仍保留,并已按旧版语义接回全文搜索 / `search-notes`
-`autocomplete:opened` / `autocomplete:closed` 事件已重新补回,`readonly` 与“关闭时空输入框清理”逻辑重新对齐旧版。
- ✅ 清空按钮、最近笔记按钮、全文搜索按钮都继续走 service 内部统一入口,而不是分散拼状态。
- ⚠️ 这一步仍以 `note_autocomplete.ts` 核心行为为主React 消费方的问题继续留在 **Step 5**
---
### Step 4: 迁移辅助函数 ✅ 完成
**文件变更:**
- `apps/client/src/services/note_autocomplete.ts``clearText`, `setText`, `showRecentNotes` 等函数
**说明:**
这些函数使用旧库的操作 API`$el.autocomplete("val", value)` 等),需要改为新库的 `api.setQuery()` / `api.setIsOpen()` / `api.refresh()`
这一步与 **Step 3.4** 有交叉,但并不重复:
- **Step 3.4** 关注的是 IME、快捷键、按钮点击后的交互语义是否与旧版一致
- **Step 4** 关注的是 helper 函数本身是否已经彻底切到新 API而不再依赖旧版 `.autocomplete("...")`
**当前完成情况:**
-`clearText()` 已改为通过 headless instance 清空 query、关闭面板并触发 `change`
-`setText()` 已改为通过 `showQuery()` 驱动 `setQuery()` / `refresh()`
-`showRecentNotes()` 已改为走 `openRecentNotes()`,不再依赖旧版 `.autocomplete("open")`
-`showAllCommands()` 已改为直接设置 `">"` query 打开命令面板
-`fullTextSearch()` 已改为使用新状态流重跑全文搜索
**验证方式:**
- 最近笔记按钮 → 下拉菜单正常打开
- 清除按钮 → 输入框被清空
- Shift+Enter → 触发全文搜索
---
### Step 5: 迁移 `NoteAutocomplete.tsx` (React/Preact 组件)
**文件变更:**
- `apps/client/src/widgets/react/NoteAutocomplete.tsx` — 传入容器 `<div>`,管理 `api` 生命周期
**验证方式:**
- 关系属性的目标笔记选择正常工作
- `noteId``text` props 的动态更新正确
**当前状态:**
- ⚠️ 尚未完成。虽然底层 `note_autocomplete.ts` 已经切到新实现,但 React 消费方仍需逐一验收。
- ⚠️ 已抽查 `Move to`,当前功能不正常,说明 Step 5 仍存在待修复问题。
---
### Step 6: 迁移"关闭弹窗"逻辑 + `attribute_detail.ts` 引用
**文件变更:**
- `apps/client/src/services/dialog.ts` — 替换 `$(".aa-input").autocomplete("close")`
- `apps/client/src/components/entrypoints.ts` — 替换 `$(".aa-input").autocomplete("close")`
- `apps/client/src/components/tab_manager.ts` — 替换 `$(".aa-input").autocomplete("close")`
- `apps/client/src/widgets/attribute_widgets/attribute_detail.ts` — 更新 `.algolia-autocomplete` 选择器
**说明:**
需要一个全局的"关闭所有 autocomplete"机制。方案:维护一个全局 `Set<AutocompleteApi>`,在各处调用时遍历关闭。可以放在 `note_autocomplete.ts` 中导出。
**验证方式:**
- autocomplete 弹窗打开时切换标签页 → 弹窗自动关闭
- autocomplete 弹窗打开时打开对话框 → 弹窗自动关闭
- 点击 autocomplete 下拉菜单时属性面板不应关闭
---
### Step 7: 更新 CSS 样式
**文件变更:**
- `apps/client/src/stylesheets/style.css`(第 895-961 行)
**说明:**
新库使用的 CSS 类名:
- `.aa-Autocomplete` — 容器
- `.aa-Form` — 搜索表单(含 input
- `.aa-Input` — 输入框
- `.aa-Panel` — 下拉面板
- `.aa-List` — 列表
- `.aa-Item` — 列表项
- `.aa-Item[aria-selected="true"]` — 选中项
**验证方式:**
- 下拉菜单样式正常(亮色/暗色模式)
- 选中项高亮正确
---
### Step 8: 更新类型声明
**文件变更:**
- `apps/client/src/types.d.ts` — 移除 `AutoCompleteConfig``AutoCompleteArg`、jQuery `.autocomplete()` 方法
**验证方式:**
- TypeScript 编译无错误
---
### Step 9: 移除旧库和 Polyfill
**文件变更:**
- `apps/client/package.json` — 移除 `"autocomplete.js": "0.38.1"`
- `apps/client/src/desktop.ts` — 移除 `import "autocomplete.js/index_jquery.js";`
- `apps/client/src/mobile.ts` — 移除 `import "autocomplete.js/index_jquery.js";`
- `apps/client/src/runtime.ts` — 移除 jQuery polyfill
- `apps/client/src/index.ts` — 移除 jQuery polyfill
**验证方式:**
- 完整回归测试
- 构建无错误
---
### Step 10: 更新 E2E 测试
**文件变更:**
- `apps/server-e2e/src/support/app.ts`
- `apps/server-e2e/src/layout/split_pane.spec.ts`
**验证方式:**
- E2E 测试全部通过
---
## 依赖关系图
```
Step 0 (安装新库) ✅
├── Step 1 (属性名称 autocomplete) ← 最简单,优先迁移
├── Step 2 (标签值 autocomplete)
├── Step 3 (笔记搜索 autocomplete 核心) ← 最复杂
│ ├── Step 4 (辅助函数)
│ └── Step 5 (React 组件)
├── Step 6 (关闭弹窗 + attribute_detail 引用)
└── Step 7 (CSS 样式)
└── Step 8 (类型声明)
└── Step 9 (移除旧库) ← 最后执行
└── Step 10 (E2E 测试)
```
## 风险点
1. **消费者代码需要改动**:新库要求传入容器而非 input消费者需要调整 HTML 模板和值的读写方式。
2. **自定义事件兼容性**:旧库通过 jQuery 事件与外部交互,新库使用回调,`attribute_detail.ts` 等消费者中的事件监听需要更新。
3. **IME 输入处理**:新库原生支持 `ignoreCompositionEvents` 选项,但需要验证行为是否与旧的手动处理一致。
4. **CSS 类名变化**:多处代码通过 `.aa-input``.algolia-autocomplete` 定位元素,需要统一更新为新的 `.aa-*` 类名。
5. **全局关闭机制**:旧代码通过 `$(".aa-input").autocomplete("close")` 关闭所有实例,新库需要手动维护实例注册表。

View File

@@ -16,6 +16,7 @@
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
},
"dependencies": {
"@algolia/autocomplete-js": "1.19.6",
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",

View File

@@ -1,114 +1,447 @@
import type { AutocompleteApi as CoreAutocompleteApi, BaseItem } from "@algolia/autocomplete-core";
import { createAutocomplete } from "@algolia/autocomplete-core";
import type { AttributeType } from "../entities/fattribute.js";
import server from "./server.js";
interface InitOptions {
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface NameItem extends BaseItem {
name: string;
}
interface InitAttributeNameOptions {
/** The <input> element where the user types */
$el: JQuery<HTMLElement>;
attributeType?: AttributeType | (() => AttributeType);
open: boolean;
nameCallback?: () => string;
/** Called when the user selects a value or the panel closes */
onValueChange?: (value: string) => void;
}
/**
* @param $el - element on which to init autocomplete
* @param attributeType - "relation" or "label" or callback providing one of those values as a type of autocompleted attributes
* @param open - should the autocomplete be opened after init?
*/
function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions) {
if (!$el.hasClass("aa-input")) {
$el.autocomplete(
{
appendTo: document.querySelector("body"),
hint: false,
openOnFocus: true,
minLength: 0,
tabAutocomplete: false
},
[
{
displayKey: "name",
// disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type
cache: false,
source: async (term, cb) => {
const type = typeof attributeType === "function" ? attributeType() : attributeType;
// ---------------------------------------------------------------------------
// Instance tracking
// ---------------------------------------------------------------------------
const names = await server.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`);
const result = names.map((name) => ({ name }));
cb(result);
}
}
]
);
$el.on("autocomplete:opened", () => {
if ($el.attr("readonly")) {
$el.autocomplete("close");
}
});
}
if (open) {
$el.autocomplete("open");
}
interface ManagedInstance {
autocomplete: CoreAutocompleteApi<NameItem>;
panelEl: HTMLElement;
cleanup: () => void;
}
async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptions) {
if ($el.hasClass("aa-input")) {
// we reinit every time because autocomplete seems to have a bug where it retains state from last
// open even though the value was reset
$el.autocomplete("destroy");
}
const instanceMap = new WeakMap<HTMLElement, ManagedInstance>();
let attributeName = "";
if (nameCallback) {
attributeName = nameCallback();
}
// ---------------------------------------------------------------------------
// Dropdown panel DOM helpers
// ---------------------------------------------------------------------------
if (attributeName.trim() === "") {
function createPanelEl(): HTMLElement {
const panel = document.createElement("div");
panel.className = "aa-core-panel";
panel.style.display = "none";
document.body.appendChild(panel);
return panel;
}
function renderItems(panelEl: HTMLElement, items: NameItem[], activeItemId: number | null, onSelect: (item: NameItem) => void): void {
panelEl.innerHTML = "";
if (items.length === 0) {
panelEl.style.display = "none";
return;
}
const attributeValues = (await server.get<string[]>(`attribute-values/${encodeURIComponent(attributeName)}`)).map((attribute) => ({ value: attribute }));
if (attributeValues.length === 0) {
return;
}
$el.autocomplete(
{
appendTo: document.querySelector("body"),
hint: false,
openOnFocus: false, // handled manually
minLength: 0,
tabAutocomplete: false
},
[
{
displayKey: "value",
cache: false,
source: async function (term, cb) {
term = term.toLowerCase();
const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term));
cb(filtered);
}
}
]
);
$el.on("autocomplete:opened", () => {
if ($el.attr("readonly")) {
$el.autocomplete("close");
const list = document.createElement("ul");
list.className = "aa-core-list";
items.forEach((item, index) => {
const li = document.createElement("li");
li.className = "aa-core-item";
if (index === activeItemId) {
li.classList.add("aa-core-item--active");
}
li.textContent = item.name;
li.addEventListener("mousedown", (e) => {
e.preventDefault(); // prevent input blur
onSelect(item);
});
list.appendChild(li);
});
panelEl.appendChild(list);
}
function positionPanel(panelEl: HTMLElement, inputEl: HTMLElement): void {
const rect = inputEl.getBoundingClientRect();
const top = `${rect.bottom}px`;
const left = `${rect.left}px`;
const width = `${rect.width}px`;
panelEl.style.position = "fixed";
if (panelEl.style.top !== top) panelEl.style.top = top;
if (panelEl.style.left !== left) panelEl.style.left = left;
if (panelEl.style.width !== width) panelEl.style.width = width;
if (panelEl.style.display !== "block") panelEl.style.display = "block";
}
// ---------------------------------------------------------------------------
// Attribute name autocomplete — new (autocomplete-core, headless)
// ---------------------------------------------------------------------------
function initAttributeNameAutocomplete({ $el, attributeType, open, onValueChange }: InitAttributeNameOptions) {
const inputEl = $el[0] as HTMLInputElement;
const syncQueryFromInputValue = (autocomplete: CoreAutocompleteApi<NameItem>) => {
autocomplete.setQuery(inputEl.value || "");
};
// Already initialized — just open if requested
if (instanceMap.has(inputEl)) {
if (open) {
const inst = instanceMap.get(inputEl)!;
syncQueryFromInputValue(inst.autocomplete);
inst.autocomplete.setIsOpen(true);
inst.autocomplete.refresh();
}
return;
}
const panelEl = createPanelEl();
let isPanelOpen = false;
let hasActiveItem = false;
let rafId: number | null = null;
function startPositioning() {
if (rafId !== null) return;
const update = () => {
positionPanel(panelEl, inputEl);
rafId = requestAnimationFrame(update);
};
update();
}
function stopPositioning() {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
}
const autocomplete = createAutocomplete<NameItem>({
openOnFocus: true,
defaultActiveItemId: 0,
shouldPanelOpen() {
return true;
},
getSources({ query }) {
return [
{
sourceId: "attribute-names",
getItems() {
const type = typeof attributeType === "function" ? attributeType() : attributeType;
return server
.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(query)}`)
.then((names) => names.map((name) => ({ name })));
},
getItemInputValue({ item }) {
return item.name;
},
onSelect({ item }) {
inputEl.value = item.name;
autocomplete.setQuery(item.name);
autocomplete.setIsOpen(false);
onValueChange?.(item.name);
},
},
];
},
onStateChange({ state }) {
isPanelOpen = state.isOpen;
hasActiveItem = state.activeItemId !== null;
// Render items
const collections = state.collections;
const items = collections.length > 0 ? (collections[0].items as NameItem[]) : [];
const activeId = state.activeItemId ?? null;
if (state.isOpen && items.length > 0) {
renderItems(panelEl, items, activeId, (item) => {
inputEl.value = item.name;
autocomplete.setQuery(item.name);
autocomplete.setIsOpen(false);
onValueChange?.(item.name);
});
startPositioning();
} else {
panelEl.style.display = "none";
stopPositioning();
}
if (!state.isOpen) {
panelEl.style.display = "none";
stopPositioning();
}
},
});
// Wire up the input events
const handlers = autocomplete.getInputProps({ inputElement: inputEl });
const onInput = (e: Event) => {
handlers.onChange(e as any);
};
const onFocus = (e: Event) => {
syncQueryFromInputValue(autocomplete);
handlers.onFocus(e as any);
};
const onBlur = () => {
// Delay to allow mousedown on panel items
setTimeout(() => {
autocomplete.setIsOpen(false);
panelEl.style.display = "none";
stopPositioning();
onValueChange?.(inputEl.value);
}, 50);
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" && isPanelOpen && hasActiveItem) {
// Prevent the enter key from propagating to parent dialogs
// (which might interpret it as "submit" or "save and close")
e.stopPropagation();
// We shouldn't preventDefault here because we want handlers.onKeyDown
// to process it properly. OnSelect will correctly close the panel.
}
handlers.onKeyDown(e as any);
};
inputEl.addEventListener("input", onInput);
inputEl.addEventListener("focus", onFocus);
inputEl.addEventListener("blur", onBlur);
inputEl.addEventListener("keydown", onKeyDown);
const cleanup = () => {
inputEl.removeEventListener("input", onInput);
inputEl.removeEventListener("focus", onFocus);
inputEl.removeEventListener("blur", onBlur);
inputEl.removeEventListener("keydown", onKeyDown);
stopPositioning();
if (panelEl.parentElement) {
panelEl.parentElement.removeChild(panelEl);
}
};
instanceMap.set(inputEl, { autocomplete, panelEl, cleanup });
if (open) {
$el.autocomplete("open");
syncQueryFromInputValue(autocomplete);
autocomplete.setIsOpen(true);
autocomplete.refresh();
startPositioning();
}
}
// ---------------------------------------------------------------------------
// Label value autocomplete (headless autocomplete-core)
// ---------------------------------------------------------------------------
interface LabelValueInitOptions {
$el: JQuery<HTMLElement>;
open: boolean;
nameCallback?: () => string;
onValueChange?: (value: string) => void;
}
function initLabelValueAutocomplete({ $el, open, nameCallback, onValueChange }: LabelValueInitOptions) {
const inputEl = $el[0] as HTMLInputElement;
const syncQueryFromInputValue = (autocomplete: CoreAutocompleteApi<NameItem>) => {
autocomplete.setQuery(inputEl.value || "");
};
if (instanceMap.has(inputEl)) {
if (open) {
const inst = instanceMap.get(inputEl)!;
syncQueryFromInputValue(inst.autocomplete);
inst.autocomplete.setIsOpen(true);
inst.autocomplete.refresh();
}
return;
}
const panelEl = createPanelEl();
let isPanelOpen = false;
let hasActiveItem = false;
let isSelecting = false;
let rafId: number | null = null;
function startPositioning() {
if (rafId !== null) return;
const update = () => {
positionPanel(panelEl, inputEl);
rafId = requestAnimationFrame(update);
};
update();
}
function stopPositioning() {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
}
let cachedAttributeName = "";
let cachedAttributeValues: NameItem[] = [];
const handleSelect = (item: NameItem) => {
isSelecting = true;
inputEl.value = item.name;
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
autocomplete.setQuery(item.name);
autocomplete.setIsOpen(false);
onValueChange?.(item.name);
isSelecting = false;
setTimeout(() => {
// Preserve the legacy contract: several consumers still commit the
// selected value from their existing Enter key handlers instead of
// listening to the autocomplete selection event directly.
inputEl.dispatchEvent(new KeyboardEvent("keydown", {
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
}));
}, 0);
};
const autocomplete = createAutocomplete<NameItem>({
openOnFocus: true,
defaultActiveItemId: null,
shouldPanelOpen() {
return true;
},
getSources({ query }) {
return [
{
sourceId: "attribute-values",
async getItems() {
const attributeName = nameCallback ? nameCallback() : "";
if (!attributeName.trim()) {
return [];
}
if (attributeName !== cachedAttributeName || cachedAttributeValues.length === 0) {
cachedAttributeName = attributeName;
const values = await server.get<string[]>(`attribute-values/${encodeURIComponent(attributeName)}`);
cachedAttributeValues = values.map((name) => ({ name }));
}
const q = query.toLowerCase();
return cachedAttributeValues.filter((attr) => attr.name.toLowerCase().includes(q));
},
getItemInputValue({ item }) {
return item.name;
},
onSelect({ item }) {
handleSelect(item);
},
},
];
},
onStateChange({ state }) {
isPanelOpen = state.isOpen;
hasActiveItem = state.activeItemId !== null;
const collections = state.collections;
const items = collections.length > 0 ? (collections[0].items as NameItem[]) : [];
const activeId = state.activeItemId ?? null;
if (state.isOpen && items.length > 0) {
renderItems(panelEl, items, activeId, handleSelect);
startPositioning();
} else {
panelEl.style.display = "none";
stopPositioning();
}
if (!state.isOpen) {
panelEl.style.display = "none";
stopPositioning();
}
},
});
const handlers = autocomplete.getInputProps({ inputElement: inputEl });
const onInput = (e: Event) => {
if (!isSelecting) {
handlers.onChange(e as any);
}
};
const onFocus = (e: Event) => {
const attributeName = nameCallback ? nameCallback() : "";
if (attributeName !== cachedAttributeName) {
cachedAttributeName = "";
cachedAttributeValues = [];
}
syncQueryFromInputValue(autocomplete);
handlers.onFocus(e as any);
};
const onBlur = () => {
setTimeout(() => {
autocomplete.setIsOpen(false);
panelEl.style.display = "none";
stopPositioning();
onValueChange?.(inputEl.value);
}, 50);
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" && isPanelOpen && hasActiveItem) {
e.stopPropagation();
}
handlers.onKeyDown(e as any);
};
inputEl.addEventListener("input", onInput);
inputEl.addEventListener("focus", onFocus);
inputEl.addEventListener("blur", onBlur);
inputEl.addEventListener("keydown", onKeyDown);
const cleanup = () => {
inputEl.removeEventListener("input", onInput);
inputEl.removeEventListener("focus", onFocus);
inputEl.removeEventListener("blur", onBlur);
inputEl.removeEventListener("keydown", onKeyDown);
stopPositioning();
if (panelEl.parentElement) {
panelEl.parentElement.removeChild(panelEl);
}
};
instanceMap.set(inputEl, { autocomplete, panelEl, cleanup });
if (open) {
syncQueryFromInputValue(autocomplete);
autocomplete.setIsOpen(true);
autocomplete.refresh();
startPositioning();
}
}
export function destroyAutocomplete($el: JQuery<HTMLElement> | HTMLElement) {
const inputEl = $el instanceof HTMLElement ? $el : $el[0] as HTMLInputElement;
const instance = instanceMap.get(inputEl);
if (instance) {
instance.cleanup();
instanceMap.delete(inputEl);
}
}
export default {
initAttributeNameAutocomplete,
initLabelValueAutocomplete
destroyAutocomplete,
initLabelValueAutocomplete,
};

File diff suppressed because it is too large Load Diff

View File

@@ -960,6 +960,153 @@ table.promoted-attributes-in-tooltip th {
background-color: var(--active-item-background-color);
}
/* ===== @algolia/autocomplete-core (headless, custom panel) ===== */
.aa-core-panel {
z-index: 10000;
background-color: var(--main-background-color);
border: 1px solid var(--main-border-color);
border-top: none;
max-height: 500px;
overflow: auto;
padding: 0;
margin: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.aa-core-panel.aa-dropdown-menu {
width: 100%;
}
.aa-core-panel--contained {
position: static !important;
border: 0;
background: transparent;
box-shadow: none;
}
.aa-core-list {
list-style: none;
padding: 0;
margin: 0;
}
.aa-core-item {
cursor: pointer;
padding: 7px 16px;
margin: 0;
white-space: normal;
}
.aa-core-item--active {
color: var(--active-item-text-color);
background-color: var(--active-item-background-color);
}
.aa-core-item .note-suggestion {
display: flex;
align-items: flex-start;
gap: 8px;
width: 100%;
}
.aa-core-item .icon,
.aa-core-item .command-icon {
flex-shrink: 0;
line-height: 1.4;
margin-top: 1px;
}
.aa-core-item .text {
min-width: 0;
flex: 1;
}
.aa-core-item .aa-core-primary-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.aa-core-item .search-result-title {
display: block;
min-width: 0;
line-height: 1.35;
word-break: break-word;
font-size: 1.02em;
}
.aa-core-item .search-result-attributes {
display: block;
margin-top: 1px;
font-size: 0.8em;
color: var(--muted-text-color);
opacity: 0.65;
line-height: 1.2;
word-break: break-word;
}
.aa-core-item .search-result-attributes {
padding-inline-start: 14px;
}
.aa-core-item .aa-core-shortcut,
.aa-core-item kbd.command-shortcut {
flex-shrink: 0;
padding: 0;
border: 0;
background: transparent;
color: var(--muted-text-color);
font-family: inherit !important;
font-size: 0.8em;
opacity: 0.85;
}
.aa-core-item .command-suggestion {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
font-size: 0.9em;
}
.aa-core-item .command-content {
flex-grow: 1;
min-width: 0;
}
.aa-core-item .command-name {
font-weight: bold;
line-height: 1.35;
}
.aa-core-item .command-description {
font-size: 0.8em;
line-height: 1.3;
opacity: 0.75;
}
.aa-core-item .search-result-title b,
.aa-core-item .search-result-path b,
.aa-core-item .search-result-attributes b,
.aa-core-item .command-name b,
.aa-core-item .command-description b {
color: var(--admonition-warning-accent-color);
text-decoration: underline;
}
.aa-core-item .aa-core-separator {
padding: 0 2px;
}
.jump-to-note-results .aa-core-panel--contained {
max-height: calc(80vh - 200px);
overflow-y: auto;
overflow-x: hidden;
text-overflow: ellipsis;
}
.help-button {
float: inline-end;
background: none;

View File

@@ -378,7 +378,8 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
attributeAutocompleteService.initAttributeNameAutocomplete({
$el: this.$inputName,
attributeType: () => (["relation", "relation-definition"].includes(this.attrType || "") ? "relation" : "label"),
open: true
open: true,
onValueChange: () => this.userEditedAttribute(),
});
});
@@ -391,12 +392,12 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
}
});
this.$inputValue.on("change", () => this.userEditedAttribute());
this.$inputValue.on("autocomplete:closed", () => this.userEditedAttribute());
this.$inputValue.on("focus", () => {
attributeAutocompleteService.initLabelValueAutocomplete({
$el: this.$inputValue,
open: true,
nameCallback: () => String(this.$inputName.val())
nameCallback: () => String(this.$inputName.val()),
onValueChange: () => this.userEditedAttribute(),
});
});

View File

@@ -24,6 +24,7 @@ export interface PromptDialogOptions {
shown?: PromptShownDialogCallback;
callback?: (value: string | null) => void;
readOnly?: boolean;
submitWithCtrlEnter?: boolean;
}
export default function PromptDialog() {
@@ -69,7 +70,7 @@ export default function PromptDialog() {
submitValue.current = null;
opts.current = undefined;
}}
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" kind="primary" />}
footer={<Button text={t("prompt.ok")} keyboardShortcut={opts.current?.submitWithCtrlEnter ? "ctrl+return" : "Enter"} kind="primary" />}
show={shown}
stackable
>
@@ -78,6 +79,13 @@ export default function PromptDialog() {
inputRef={answerRef}
currentValue={value} onChange={setValue}
readOnly={opts.current?.readOnly}
onKeyDown={(e: KeyboardEvent) => {
if (opts.current?.submitWithCtrlEnter && (e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
submitValue.current = answerRef.current?.value || value;
setShown(false);
}
}}
/>
</FormGroup>
</Modal>

View File

@@ -416,6 +416,7 @@ function useRelationCreation({ mapApiRef, jsPlumbApiRef }: { mapApiRef: RefObjec
if (!originalEvent || !mapApiRef.current) return;
const name = await dialog.prompt({
submitWithCtrlEnter: true,
message: t("relation_map.specify_new_relation_name"),
shown: ({ $answer }) => {
if (!$answer) {

View File

@@ -71,6 +71,27 @@ function getAttributeNames(type: string, nameLike: string) {
[type, `%${nameLike}%`]
);
// Also include attribute definitions (e.g. 'relation:*' or 'label:*') which are saved as type='label'
if (type === "relation" || type === "label") {
const prefix = `${type}:`;
const defNames = sql.getColumn<string>(
/*sql*/`SELECT DISTINCT name
FROM attributes
WHERE isDeleted = 0
AND type = 'label'
AND name LIKE ?`,
[`${prefix}%${nameLike}%`]
);
for (const dn of defNames) {
if (dn.startsWith(prefix)) {
const stripped = dn.substring(prefix.length);
if (!names.includes(stripped)) {
names.push(stripped);
}
}
}
}
for (const attr of BUILTIN_ATTRIBUTES) {
if (attr.type === type && attr.name.toLowerCase().includes(nameLike) && !names.includes(attr.name)) {
names.push(attr.name);

278
pnpm-lock.yaml generated
View File

@@ -182,6 +182,9 @@ importers:
apps/client:
dependencies:
'@algolia/autocomplete-js':
specifier: 1.19.6
version: 1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)
'@excalidraw/excalidraw':
specifier: 0.18.0
version: 0.18.0(@types/react-dom@19.1.6(@types/react@19.1.7))(@types/react@19.1.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -1530,6 +1533,88 @@ packages:
rollup:
optional: true
'@algolia/abtesting@1.15.1':
resolution: {integrity: sha512-2yuIC48rUuHGhU1U5qJ9kJHaxYpJ0jpDHJVI5ekOxSMYXlH4+HP+pA31G820lsAznfmu2nzDV7n5RO44zIY1zw==}
engines: {node: '>= 14.0.0'}
'@algolia/autocomplete-core@1.19.6':
resolution: {integrity: sha512-6EoD7PeM2WBq5GY1jm0gGonDW2JVU4BaHT9tAwDcaPkc6gYIRZeY7X7aFuwdRvk9R/jwsh8sz4flDao0+Kua6g==}
'@algolia/autocomplete-js@1.19.6':
resolution: {integrity: sha512-rHYKT6P+2FZ1+7a1/JtWIuCmfioOt5eXsAcri6XTYsSutl3BIh8s2e98kbvjbhLfwEuuVDWtST1hdAY2pQdrKw==}
peerDependencies:
'@algolia/client-search': '>= 4.5.1 < 6'
algoliasearch: '>= 4.9.1 < 6'
'@algolia/autocomplete-plugin-algolia-insights@1.19.6':
resolution: {integrity: sha512-VD53DBixhEwDvOB00D03DtBVhh5crgb1N0oH3QTscfYk4TpBH+CKrwmN/XrN/VdJAdP+4K6SgwLii/3OwM9dHw==}
peerDependencies:
search-insights: '>= 1 < 3'
'@algolia/autocomplete-preset-algolia@1.19.6':
resolution: {integrity: sha512-/uQlHGK5Q2x5Nvrp3W7JMg4YNGG/ygkHtQLTltDbkpd45wnhV9jUiQA6aCnBed9cq0BXhOJZRxh1zGVZ3yRhBg==}
peerDependencies:
'@algolia/client-search': '>= 4.9.1 < 6'
algoliasearch: '>= 4.9.1 < 6'
'@algolia/autocomplete-shared@1.19.6':
resolution: {integrity: sha512-DG1n2B6XQw6DWB5veO4RuzQ/N2oGNpG+sSzGT7gUbi7WhF+jN57abcv2QhB5flXZ0NgddE1i6h7dZuQmYBEorQ==}
peerDependencies:
'@algolia/client-search': '>= 4.9.1 < 6'
algoliasearch: '>= 4.9.1 < 6'
'@algolia/client-abtesting@5.49.1':
resolution: {integrity: sha512-h6M7HzPin+45/l09q0r2dYmocSSt2MMGOOk5c4O5K/bBBlEwf1BKfN6z+iX4b8WXcQQhf7rgQwC52kBZJt/ZZw==}
engines: {node: '>= 14.0.0'}
'@algolia/client-analytics@5.49.1':
resolution: {integrity: sha512-048T9/Z8OeLmTk8h76QUqaNFp7Rq2VgS2Zm6Y2tNMYGQ1uNuzePY/udB5l5krlXll7ZGflyCjFvRiOtlPZpE9g==}
engines: {node: '>= 14.0.0'}
'@algolia/client-common@5.49.1':
resolution: {integrity: sha512-vp5/a9ikqvf3mn9QvHN8PRekn8hW34aV9eX+O0J5mKPZXeA6Pd5OQEh2ZWf7gJY6yyfTlLp5LMFzQUAU+Fpqpg==}
engines: {node: '>= 14.0.0'}
'@algolia/client-insights@5.49.1':
resolution: {integrity: sha512-B6N7PgkvYrul3bntTz/l6uXnhQ2bvP+M7NqTcayh681tSqPaA5cJCUBp/vrP7vpPRpej4Eeyx2qz5p0tE/2N2g==}
engines: {node: '>= 14.0.0'}
'@algolia/client-personalization@5.49.1':
resolution: {integrity: sha512-v+4DN+lkYfBd01Hbnb9ZrCHe7l+mvihyx218INRX/kaCXROIWUDIT1cs3urQxfE7kXBFnLsqYeOflQALv/gA5w==}
engines: {node: '>= 14.0.0'}
'@algolia/client-query-suggestions@5.49.1':
resolution: {integrity: sha512-Un11cab6ZCv0W+Jiak8UktGIqoa4+gSNgEZNfG8m8eTsXGqwIEr370H3Rqwj87zeNSlFpH2BslMXJ/cLNS1qtg==}
engines: {node: '>= 14.0.0'}
'@algolia/client-search@5.49.1':
resolution: {integrity: sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA==}
engines: {node: '>= 14.0.0'}
'@algolia/ingestion@1.49.1':
resolution: {integrity: sha512-b5hUXwDqje0Y4CpU6VL481DXgPgxpTD5sYMnfQTHKgUispGnaCLCm2/T9WbJo1YNUbX3iHtYDArp804eD6CmRQ==}
engines: {node: '>= 14.0.0'}
'@algolia/monitoring@1.49.1':
resolution: {integrity: sha512-bvrXwZ0WsL3rN6Q4m4QqxsXFCo6WAew7sAdrpMQMK4Efn4/W920r9ptOuckejOSSvyLr9pAWgC5rsHhR2FYuYw==}
engines: {node: '>= 14.0.0'}
'@algolia/recommend@5.49.1':
resolution: {integrity: sha512-h2yz3AGeGkQwNgbLmoe3bxYs8fac4An1CprKTypYyTU/k3Q+9FbIvJ8aS1DoBKaTjSRZVoyQS7SZQio6GaHbZw==}
engines: {node: '>= 14.0.0'}
'@algolia/requester-browser-xhr@5.49.1':
resolution: {integrity: sha512-2UPyRuUR/qpqSqH8mxFV5uBZWEpxhGPHLlx9Xf6OVxr79XO2ctzZQAhsmTZ6X22x+N8MBWpB9UEky7YU2HGFgA==}
engines: {node: '>= 14.0.0'}
'@algolia/requester-fetch@5.49.1':
resolution: {integrity: sha512-N+xlE4lN+wpuT+4vhNEwPVlrfN+DWAZmSX9SYhbz986Oq8AMsqdntOqUyiOXVxYsQtfLwmiej24vbvJGYv1Qtw==}
engines: {node: '>= 14.0.0'}
'@algolia/requester-node-http@5.49.1':
resolution: {integrity: sha512-zA5bkUOB5PPtTr182DJmajCiizHp0rCJQ0Chf96zNFvkdESKYlDeYA3tQ7r2oyHbu/8DiohAQ5PZ85edctzbXA==}
engines: {node: '>= 14.0.0'}
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
@@ -7518,6 +7603,10 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
algoliasearch@5.49.1:
resolution: {integrity: sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==}
engines: {node: '>= 14.0.0'}
alien-signals@0.4.14:
resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==}
@@ -10462,6 +10551,9 @@ packages:
hpack.js@2.1.6:
resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==}
htm@3.1.1:
resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==}
html-encoding-sniffer@2.0.1:
resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==}
engines: {node: '>=10'}
@@ -14406,6 +14498,9 @@ packages:
scule@1.3.0:
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
search-insights@2.17.3:
resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==}
secure-compare@3.0.1:
resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==}
@@ -16489,6 +16584,130 @@ snapshots:
optionalDependencies:
rollup: 4.52.0
'@algolia/abtesting@1.15.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
'@algolia/autocomplete-core@1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)':
dependencies:
'@algolia/autocomplete-plugin-algolia-insights': 1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)
'@algolia/autocomplete-shared': 1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)
transitivePeerDependencies:
- '@algolia/client-search'
- algoliasearch
- search-insights
'@algolia/autocomplete-js@1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)':
dependencies:
'@algolia/autocomplete-core': 1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)
'@algolia/autocomplete-preset-algolia': 1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)
'@algolia/autocomplete-shared': 1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)
'@algolia/client-search': 5.49.1
algoliasearch: 5.49.1
htm: 3.1.1
preact: 10.28.4
transitivePeerDependencies:
- search-insights
'@algolia/autocomplete-plugin-algolia-insights@1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)':
dependencies:
'@algolia/autocomplete-shared': 1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)
search-insights: 2.17.3
transitivePeerDependencies:
- '@algolia/client-search'
- algoliasearch
'@algolia/autocomplete-preset-algolia@1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)':
dependencies:
'@algolia/autocomplete-shared': 1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)
'@algolia/client-search': 5.49.1
algoliasearch: 5.49.1
'@algolia/autocomplete-shared@1.19.6(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)':
dependencies:
'@algolia/client-search': 5.49.1
algoliasearch: 5.49.1
'@algolia/client-abtesting@5.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
'@algolia/client-analytics@5.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
'@algolia/client-common@5.49.1': {}
'@algolia/client-insights@5.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
'@algolia/client-personalization@5.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
'@algolia/client-query-suggestions@5.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
'@algolia/client-search@5.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
'@algolia/ingestion@1.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
'@algolia/monitoring@1.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
'@algolia/recommend@5.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
'@algolia/requester-browser-xhr@5.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-fetch@5.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@algolia/requester-node-http@5.49.1':
dependencies:
'@algolia/client-common': 5.49.1
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
@@ -17281,6 +17500,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
'@ckeditor/ckeditor5-widget': 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-cloud-services@47.4.0':
dependencies:
@@ -17300,8 +17521,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-collaboration-core@47.4.0':
dependencies:
@@ -17501,6 +17720,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-inline@47.4.0':
dependencies:
@@ -17534,8 +17755,6 @@ snapshots:
'@ckeditor/ckeditor5-table': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-emoji@47.4.0':
dependencies:
@@ -17592,8 +17811,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-export-word@47.4.0':
dependencies:
@@ -17618,6 +17835,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-font@47.4.0':
dependencies:
@@ -17753,8 +17972,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-indent@47.4.0':
dependencies:
@@ -17878,8 +18095,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-merge-fields@47.4.0':
dependencies:
@@ -17892,8 +18107,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-minimap@47.4.0':
dependencies:
@@ -17902,8 +18115,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-operations-compressor@47.4.0':
dependencies:
@@ -17958,8 +18169,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-pagination@47.4.0':
dependencies:
@@ -18023,8 +18232,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-restricted-editing@47.4.0':
dependencies:
@@ -18069,8 +18276,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-slash-command@47.4.0':
dependencies:
@@ -18083,8 +18288,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-source-editing-enhanced@47.4.0':
dependencies:
@@ -18111,8 +18314,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-special-characters@47.4.0':
dependencies:
@@ -18134,8 +18335,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-table@47.4.0':
dependencies:
@@ -18148,8 +18347,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-template@47.4.0':
dependencies:
@@ -18260,8 +18457,6 @@ snapshots:
'@ckeditor/ckeditor5-engine': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-widget@47.4.0':
dependencies:
@@ -18281,8 +18476,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@codemirror/autocomplete@6.18.6':
dependencies:
@@ -25148,6 +25341,23 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
algoliasearch@5.49.1:
dependencies:
'@algolia/abtesting': 1.15.1
'@algolia/client-abtesting': 5.49.1
'@algolia/client-analytics': 5.49.1
'@algolia/client-common': 5.49.1
'@algolia/client-insights': 5.49.1
'@algolia/client-personalization': 5.49.1
'@algolia/client-query-suggestions': 5.49.1
'@algolia/client-search': 5.49.1
'@algolia/ingestion': 1.49.1
'@algolia/monitoring': 1.49.1
'@algolia/recommend': 5.49.1
'@algolia/requester-browser-xhr': 5.49.1
'@algolia/requester-fetch': 5.49.1
'@algolia/requester-node-http': 5.49.1
alien-signals@0.4.14: {}
amator@1.1.0:
@@ -28989,6 +29199,8 @@ snapshots:
readable-stream: 2.3.8
wbuf: 1.7.3
htm@3.1.1: {}
html-encoding-sniffer@2.0.1:
dependencies:
whatwg-encoding: 1.0.5
@@ -33520,6 +33732,8 @@ snapshots:
scule@1.3.0: {}
search-insights@2.17.3: {}
secure-compare@3.0.1: {}
selderee@0.11.0: