跳转到内容
Pivotick
Main Navigation 首页
快速入门
配置
API 参考
示例库项目在线体验

简体中文

English
Français
Deutsch
Português

简体中文

English
Français
Deutsch
Português

外观

Sidebar Navigation

快速入门

使用 Pivotick 的项目

配置

回调

边图层

简化

布局

渲染

模拟

自定义界面

侧栏

提示框

上下文菜单

筛选

图例

数据表格

界面样式

翻译界面

插件

Pivotick API

拓展与数据增强

保存拓展结果

撤销与历史记录

安全

API 文档

示例库

基础

节点样式

边样式

布局

事件与回调

界面自定义

编辑与创建

备注与注释

筛选、搜索与层级

编程控制

主题与性能

拓展与数据增强

综合示例

保留删除、编辑和创建 ​

当图是持久记录的视图时,没有写操作是乐观的。三个 before-hooks 使每个用户发起的编写一个消费者决定的提案,因此画布和数据库永远不会不一致:

  • onBeforeDelete — 一旦整个目标集被解析,每个删除功能(侧边栏的批量行、节点/边缘/注释上下文菜单条目)都会触发,包括 cascadingEdges:删除节点会破坏的关系。返回 false 否决,或 { accept: true, nodes, edges } 仅保留服务器实际删除的内容。 ctx.confirm()是内置确认模式。
  • onBeforeNodeCreate — 触发创建▸添加节点和画布菜单的在此处添加节点,以及图形空间position和promptData()形式。您返回的内容就是进入图表的内容。
  • onBeforeEdgeEditCommit — 提交边编辑时触发(右键单击边 ▸ 编辑边)。拒绝它,边缘会保留其数据。

所有三个手势都可以是异步的,当一个手势待处理时,第二个手势将被忽略。编程 graph.removeNode() / addNode() 永远不会被门控,因此您自己的代码不会被您自己的钩子阻塞。

尝试一下:选择节点并点击侧栏中的删除(已发布记录返回被拒绝),右键单击一条边以更改其关系,或使用创建▸添加节点。设置 editors.deletion.enabled: false 以完全删除删除功能,而不是否决每次点击。

js
const options = {
    // Label each attribute with its value, so an edit's effect is visible.
    render: {
        defaultNodeStyle: {
            size: 16,
            textColor: '#334155',
            text: (node) => node.getData()?.label,
            textVerticalShift: -1.6
        }
    },
    UI: {
        mode: 'full',
        // Off: `full` mode brings a minimap and the data dock, and this card is about
        // neither (see the Minimap and Data table cards for those).
        minimap: false,
        table: false,
        // Give the edge editor a relationship dropdown instead of a free-text field.
        editors: {
            edgeEditor: {
                fields: [{
                    key: 'label',
                    label: 'Relationship',
                    type: 'select',
                    options: RELATIONSHIPS.map((value) => ({ value, label: value }))
                }]
            }
        }
    },
    callbacks: {
        // Nothing leaves the model until the server agrees. The library resolves the
        // whole target set first — including `cascadingEdges`, the relationships that
        // removing a node would take with it — so you can persist the real consequence.
        onBeforeDelete: async ({ nodes, edges, cascadingEdges, origin, confirm }) => {
            const links = edges.length + cascadingEdges.length
            const ok = await confirm({
                title: 'Delete from event?',
                body: `${nodes.length} attribute(s) and ${links} relationship(s) will be `
                    + `deleted server-side. (asked from the ${origin})`,
                confirmLabel: 'Delete'
            })
            if (!ok) return false

            graph.notifier.info('Saving…', 'POST /events/delete')
            await wait(700) // a backend call that could fail

            // The server refused the published record: keep exactly what it deleted.
            const refused = nodes.filter((node) => node.getData().published)
            if (refused.length) {
                graph.notifier.warning('Kept', `${refused[0].getData().label} is published — not deleted.`)
            }
            return { accept: true, nodes: nodes.filter((node) => !node.getData().published) }
        },

        // Create ▸ Add node, or the canvas menu's "Add Node Here". `promptData` opens a
        // form; the id/data you return is what enters the graph.
        onBeforeNodeCreate: async ({ position, promptData }) => {
            const values = await promptData({
                title: 'New attribute',
                fields: [
                    { key: 'label', label: 'Value', type: 'text' },
                    {
                        key: 'type', label: 'Type', type: 'select', defaultValue: 'ip-src',
                        options: ['ip-src', 'domain', 'md5', 'url'].map((v) => ({ value: v, label: v }))
                    }
                ]
            })
            if (!values) return false // cancelled

            graph.notifier.info('Saving…', 'POST /attributes/add')
            await wait(500)
            graph.notifier.success('Created', `${values.label} at ${Math.round(position.x)},${Math.round(position.y)}`)
            return { accept: true, id: `attr-${values.label}`, data: values }
        },

        // Right-click an edge ▸ Edit Edge. The commit is a proposal too: refuse it and
        // the edge keeps its data, with the modal still open to correct.
        onBeforeEdgeEditCommit: async ({ previousData, nextData }) => {
            if (!RELATIONSHIPS.includes(nextData.label)) {
                graph.notifier.warning('Rejected', `"${nextData.label}" is not a known relationship.`)
                return false
            }
            graph.notifier.info('Saving…', 'PUT /object_references')
            await wait(500)
            graph.notifier.success('Updated', `${previousData.label} → ${nextData.label}`)
            return true
        }
    }
}
js
const data = {
    nodes: [
        { id: 'ip-8888', data: { label: '8.8.8.8', type: 'ip-src' } },
        { id: 'dom-evil', data: { label: 'evil.example.com', type: 'domain' } },
        { id: 'md5', data: { label: 'd41d8cd9…f8427e', type: 'md5' } },
        // The server refuses to delete this one — a published record.
        { id: 'file-obj', data: { label: 'invoice.doc', type: 'file', published: true } }
    ],
    edges: [
        { id: 'r1', from: 'file-obj', to: 'md5', data: { label: 'contains' } },
        { id: 'r2', from: 'dom-evil', to: 'ip-8888', data: { label: 'resolves-to' } }
    ]
}
Pager
下一页快速入门