@knotx/plugins-guideline 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 格桑
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,294 @@
1
+ # @knotx/plugins-guideline
2
+
3
+ English | [中文](./README.md)
4
+
5
+ Guideline plugin that provides alignment hints for node dragging in KnotX.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @knotx/plugins-guideline
11
+ # or
12
+ pnpm add @knotx/plugins-guideline
13
+ # or
14
+ yarn add @knotx/plugins-guideline
15
+ ```
16
+
17
+ ## Overview
18
+
19
+ The Guideline plugin provides intelligent node alignment assistance for KnotX, displaying alignment guidelines in real-time during node dragging. This plugin supports multiple alignment methods, automatic snapping functionality, and delivers excellent performance and user experience.
20
+
21
+ ## Features
22
+
23
+ - 🎯 **Smart Alignment Detection** - Real-time detection of alignment relationships between nodes
24
+ - 📏 **Multiple Alignment Types** - Support for left, right, center, top, bottom alignment
25
+ - 🧲 **Auto-Snap Functionality** - Automatically snap nodes to precise positions when close to alignment
26
+ - ⚡ **Performance Optimized** - Guidelines calculated and rendered only during dragging
27
+ - 🎨 **Customizable** - Support for custom colors, thresholds, and other configurations
28
+ - 🔄 **Reactive Updates** - Reactive state management based on RxJS
29
+
30
+ ## Implementation Principle
31
+
32
+ The core implementation principles of the Guideline plugin:
33
+
34
+ 1. **Event Listening**: Listens to node `draftOperation` events to capture position changes during dragging
35
+ 2. **Distance Calculation**: Real-time calculation of distance relationships between dragged nodes and other nodes
36
+ 3. **Alignment Detection**: Detects various alignment possibilities within the set threshold range
37
+ 4. **Guideline Generation**: Generates corresponding vertical and horizontal guidelines based on detection results
38
+ 5. **Auto-Snap**: When snap functionality is enabled, automatically adjusts node positions to precise alignment positions
39
+ 6. **Render Optimization**: Renders guidelines on the foreground layer to ensure correct visual hierarchy
40
+
41
+ ## Dependencies
42
+
43
+ ### Core Dependencies
44
+ - `@knotx/core`: Provides base plugin architecture and data management
45
+ - `@knotx/decorators`: Provides decorator support
46
+ - `rxjs`: Provides reactive programming support
47
+
48
+ ## API Documentation
49
+
50
+ ### Main Classes
51
+
52
+ #### Guideline
53
+
54
+ The main class of the Guideline plugin, extending `BasePlugin`.
55
+
56
+ ```typescript
57
+ export class Guideline extends BasePlugin {
58
+ name = 'guideline' as const
59
+ }
60
+ ```
61
+
62
+ ### Configuration Options
63
+
64
+ #### GuidelineConfig
65
+
66
+ ```typescript
67
+ export interface GuidelineConfig {
68
+ /** Alignment threshold (pixels), guidelines will be displayed when nodes are within this range */
69
+ threshold?: number
70
+ /** Guideline color */
71
+ color?: string
72
+ /** Guideline width (pixels) */
73
+ lineWidth?: number
74
+ /** Whether to enable the plugin */
75
+ enabled?: boolean
76
+ /** Whether to enable auto-snap functionality */
77
+ enableSnap?: boolean
78
+ }
79
+ ```
80
+
81
+ ### Type Definitions
82
+
83
+ #### GuidelineItem
84
+
85
+ ```typescript
86
+ interface GuidelineItem {
87
+ /** Guideline type */
88
+ type: 'vertical' | 'horizontal'
89
+ /** Guideline position coordinate */
90
+ position: number
91
+ /** Optional label information */
92
+ label?: string
93
+ }
94
+ ```
95
+
96
+ ## Usage Examples
97
+
98
+ ### Basic Usage
99
+
100
+ ```typescript
101
+ import { Engine } from '@knotx/core'
102
+ import { Guideline } from '@knotx/plugins-guideline'
103
+
104
+ const engine = new Engine({
105
+ container: { width: 800, height: 600, direction: 'horizontal' },
106
+ plugins: [
107
+ Guideline,
108
+ // Other plugins...
109
+ ],
110
+ })
111
+ ```
112
+
113
+ ### Custom Configuration
114
+
115
+ ```typescript
116
+ const engine = new Engine({
117
+ plugins: [Guideline],
118
+ pluginConfig: {
119
+ guideline: {
120
+ threshold: 5,
121
+ color: '#007ACC',
122
+ lineWidth: 1,
123
+ enabled: true,
124
+ enableSnap: true,
125
+ },
126
+ },
127
+ })
128
+ ```
129
+
130
+ ### Integration with Drag Plugin
131
+
132
+ ```typescript
133
+ import { Drag } from '@knotx/plugins-drag'
134
+ import { Guideline } from '@knotx/plugins-guideline'
135
+
136
+ const engine = new Engine({
137
+ plugins: [Drag, Guideline],
138
+ pluginConfig: {
139
+ guideline: {
140
+ threshold: 8,
141
+ color: '#FF6B6B',
142
+ enableSnap: true,
143
+ },
144
+ },
145
+ })
146
+ ```
147
+
148
+ ### Dynamic Configuration
149
+
150
+ ```typescript
151
+ const engine = new Engine({
152
+ plugins: [Guideline],
153
+ })
154
+
155
+ // Modify configuration at runtime
156
+ engine.updatePluginConfig('guideline', {
157
+ threshold: 10,
158
+ color: '#4ECDC4',
159
+ enableSnap: false,
160
+ })
161
+ ```
162
+
163
+ ## Configuration Options
164
+
165
+ | Option | Type | Default | Description |
166
+ |--------|------|---------|-------------|
167
+ | `threshold` | `number` | `5` | Alignment threshold (pixels), guidelines will be displayed when nodes are within this range |
168
+ | `color` | `string` | `'#007ACC'` | Guideline color |
169
+ | `lineWidth` | `number` | `1` | Guideline width (pixels) |
170
+ | `enabled` | `boolean` | `true` | Whether to enable the plugin |
171
+ | `enableSnap` | `boolean` | `true` | Whether to enable auto-snap functionality |
172
+
173
+ ## Alignment Types
174
+
175
+ The plugin supports the following alignment types:
176
+
177
+ ### Vertical Alignment
178
+ - **Left Alignment** - Align node left edges
179
+ - **Right Alignment** - Align node right edges
180
+ - **Center Alignment** - Align node horizontal centers
181
+ - **Edge Alignment** - Align one node's edge with another node's opposite edge
182
+
183
+ ### Horizontal Alignment
184
+ - **Top Alignment** - Align node top edges
185
+ - **Bottom Alignment** - Align node bottom edges
186
+ - **Center Alignment** - Align node vertical centers
187
+ - **Edge Alignment** - Align one node's edge with another node's opposite edge
188
+
189
+ ## How It Works
190
+
191
+ 1. Listen to node `draftOperation` events
192
+ 2. When node position changes, calculate distances to other nodes
193
+ 3. Generate corresponding guidelines when within threshold range
194
+ 4. **Snap Feature**: If enabled, automatically adjust node position to the nearest alignment position
195
+ 5. Render guidelines on the `Layer.Foreground` layer
196
+ 6. Automatically clear guidelines when dragging ends
197
+
198
+ ## Advanced Features
199
+
200
+ ### Custom Guideline Styling
201
+
202
+ ```typescript
203
+ class CustomGuidelinePlugin extends BasePlugin {
204
+ @OnInit
205
+ init() {
206
+ // Customize guideline rendering style through inheritance or configuration
207
+ const config = this.engine.getPluginConfig('guideline')
208
+ config.color = this.getThemeColor()
209
+ config.lineWidth = this.getLineWidth()
210
+ }
211
+
212
+ private getThemeColor(): string {
213
+ // Return different colors based on theme
214
+ return document.body.classList.contains('dark-theme') ? '#64FFDA' : '#007ACC'
215
+ }
216
+
217
+ private getLineWidth(): number {
218
+ // Adjust line width based on canvas zoom level
219
+ const scale = this.engine.getPluginData('canvas')?.transform?.scale || 1
220
+ return Math.max(1, Math.round(2 / scale))
221
+ }
222
+ }
223
+ ```
224
+
225
+ ### Performance Optimization Configuration
226
+
227
+ ```typescript
228
+ const engine = new Engine({
229
+ plugins: [Guideline],
230
+ pluginConfig: {
231
+ guideline: {
232
+ // Increase threshold appropriately to reduce computation in large node scenarios
233
+ threshold: 10,
234
+ // Disable auto-snap to improve performance
235
+ enableSnap: false,
236
+ },
237
+ },
238
+ })
239
+ ```
240
+
241
+ ## File Directory Structure
242
+
243
+ ```
244
+ packages/plugins-guideline/
245
+ ├── src/
246
+ │ ├── index.ts # Export file
247
+ │ └── plugin.tsx # Main implementation file
248
+ ├── dist/ # Build output directory
249
+ ├── package.json # Package configuration
250
+ ├── build.config.ts # Build configuration
251
+ ├── tsconfig.json # TypeScript configuration
252
+ ├── eslint.config.mjs # ESLint configuration
253
+ └── CHANGELOG.md # Changelog
254
+ ```
255
+
256
+ ### Core Files Description
257
+
258
+ - **plugin.tsx**: Contains the main implementation of the Guideline plugin, including alignment detection algorithms, guideline rendering, and auto-snap functionality
259
+ - **index.ts**: Exports the Guideline class and related type definitions
260
+
261
+ ## Best Practices
262
+
263
+ ### Performance Optimization
264
+
265
+ 1. **Set Reasonable Threshold**: Adjust the `threshold` value based on usage scenarios; too small thresholds increase computational burden
266
+ 2. **Enable Snap As Needed**: Enable `enableSnap` in scenarios where precise positioning is important, otherwise disable it to improve performance
267
+ 3. **Consider Node Count**: In scenarios with many nodes, consider appropriately increasing the threshold or optimizing rendering strategy
268
+
269
+ ### User Experience
270
+
271
+ 1. **Visual Consistency**: Ensure guideline colors are consistent with the overall design style
272
+ 2. **Responsive Configuration**: Dynamically adjust line width and threshold based on canvas zoom level
273
+ 3. **Progressive Enhancement**: Consider disabling or simplifying guideline functionality on mobile devices
274
+
275
+ ### Integration Recommendations
276
+
277
+ 1. **Pair with Drag Plugin**: Recommended to use with `@knotx/plugins-drag` plugin for the best experience
278
+ 2. **Theme Adaptation**: Dynamically adjust guideline colors when supporting light/dark theme switching
279
+ 3. **Keyboard Support**: Consider adding keyboard shortcuts to temporarily disable/enable guideline functionality
280
+
281
+ ## Examples
282
+
283
+ Check the example code in `apps/playground/src/examples/basic/guideline.tsx` to learn how to use the guideline plugin.
284
+
285
+ ## Notes
286
+
287
+ 1. **Performance Considerations**: Frequent alignment calculations in scenarios with many nodes may impact performance
288
+ 2. **Event Priority**: Ensure correct event handling order with other interaction plugins
289
+ 3. **Memory Management**: The plugin automatically cleans up unneeded guidelines; no manual management required
290
+ 4. **Browser Compatibility**: Uses modern browser features; ensure target browsers support them
291
+
292
+ ## License
293
+
294
+ MIT © boenfu
package/README.md ADDED
@@ -0,0 +1,294 @@
1
+ # @knotx/plugins-guideline
2
+
3
+ [English](./README.en.md) | 中文
4
+
5
+ 辅助线插件,为 KnotX 提供节点拖拽时的对齐提示功能。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install @knotx/plugins-guideline
11
+ # or
12
+ pnpm add @knotx/plugins-guideline
13
+ # or
14
+ yarn add @knotx/plugins-guideline
15
+ ```
16
+
17
+ ## 概述
18
+
19
+ Guideline 插件为 KnotX 提供了智能的节点对齐辅助功能,在节点拖拽过程中实时检测并显示对齐辅助线。该插件支持多种对齐方式、自动吸附功能,并提供了优秀的性能和用户体验。
20
+
21
+ ## 功能特性
22
+
23
+ - 🎯 **智能对齐检测** - 实时检测节点之间的对齐关系
24
+ - 📏 **多种对齐方式** - 支持左、右、中心、顶部、底部对齐
25
+ - 🧲 **自动吸附功能** - 节点接近对齐位置时自动吸附到精确位置
26
+ - ⚡ **性能优化** - 仅在拖拽时计算和渲染辅助线
27
+ - 🎨 **可自定义** - 支持自定义颜色、阈值等配置
28
+ - 🔄 **响应式更新** - 基于 RxJS 实现响应式的状态管理
29
+
30
+ ## 实现原理
31
+
32
+ Guideline 插件的核心实现原理:
33
+
34
+ 1. **事件监听**:监听节点的 `draftOperation` 事件,获取拖拽中的节点位置变化
35
+ 2. **距离计算**:实时计算拖拽节点与其他节点之间的距离关系
36
+ 3. **对齐检测**:在设定的阈值范围内检测各种对齐可能性
37
+ 4. **辅助线生成**:根据检测结果生成相应的垂直和水平辅助线
38
+ 5. **自动吸附**:当启用 snap 功能时,自动调整节点位置到精确的对齐位置
39
+ 6. **渲染优化**:在前景层渲染辅助线,确保视觉层级正确
40
+
41
+ ## 依赖关系
42
+
43
+ ### 核心依赖
44
+ - `@knotx/core`:提供基础插件架构和数据管理
45
+ - `@knotx/decorators`:提供装饰器支持
46
+ - `rxjs`:提供响应式编程支持
47
+
48
+ ## API 文档
49
+
50
+ ### 主要类
51
+
52
+ #### Guideline
53
+
54
+ 辅助线插件的主要类,继承自 `BasePlugin`。
55
+
56
+ ```typescript
57
+ export class Guideline extends BasePlugin {
58
+ name = 'guideline' as const
59
+ }
60
+ ```
61
+
62
+ ### 配置选项
63
+
64
+ #### GuidelineConfig
65
+
66
+ ```typescript
67
+ export interface GuidelineConfig {
68
+ /** 对齐阈值(像素),节点在此范围内将显示辅助线 */
69
+ threshold?: number
70
+ /** 辅助线颜色 */
71
+ color?: string
72
+ /** 辅助线宽度(像素) */
73
+ lineWidth?: number
74
+ /** 是否启用插件 */
75
+ enabled?: boolean
76
+ /** 是否启用自动吸附功能 */
77
+ enableSnap?: boolean
78
+ }
79
+ ```
80
+
81
+ ### 类型定义
82
+
83
+ #### GuidelineItem
84
+
85
+ ```typescript
86
+ interface GuidelineItem {
87
+ /** 辅助线类型 */
88
+ type: 'vertical' | 'horizontal'
89
+ /** 辅助线位置坐标 */
90
+ position: number
91
+ /** 可选的标签信息 */
92
+ label?: string
93
+ }
94
+ ```
95
+
96
+ ## 使用示例
97
+
98
+ ### 基本用法
99
+
100
+ ```typescript
101
+ import { Engine } from '@knotx/core'
102
+ import { Guideline } from '@knotx/plugins-guideline'
103
+
104
+ const engine = new Engine({
105
+ container: { width: 800, height: 600, direction: 'horizontal' },
106
+ plugins: [
107
+ Guideline,
108
+ // 其他插件...
109
+ ],
110
+ })
111
+ ```
112
+
113
+ ### 自定义配置
114
+
115
+ ```typescript
116
+ const engine = new Engine({
117
+ plugins: [Guideline],
118
+ pluginConfig: {
119
+ guideline: {
120
+ threshold: 5,
121
+ color: '#007ACC',
122
+ lineWidth: 1,
123
+ enabled: true,
124
+ enableSnap: true,
125
+ },
126
+ },
127
+ })
128
+ ```
129
+
130
+ ### 与拖拽插件配合使用
131
+
132
+ ```typescript
133
+ import { Drag } from '@knotx/plugins-drag'
134
+ import { Guideline } from '@knotx/plugins-guideline'
135
+
136
+ const engine = new Engine({
137
+ plugins: [Drag, Guideline],
138
+ pluginConfig: {
139
+ guideline: {
140
+ threshold: 8,
141
+ color: '#FF6B6B',
142
+ enableSnap: true,
143
+ },
144
+ },
145
+ })
146
+ ```
147
+
148
+ ### 动态配置
149
+
150
+ ```typescript
151
+ const engine = new Engine({
152
+ plugins: [Guideline],
153
+ })
154
+
155
+ // 运行时修改配置
156
+ engine.updatePluginConfig('guideline', {
157
+ threshold: 10,
158
+ color: '#4ECDC4',
159
+ enableSnap: false,
160
+ })
161
+ ```
162
+
163
+ ## 配置选项
164
+
165
+ | 选项 | 类型 | 默认值 | 描述 |
166
+ |------|------|--------|------|
167
+ | `threshold` | `number` | `5` | 对齐阈值(像素),节点在此范围内将显示辅助线 |
168
+ | `color` | `string` | `'#007ACC'` | 辅助线颜色 |
169
+ | `lineWidth` | `number` | `1` | 辅助线宽度(像素) |
170
+ | `enabled` | `boolean` | `true` | 是否启用插件 |
171
+ | `enableSnap` | `boolean` | `true` | 是否启用自动吸附功能 |
172
+
173
+ ## 对齐类型
174
+
175
+ 插件支持以下对齐类型:
176
+
177
+ ### 垂直对齐
178
+ - **左边对齐** - 节点左边缘对齐
179
+ - **右边对齐** - 节点右边缘对齐
180
+ - **中心对齐** - 节点水平中心对齐
181
+ - **边缘对齐** - 一个节点的边缘与另一个节点的相对边缘对齐
182
+
183
+ ### 水平对齐
184
+ - **顶部对齐** - 节点顶部对齐
185
+ - **底部对齐** - 节点底部对齐
186
+ - **中心对齐** - 节点垂直中心对齐
187
+ - **边缘对齐** - 一个节点的边缘与另一个节点的相对边缘对齐
188
+
189
+ ## 工作原理
190
+
191
+ 1. 监听节点的 `draftOperation` 事件
192
+ 2. 当节点位置发生变化时,计算与其他节点的距离
193
+ 3. 在阈值范围内时,生成相应的辅助线
194
+ 4. **Snap功能**:如果启用,自动将节点位置调整到最近的对齐位置
195
+ 5. 在 `Layer.Foreground` 层渲染辅助线
196
+ 6. 拖拽结束时自动清除辅助线
197
+
198
+ ## 高级功能
199
+
200
+ ### 自定义辅助线样式
201
+
202
+ ```typescript
203
+ class CustomGuidelinePlugin extends BasePlugin {
204
+ @OnInit
205
+ init() {
206
+ // 可以通过继承或配置来自定义辅助线的渲染样式
207
+ const config = this.engine.getPluginConfig('guideline')
208
+ config.color = this.getThemeColor()
209
+ config.lineWidth = this.getLineWidth()
210
+ }
211
+
212
+ private getThemeColor(): string {
213
+ // 根据主题返回不同的颜色
214
+ return document.body.classList.contains('dark-theme') ? '#64FFDA' : '#007ACC'
215
+ }
216
+
217
+ private getLineWidth(): number {
218
+ // 根据画布缩放级别调整线宽
219
+ const scale = this.engine.getPluginData('canvas')?.transform?.scale || 1
220
+ return Math.max(1, Math.round(2 / scale))
221
+ }
222
+ }
223
+ ```
224
+
225
+ ### 性能优化配置
226
+
227
+ ```typescript
228
+ const engine = new Engine({
229
+ plugins: [Guideline],
230
+ pluginConfig: {
231
+ guideline: {
232
+ // 在大量节点场景下,适当增加阈值以减少计算
233
+ threshold: 10,
234
+ // 禁用自动吸附以提升性能
235
+ enableSnap: false,
236
+ },
237
+ },
238
+ })
239
+ ```
240
+
241
+ ## 文件目录结构
242
+
243
+ ```
244
+ packages/plugins-guideline/
245
+ ├── src/
246
+ │ ├── index.ts # 导出文件
247
+ │ └── plugin.tsx # 主要实现文件
248
+ ├── dist/ # 构建输出目录
249
+ ├── package.json # 包配置文件
250
+ ├── build.config.ts # 构建配置
251
+ ├── tsconfig.json # TypeScript 配置
252
+ ├── eslint.config.mjs # ESLint 配置
253
+ └── CHANGELOG.md # 更新日志
254
+ ```
255
+
256
+ ### 核心文件说明
257
+
258
+ - **plugin.tsx**:包含 Guideline 插件的主要实现,包括对齐检测算法、辅助线渲染和自动吸附功能
259
+ - **index.ts**:导出 Guideline 类和相关类型定义
260
+
261
+ ## 最佳实践
262
+
263
+ ### 性能优化
264
+
265
+ 1. **合理设置阈值**:根据使用场景调整 `threshold` 值,过小的阈值会增加计算负担
266
+ 2. **按需启用吸附**:在精确定位重要的场景启用 `enableSnap`,否则可以禁用以提升性能
267
+ 3. **节点数量考虑**:在节点数量较多的场景下,考虑适当增加阈值或优化渲染策略
268
+
269
+ ### 用户体验
270
+
271
+ 1. **视觉一致性**:确保辅助线颜色与整体设计风格保持一致
272
+ 2. **响应性配置**:根据画布缩放级别动态调整线宽和阈值
273
+ 3. **渐进式增强**:在移动设备上可以考虑禁用或简化辅助线功能
274
+
275
+ ### 集成建议
276
+
277
+ 1. **与拖拽插件配合**:建议与 `@knotx/plugins-drag` 插件一起使用以获得最佳体验
278
+ 2. **主题适配**:支持明暗主题切换时,动态调整辅助线颜色
279
+ 3. **快捷键支持**:可以考虑添加快捷键来临时禁用/启用辅助线功能
280
+
281
+ ## 示例
282
+
283
+ 查看 `apps/playground/src/examples/basic/guideline.tsx` 中的示例代码,了解如何使用辅助线插件。
284
+
285
+ ## 注意事项
286
+
287
+ 1. **性能考虑**:在大量节点的场景下,频繁的对齐计算可能影响性能
288
+ 2. **事件优先级**:确保与其他交互插件的事件处理顺序正确
289
+ 3. **内存管理**:插件会自动清理不再需要的辅助线,无需手动管理
290
+ 4. **浏览器兼容性**:使用了现代浏览器特性,确保目标浏览器支持
291
+
292
+ ## 许可证
293
+
294
+ MIT © boenfu
package/dist/index.cjs ADDED
@@ -0,0 +1,262 @@
1
+ 'use strict';
2
+
3
+ const jsxRuntime = require('@knotx/jsx/jsx-runtime');
4
+ const core = require('@knotx/core');
5
+ const decorators = require('@knotx/decorators');
6
+ const operators = require('rxjs/operators');
7
+
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
12
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
13
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
14
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
15
+ var __typeError = (msg) => {
16
+ throw TypeError(msg);
17
+ };
18
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
19
+ var __spreadValues = (a, b) => {
20
+ for (var prop in b || (b = {}))
21
+ if (__hasOwnProp.call(b, prop))
22
+ __defNormalProp(a, prop, b[prop]);
23
+ if (__getOwnPropSymbols)
24
+ for (var prop of __getOwnPropSymbols(b)) {
25
+ if (__propIsEnum.call(b, prop))
26
+ __defNormalProp(a, prop, b[prop]);
27
+ }
28
+ return a;
29
+ };
30
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
31
+ var __decoratorStart = (base) => {
32
+ var _a2;
33
+ return [, , , __create((_a2 = base == null ? void 0 : base[__knownSymbol("metadata")]) != null ? _a2 : null)];
34
+ };
35
+ var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
36
+ var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
37
+ var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
38
+ var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
39
+ var __runInitializers = (array, flags, self, value) => {
40
+ for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
41
+ return value;
42
+ };
43
+ var __decorateElement = (array, flags, name, decorators, target, extra) => {
44
+ var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
45
+ var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
46
+ var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
47
+ var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
48
+ return __privateGet(this, extra);
49
+ }, set [name](x) {
50
+ return __privateSet(this, extra, x);
51
+ } }, name));
52
+ k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
53
+ for (var i = decorators.length - 1; i >= 0; i--) {
54
+ ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
55
+ if (k) {
56
+ ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
57
+ if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
58
+ if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
59
+ }
60
+ it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
61
+ if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
62
+ else if (typeof it !== "object" || it === null) __typeError("Object expected");
63
+ else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
64
+ }
65
+ return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
66
+ };
67
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
68
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
69
+ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
70
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
71
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
72
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
73
+ var _render_dec, _init_dec, _draggingNodeId_dec, _guidelines_dec, _transform_dec, _addNodePipe_dec, _getNode_dec, _container_dec, _nodes_dec, _a, _init;
74
+ class Guideline extends (_a = core.BasePlugin, _nodes_dec = [decorators.inject.nodes()], _container_dec = [decorators.inject.container()], _getNode_dec = [decorators.inject.getNode()], _addNodePipe_dec = [decorators.inject.addNodePipe()], _transform_dec = [decorators.inject.canvas.transform()], _guidelines_dec = [decorators.observable()], _draggingNodeId_dec = [decorators.observable()], _init_dec = [decorators.OnInit], _render_dec = [decorators.layer(core.Layer.Foreground)], _a) {
75
+ constructor() {
76
+ super(...arguments);
77
+ __runInitializers(_init, 5, this);
78
+ __publicField(this, "name", "guideline");
79
+ __publicField(this, "config", {
80
+ threshold: 4,
81
+ color: "#007ACC",
82
+ lineWidth: 1,
83
+ enabled: true,
84
+ enableSnap: true
85
+ });
86
+ __publicField(this, "nodes", __runInitializers(_init, 8, this, [])), __runInitializers(_init, 11, this);
87
+ __publicField(this, "container", __runInitializers(_init, 12, this)), __runInitializers(_init, 15, this);
88
+ __publicField(this, "getNode", __runInitializers(_init, 16, this)), __runInitializers(_init, 19, this);
89
+ __publicField(this, "addNodePipe", __runInitializers(_init, 20, this)), __runInitializers(_init, 23, this);
90
+ __publicField(this, "transform", __runInitializers(_init, 24, this)), __runInitializers(_init, 27, this);
91
+ __publicField(this, "guidelines", __runInitializers(_init, 28, this, [])), __runInitializers(_init, 31, this);
92
+ __publicField(this, "draggingNodeId", __runInitializers(_init, 32, this, null)), __runInitializers(_init, 35, this);
93
+ }
94
+ init(config = {}) {
95
+ this.config = __spreadValues(__spreadValues({}, this.config), config);
96
+ this.subscriptions.push(this.addNodePipe({
97
+ transform: () => (operation$) => {
98
+ return operation$.pipe(
99
+ operators.tap((operation) => {
100
+ if (core.isDraftOperation(operation) && operation.type === "draftOperation") {
101
+ this.handleDraftOperation(operation);
102
+ } else if (operation.type === "commitDraft" || operation.type === "discardDraft") {
103
+ this.clearGuidelines();
104
+ }
105
+ })
106
+ );
107
+ }
108
+ }));
109
+ }
110
+ handleDraftOperation(operation) {
111
+ if (!this.config.enabled) {
112
+ return;
113
+ }
114
+ if (operation.operation.type === "update" && operation.operation.data.position) {
115
+ const nodeId = operation.operation.id;
116
+ const newPosition = operation.operation.data.position;
117
+ this.draggingNodeId = nodeId;
118
+ const snapResult = this.calculateGuidelines(nodeId, newPosition);
119
+ if (this.config.enableSnap && snapResult.snapPosition) {
120
+ if (snapResult.snapPosition.x !== void 0) {
121
+ operation.operation.data.position.x = snapResult.snapPosition.x;
122
+ }
123
+ if (snapResult.snapPosition.y !== void 0) {
124
+ operation.operation.data.position.y = snapResult.snapPosition.y;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ calculateGuidelines(draggingNodeId, newPosition) {
130
+ var _a2, _b, _c;
131
+ const guidelines = [];
132
+ const threshold = (_a2 = this.config.threshold) != null ? _a2 : 5;
133
+ const snapPosition = {};
134
+ const draggingNode = this.getNode({ id: draggingNodeId });
135
+ if (!draggingNode)
136
+ return { guidelines: [] };
137
+ const draggingWidth = ((_b = draggingNode.measured) == null ? void 0 : _b.width) || 0;
138
+ const draggingHeight = ((_c = draggingNode.measured) == null ? void 0 : _c.height) || 0;
139
+ const draggingLeft = newPosition.x;
140
+ const draggingRight = newPosition.x + draggingWidth;
141
+ const draggingCenterX = newPosition.x + draggingWidth / 2;
142
+ const draggingTop = newPosition.y;
143
+ const draggingBottom = newPosition.y + draggingHeight;
144
+ const draggingCenterY = newPosition.y + draggingHeight / 2;
145
+ let nearestXSnap;
146
+ let nearestYSnap;
147
+ this.nodes.forEach((node) => {
148
+ var _a3, _b2, _c2, _d;
149
+ if (node.id === draggingNodeId)
150
+ return;
151
+ const nodeLeft = node.position.x;
152
+ const nodeRight = node.position.x + (((_a3 = node.measured) == null ? void 0 : _a3.width) || 0);
153
+ const nodeCenterX = node.position.x + (((_b2 = node.measured) == null ? void 0 : _b2.width) || 0) / 2;
154
+ const nodeTop = node.position.y;
155
+ const nodeBottom = node.position.y + (((_c2 = node.measured) == null ? void 0 : _c2.height) || 0);
156
+ const nodeCenterY = node.position.y + (((_d = node.measured) == null ? void 0 : _d.height) || 0) / 2;
157
+ const checkVerticalSnap = (targetX, label, draggedX) => {
158
+ const distance = Math.abs(draggedX - targetX);
159
+ if (distance <= threshold) {
160
+ guidelines.push({
161
+ type: "vertical",
162
+ position: targetX,
163
+ label
164
+ });
165
+ const newX = targetX - (draggedX - newPosition.x);
166
+ if (!nearestXSnap || distance < nearestXSnap.distance) {
167
+ nearestXSnap = { distance, position: targetX, newX };
168
+ }
169
+ }
170
+ };
171
+ const checkHorizontalSnap = (targetY, label, draggedY) => {
172
+ const distance = Math.abs(draggedY - targetY);
173
+ if (distance <= threshold) {
174
+ guidelines.push({
175
+ type: "horizontal",
176
+ position: targetY,
177
+ label
178
+ });
179
+ const newY = targetY - (draggedY - newPosition.y);
180
+ if (!nearestYSnap || distance < nearestYSnap.distance) {
181
+ nearestYSnap = { distance, position: targetY, newY };
182
+ }
183
+ }
184
+ };
185
+ checkVerticalSnap(nodeLeft, "left", draggingLeft);
186
+ checkVerticalSnap(nodeRight, "right", draggingRight);
187
+ checkVerticalSnap(nodeCenterX, "center", draggingCenterX);
188
+ checkVerticalSnap(nodeRight, "edge", draggingLeft);
189
+ checkVerticalSnap(nodeLeft, "edge", draggingRight);
190
+ checkHorizontalSnap(nodeTop, "top", draggingTop);
191
+ checkHorizontalSnap(nodeBottom, "bottom", draggingBottom);
192
+ checkHorizontalSnap(nodeCenterY, "center", draggingCenterY);
193
+ checkHorizontalSnap(nodeBottom, "edge", draggingTop);
194
+ checkHorizontalSnap(nodeTop, "edge", draggingBottom);
195
+ });
196
+ const uniqueGuidelines = guidelines.filter((guideline, index) => {
197
+ return guidelines.findIndex(
198
+ (g) => g.type === guideline.type && Math.abs(g.position - guideline.position) < 1
199
+ ) === index;
200
+ });
201
+ if (nearestXSnap) {
202
+ snapPosition.x = nearestXSnap.newX;
203
+ }
204
+ if (nearestYSnap) {
205
+ snapPosition.y = nearestYSnap.newY;
206
+ }
207
+ this.guidelines = uniqueGuidelines;
208
+ return {
209
+ guidelines: uniqueGuidelines,
210
+ snapPosition: Object.keys(snapPosition).length > 0 ? snapPosition : void 0
211
+ };
212
+ }
213
+ clearGuidelines() {
214
+ this.guidelines = [];
215
+ this.draggingNodeId = null;
216
+ }
217
+ render() {
218
+ const { guidelines, transform, config } = core.use(() => ({
219
+ guidelines: this.guidelines,
220
+ transform: this.transform,
221
+ config: this.config
222
+ }));
223
+ if (!config.enabled || guidelines.length === 0) {
224
+ return null;
225
+ }
226
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: core.bem("guideline", "container"), children: guidelines.map((guideline, index) => /* @__PURE__ */ jsxRuntime.jsx(
227
+ "div",
228
+ {
229
+ className: core.bem("guideline", "line", guideline.type),
230
+ style: __spreadValues({
231
+ position: "absolute",
232
+ backgroundColor: config.color,
233
+ pointerEvents: "none"
234
+ }, guideline.type === "vertical" ? {
235
+ left: guideline.position * transform.scale + transform.positionX,
236
+ top: 0,
237
+ width: config.lineWidth,
238
+ height: this.container.height
239
+ } : {
240
+ left: 0,
241
+ top: guideline.position * transform.scale + transform.positionY,
242
+ width: this.container.width,
243
+ height: config.lineWidth
244
+ })
245
+ },
246
+ `${guideline.type}-${guideline.position}-${index}`
247
+ )) });
248
+ }
249
+ }
250
+ _init = __decoratorStart(_a);
251
+ __decorateElement(_init, 1, "init", _init_dec, Guideline);
252
+ __decorateElement(_init, 1, "render", _render_dec, Guideline);
253
+ __decorateElement(_init, 5, "nodes", _nodes_dec, Guideline);
254
+ __decorateElement(_init, 5, "container", _container_dec, Guideline);
255
+ __decorateElement(_init, 5, "getNode", _getNode_dec, Guideline);
256
+ __decorateElement(_init, 5, "addNodePipe", _addNodePipe_dec, Guideline);
257
+ __decorateElement(_init, 5, "transform", _transform_dec, Guideline);
258
+ __decorateElement(_init, 5, "guidelines", _guidelines_dec, Guideline);
259
+ __decorateElement(_init, 5, "draggingNodeId", _draggingNodeId_dec, Guideline);
260
+ __decoratorMetadata(_init, Guideline);
261
+
262
+ exports.Guideline = Guideline;
@@ -0,0 +1,60 @@
1
+ import { BasePlugin } from '@knotx/core';
2
+
3
+ interface GuidelineConfig {
4
+ /**
5
+ * 对齐阈值(像素)
6
+ * @default 4
7
+ */
8
+ threshold?: number;
9
+ /**
10
+ * 辅助线颜色
11
+ * @default '#007ACC'
12
+ */
13
+ color?: string;
14
+ /**
15
+ * 辅助线宽度
16
+ * @default 1
17
+ */
18
+ lineWidth?: number;
19
+ /**
20
+ * 是否启用
21
+ * @default true
22
+ */
23
+ enabled?: boolean;
24
+ /**
25
+ * 是否启用自动吸附
26
+ * @default true
27
+ */
28
+ enableSnap?: boolean;
29
+ }
30
+ interface GuidelineItem {
31
+ type: 'vertical' | 'horizontal';
32
+ position: number;
33
+ label?: string;
34
+ }
35
+ declare module '@knotx/core' {
36
+ interface PluginData {
37
+ guideline: {
38
+ guidelines: GuidelineItem[];
39
+ enabled: boolean;
40
+ };
41
+ }
42
+ }
43
+ declare class Guideline extends BasePlugin<'guideline', GuidelineConfig> {
44
+ name: "guideline";
45
+ private config;
46
+ private nodes;
47
+ private container;
48
+ private getNode;
49
+ private addNodePipe;
50
+ private transform;
51
+ guidelines: GuidelineItem[];
52
+ draggingNodeId: string | null;
53
+ init(config?: GuidelineConfig): void;
54
+ private handleDraftOperation;
55
+ private calculateGuidelines;
56
+ private clearGuidelines;
57
+ render(): JSX.Element | null;
58
+ }
59
+
60
+ export { Guideline, type GuidelineConfig };
@@ -0,0 +1,60 @@
1
+ import { BasePlugin } from '@knotx/core';
2
+
3
+ interface GuidelineConfig {
4
+ /**
5
+ * 对齐阈值(像素)
6
+ * @default 4
7
+ */
8
+ threshold?: number;
9
+ /**
10
+ * 辅助线颜色
11
+ * @default '#007ACC'
12
+ */
13
+ color?: string;
14
+ /**
15
+ * 辅助线宽度
16
+ * @default 1
17
+ */
18
+ lineWidth?: number;
19
+ /**
20
+ * 是否启用
21
+ * @default true
22
+ */
23
+ enabled?: boolean;
24
+ /**
25
+ * 是否启用自动吸附
26
+ * @default true
27
+ */
28
+ enableSnap?: boolean;
29
+ }
30
+ interface GuidelineItem {
31
+ type: 'vertical' | 'horizontal';
32
+ position: number;
33
+ label?: string;
34
+ }
35
+ declare module '@knotx/core' {
36
+ interface PluginData {
37
+ guideline: {
38
+ guidelines: GuidelineItem[];
39
+ enabled: boolean;
40
+ };
41
+ }
42
+ }
43
+ declare class Guideline extends BasePlugin<'guideline', GuidelineConfig> {
44
+ name: "guideline";
45
+ private config;
46
+ private nodes;
47
+ private container;
48
+ private getNode;
49
+ private addNodePipe;
50
+ private transform;
51
+ guidelines: GuidelineItem[];
52
+ draggingNodeId: string | null;
53
+ init(config?: GuidelineConfig): void;
54
+ private handleDraftOperation;
55
+ private calculateGuidelines;
56
+ private clearGuidelines;
57
+ render(): JSX.Element | null;
58
+ }
59
+
60
+ export { Guideline, type GuidelineConfig };
@@ -0,0 +1,60 @@
1
+ import { BasePlugin } from '@knotx/core';
2
+
3
+ interface GuidelineConfig {
4
+ /**
5
+ * 对齐阈值(像素)
6
+ * @default 4
7
+ */
8
+ threshold?: number;
9
+ /**
10
+ * 辅助线颜色
11
+ * @default '#007ACC'
12
+ */
13
+ color?: string;
14
+ /**
15
+ * 辅助线宽度
16
+ * @default 1
17
+ */
18
+ lineWidth?: number;
19
+ /**
20
+ * 是否启用
21
+ * @default true
22
+ */
23
+ enabled?: boolean;
24
+ /**
25
+ * 是否启用自动吸附
26
+ * @default true
27
+ */
28
+ enableSnap?: boolean;
29
+ }
30
+ interface GuidelineItem {
31
+ type: 'vertical' | 'horizontal';
32
+ position: number;
33
+ label?: string;
34
+ }
35
+ declare module '@knotx/core' {
36
+ interface PluginData {
37
+ guideline: {
38
+ guidelines: GuidelineItem[];
39
+ enabled: boolean;
40
+ };
41
+ }
42
+ }
43
+ declare class Guideline extends BasePlugin<'guideline', GuidelineConfig> {
44
+ name: "guideline";
45
+ private config;
46
+ private nodes;
47
+ private container;
48
+ private getNode;
49
+ private addNodePipe;
50
+ private transform;
51
+ guidelines: GuidelineItem[];
52
+ draggingNodeId: string | null;
53
+ init(config?: GuidelineConfig): void;
54
+ private handleDraftOperation;
55
+ private calculateGuidelines;
56
+ private clearGuidelines;
57
+ render(): JSX.Element | null;
58
+ }
59
+
60
+ export { Guideline, type GuidelineConfig };
package/dist/index.js ADDED
@@ -0,0 +1,260 @@
1
+ import { jsx } from '@knotx/jsx/jsx-runtime';
2
+ import { Layer, isDraftOperation, use, bem, BasePlugin } from '@knotx/core';
3
+ import { inject, observable, layer, OnInit } from '@knotx/decorators';
4
+ import { tap } from 'rxjs/operators';
5
+
6
+ var __create = Object.create;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
12
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
13
+ var __typeError = (msg) => {
14
+ throw TypeError(msg);
15
+ };
16
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
17
+ var __spreadValues = (a, b) => {
18
+ for (var prop in b || (b = {}))
19
+ if (__hasOwnProp.call(b, prop))
20
+ __defNormalProp(a, prop, b[prop]);
21
+ if (__getOwnPropSymbols)
22
+ for (var prop of __getOwnPropSymbols(b)) {
23
+ if (__propIsEnum.call(b, prop))
24
+ __defNormalProp(a, prop, b[prop]);
25
+ }
26
+ return a;
27
+ };
28
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
29
+ var __decoratorStart = (base) => {
30
+ var _a2;
31
+ return [, , , __create((_a2 = base == null ? void 0 : base[__knownSymbol("metadata")]) != null ? _a2 : null)];
32
+ };
33
+ var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
34
+ var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
35
+ var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
36
+ var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
37
+ var __runInitializers = (array, flags, self, value) => {
38
+ for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
39
+ return value;
40
+ };
41
+ var __decorateElement = (array, flags, name, decorators, target, extra) => {
42
+ var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
43
+ var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
44
+ var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
45
+ var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
46
+ return __privateGet(this, extra);
47
+ }, set [name](x) {
48
+ return __privateSet(this, extra, x);
49
+ } }, name));
50
+ k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
51
+ for (var i = decorators.length - 1; i >= 0; i--) {
52
+ ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
53
+ if (k) {
54
+ ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
55
+ if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
56
+ if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
57
+ }
58
+ it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
59
+ if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
60
+ else if (typeof it !== "object" || it === null) __typeError("Object expected");
61
+ else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
62
+ }
63
+ return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
64
+ };
65
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
66
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
67
+ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
68
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
69
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
70
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
71
+ var _render_dec, _init_dec, _draggingNodeId_dec, _guidelines_dec, _transform_dec, _addNodePipe_dec, _getNode_dec, _container_dec, _nodes_dec, _a, _init;
72
+ class Guideline extends (_a = BasePlugin, _nodes_dec = [inject.nodes()], _container_dec = [inject.container()], _getNode_dec = [inject.getNode()], _addNodePipe_dec = [inject.addNodePipe()], _transform_dec = [inject.canvas.transform()], _guidelines_dec = [observable()], _draggingNodeId_dec = [observable()], _init_dec = [OnInit], _render_dec = [layer(Layer.Foreground)], _a) {
73
+ constructor() {
74
+ super(...arguments);
75
+ __runInitializers(_init, 5, this);
76
+ __publicField(this, "name", "guideline");
77
+ __publicField(this, "config", {
78
+ threshold: 4,
79
+ color: "#007ACC",
80
+ lineWidth: 1,
81
+ enabled: true,
82
+ enableSnap: true
83
+ });
84
+ __publicField(this, "nodes", __runInitializers(_init, 8, this, [])), __runInitializers(_init, 11, this);
85
+ __publicField(this, "container", __runInitializers(_init, 12, this)), __runInitializers(_init, 15, this);
86
+ __publicField(this, "getNode", __runInitializers(_init, 16, this)), __runInitializers(_init, 19, this);
87
+ __publicField(this, "addNodePipe", __runInitializers(_init, 20, this)), __runInitializers(_init, 23, this);
88
+ __publicField(this, "transform", __runInitializers(_init, 24, this)), __runInitializers(_init, 27, this);
89
+ __publicField(this, "guidelines", __runInitializers(_init, 28, this, [])), __runInitializers(_init, 31, this);
90
+ __publicField(this, "draggingNodeId", __runInitializers(_init, 32, this, null)), __runInitializers(_init, 35, this);
91
+ }
92
+ init(config = {}) {
93
+ this.config = __spreadValues(__spreadValues({}, this.config), config);
94
+ this.subscriptions.push(this.addNodePipe({
95
+ transform: () => (operation$) => {
96
+ return operation$.pipe(
97
+ tap((operation) => {
98
+ if (isDraftOperation(operation) && operation.type === "draftOperation") {
99
+ this.handleDraftOperation(operation);
100
+ } else if (operation.type === "commitDraft" || operation.type === "discardDraft") {
101
+ this.clearGuidelines();
102
+ }
103
+ })
104
+ );
105
+ }
106
+ }));
107
+ }
108
+ handleDraftOperation(operation) {
109
+ if (!this.config.enabled) {
110
+ return;
111
+ }
112
+ if (operation.operation.type === "update" && operation.operation.data.position) {
113
+ const nodeId = operation.operation.id;
114
+ const newPosition = operation.operation.data.position;
115
+ this.draggingNodeId = nodeId;
116
+ const snapResult = this.calculateGuidelines(nodeId, newPosition);
117
+ if (this.config.enableSnap && snapResult.snapPosition) {
118
+ if (snapResult.snapPosition.x !== void 0) {
119
+ operation.operation.data.position.x = snapResult.snapPosition.x;
120
+ }
121
+ if (snapResult.snapPosition.y !== void 0) {
122
+ operation.operation.data.position.y = snapResult.snapPosition.y;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ calculateGuidelines(draggingNodeId, newPosition) {
128
+ var _a2, _b, _c;
129
+ const guidelines = [];
130
+ const threshold = (_a2 = this.config.threshold) != null ? _a2 : 5;
131
+ const snapPosition = {};
132
+ const draggingNode = this.getNode({ id: draggingNodeId });
133
+ if (!draggingNode)
134
+ return { guidelines: [] };
135
+ const draggingWidth = ((_b = draggingNode.measured) == null ? void 0 : _b.width) || 0;
136
+ const draggingHeight = ((_c = draggingNode.measured) == null ? void 0 : _c.height) || 0;
137
+ const draggingLeft = newPosition.x;
138
+ const draggingRight = newPosition.x + draggingWidth;
139
+ const draggingCenterX = newPosition.x + draggingWidth / 2;
140
+ const draggingTop = newPosition.y;
141
+ const draggingBottom = newPosition.y + draggingHeight;
142
+ const draggingCenterY = newPosition.y + draggingHeight / 2;
143
+ let nearestXSnap;
144
+ let nearestYSnap;
145
+ this.nodes.forEach((node) => {
146
+ var _a3, _b2, _c2, _d;
147
+ if (node.id === draggingNodeId)
148
+ return;
149
+ const nodeLeft = node.position.x;
150
+ const nodeRight = node.position.x + (((_a3 = node.measured) == null ? void 0 : _a3.width) || 0);
151
+ const nodeCenterX = node.position.x + (((_b2 = node.measured) == null ? void 0 : _b2.width) || 0) / 2;
152
+ const nodeTop = node.position.y;
153
+ const nodeBottom = node.position.y + (((_c2 = node.measured) == null ? void 0 : _c2.height) || 0);
154
+ const nodeCenterY = node.position.y + (((_d = node.measured) == null ? void 0 : _d.height) || 0) / 2;
155
+ const checkVerticalSnap = (targetX, label, draggedX) => {
156
+ const distance = Math.abs(draggedX - targetX);
157
+ if (distance <= threshold) {
158
+ guidelines.push({
159
+ type: "vertical",
160
+ position: targetX,
161
+ label
162
+ });
163
+ const newX = targetX - (draggedX - newPosition.x);
164
+ if (!nearestXSnap || distance < nearestXSnap.distance) {
165
+ nearestXSnap = { distance, position: targetX, newX };
166
+ }
167
+ }
168
+ };
169
+ const checkHorizontalSnap = (targetY, label, draggedY) => {
170
+ const distance = Math.abs(draggedY - targetY);
171
+ if (distance <= threshold) {
172
+ guidelines.push({
173
+ type: "horizontal",
174
+ position: targetY,
175
+ label
176
+ });
177
+ const newY = targetY - (draggedY - newPosition.y);
178
+ if (!nearestYSnap || distance < nearestYSnap.distance) {
179
+ nearestYSnap = { distance, position: targetY, newY };
180
+ }
181
+ }
182
+ };
183
+ checkVerticalSnap(nodeLeft, "left", draggingLeft);
184
+ checkVerticalSnap(nodeRight, "right", draggingRight);
185
+ checkVerticalSnap(nodeCenterX, "center", draggingCenterX);
186
+ checkVerticalSnap(nodeRight, "edge", draggingLeft);
187
+ checkVerticalSnap(nodeLeft, "edge", draggingRight);
188
+ checkHorizontalSnap(nodeTop, "top", draggingTop);
189
+ checkHorizontalSnap(nodeBottom, "bottom", draggingBottom);
190
+ checkHorizontalSnap(nodeCenterY, "center", draggingCenterY);
191
+ checkHorizontalSnap(nodeBottom, "edge", draggingTop);
192
+ checkHorizontalSnap(nodeTop, "edge", draggingBottom);
193
+ });
194
+ const uniqueGuidelines = guidelines.filter((guideline, index) => {
195
+ return guidelines.findIndex(
196
+ (g) => g.type === guideline.type && Math.abs(g.position - guideline.position) < 1
197
+ ) === index;
198
+ });
199
+ if (nearestXSnap) {
200
+ snapPosition.x = nearestXSnap.newX;
201
+ }
202
+ if (nearestYSnap) {
203
+ snapPosition.y = nearestYSnap.newY;
204
+ }
205
+ this.guidelines = uniqueGuidelines;
206
+ return {
207
+ guidelines: uniqueGuidelines,
208
+ snapPosition: Object.keys(snapPosition).length > 0 ? snapPosition : void 0
209
+ };
210
+ }
211
+ clearGuidelines() {
212
+ this.guidelines = [];
213
+ this.draggingNodeId = null;
214
+ }
215
+ render() {
216
+ const { guidelines, transform, config } = use(() => ({
217
+ guidelines: this.guidelines,
218
+ transform: this.transform,
219
+ config: this.config
220
+ }));
221
+ if (!config.enabled || guidelines.length === 0) {
222
+ return null;
223
+ }
224
+ return /* @__PURE__ */ jsx("div", { className: bem("guideline", "container"), children: guidelines.map((guideline, index) => /* @__PURE__ */ jsx(
225
+ "div",
226
+ {
227
+ className: bem("guideline", "line", guideline.type),
228
+ style: __spreadValues({
229
+ position: "absolute",
230
+ backgroundColor: config.color,
231
+ pointerEvents: "none"
232
+ }, guideline.type === "vertical" ? {
233
+ left: guideline.position * transform.scale + transform.positionX,
234
+ top: 0,
235
+ width: config.lineWidth,
236
+ height: this.container.height
237
+ } : {
238
+ left: 0,
239
+ top: guideline.position * transform.scale + transform.positionY,
240
+ width: this.container.width,
241
+ height: config.lineWidth
242
+ })
243
+ },
244
+ `${guideline.type}-${guideline.position}-${index}`
245
+ )) });
246
+ }
247
+ }
248
+ _init = __decoratorStart(_a);
249
+ __decorateElement(_init, 1, "init", _init_dec, Guideline);
250
+ __decorateElement(_init, 1, "render", _render_dec, Guideline);
251
+ __decorateElement(_init, 5, "nodes", _nodes_dec, Guideline);
252
+ __decorateElement(_init, 5, "container", _container_dec, Guideline);
253
+ __decorateElement(_init, 5, "getNode", _getNode_dec, Guideline);
254
+ __decorateElement(_init, 5, "addNodePipe", _addNodePipe_dec, Guideline);
255
+ __decorateElement(_init, 5, "transform", _transform_dec, Guideline);
256
+ __decorateElement(_init, 5, "guidelines", _guidelines_dec, Guideline);
257
+ __decorateElement(_init, 5, "draggingNodeId", _draggingNodeId_dec, Guideline);
258
+ __decoratorMetadata(_init, Guideline);
259
+
260
+ export { Guideline };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@knotx/plugins-guideline",
3
+ "version": "0.0.1",
4
+ "description": "Guideline Plugin for Knotx",
5
+ "author": "boenfu",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/boenfu/knotx#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/boenfu/knotx.git",
11
+ "directory": "packages/plugins-guideline"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "main": "./dist/index.cjs",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "peerDependencies": {
31
+ "@knotx/jsx": "0.4.15",
32
+ "@knotx/plugins-canvas": "0.4.15"
33
+ },
34
+ "dependencies": {
35
+ "rxjs": "^7.8.1",
36
+ "@knotx/core": "0.4.15",
37
+ "@knotx/decorators": "0.4.15"
38
+ },
39
+ "devDependencies": {
40
+ "@knotx/build-config": "0.4.15",
41
+ "@knotx/eslint-config": "0.4.15",
42
+ "@knotx/plugins-canvas": "0.4.15",
43
+ "@knotx/typescript-config": "0.4.15",
44
+ "@knotx/jsx": "0.4.15"
45
+ },
46
+ "scripts": {
47
+ "build": "unbuild",
48
+ "dev": "unbuild --stub",
49
+ "lint": "eslint .",
50
+ "typecheck": "tsc --noEmit"
51
+ }
52
+ }