mirror of
https://github.com/zadam/trilium.git
synced 2026-03-09 21:50:24 +01:00
Compare commits
21 Commits
main
...
autocomple
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6e0df70d7 | ||
|
|
0ef7dd8fc2 | ||
|
|
bd1f6b7a0f | ||
|
|
cc24c2a4dc | ||
|
|
eaa6da73ca | ||
|
|
84ea5d3401 | ||
|
|
65095713cc | ||
|
|
8f6b565673 | ||
|
|
c0dd59458b | ||
|
|
d363d2016e | ||
|
|
702df56a8a | ||
|
|
91677eb7da | ||
|
|
e74f619d06 | ||
|
|
1c79beaa57 | ||
|
|
42a2b1bc13 | ||
|
|
8393b916c1 | ||
|
|
656796d18d | ||
|
|
0fe00b77cf | ||
|
|
150f88cc56 | ||
|
|
1b013e6888 | ||
|
|
2dfc4514b0 |
272
.agents/migration_plan_autocomplete.md
Normal file
272
.agents/migration_plan_autocomplete.md
Normal 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")` 关闭所有实例,新库需要手动维护实例注册表。
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
278
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user